🚚 移动前端

This commit is contained in:
shufeng2012 committed 2026-08-08 19:09:36 +08:00
1 parent c8f2152d0a
commit b851a96a66
57 files changed
+214 -418

No files matched your search

-44
View File
@@ -1,44 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>用户使用条例</title>
<link rel="stylesheet" href="css/theme.css">
</head>
<body>
<div class="card" style="max-width:640px;text-align:left">
<h2>用户使用条例</h2>
<p style="font-size:13px;color:var(--fg-secondary);margin-bottom:16px">生效日期:2026 年 7 月 · 本平台为开源社区</p>
<h3 style="font-size:15px;margin:16px 0 6px">一、社区定位</h3>
<p style="font-size:14px;line-height:1.8;color:var(--fg-secondary)">本社区是一个以开源精神为基础的技术分享与交流平台,鼓励自由分享、协作共建、互相尊重。</p>
<h3 style="font-size:15px;margin:16px 0 6px">二、内容规范</h3>
<ol style="margin:0 0 0 20px;font-size:14px;line-height:1.8;color:var(--fg-secondary)">
<li>遵守国家法律法规,不得发布色情、赌博、诈骗、侵权、暴力恐怖等违法违规内容。</li>
<li>发布的内容将经 AI 审核,违规内容将被拒绝展示或删除,恕不另行通知。</li>
<li>不得利用本平台从事破坏、攻击、爬取扰序等影响服务稳定运行的行为。</li>
</ol>
<h3 style="font-size:15px;margin:16px 0 6px">三、开源精神</h3>
<ol style="margin:0 0 0 20px;font-size:14px;line-height:1.8;color:var(--fg-secondary)">
<li>尊重他人劳动成果,转载或引用请注明出处与作者。</li>
<li>鼓励以开源许可证分享作品,并遵守相应许可证条款。</li>
<li>友善交流,就事论事,不人身攻击。</li>
</ol>
<h3 style="font-size:15px;margin:16px 0 6px">四、账号责任</h3>
<p style="font-size:14px;line-height:1.8;color:var(--fg-secondary)">妥善保管账号与密码,账号下的全部行为由注册人本人负责。如发现账号被盗用,请及时通过找回密码功能重置。</p>
<h3 style="font-size:15px;margin:16px 0 6px">五、服务说明</h3>
<p style="font-size:14px;line-height:1.8;color:var(--fg-secondary)">本社区按"现状"提供服务,不对服务的持续性、可用性作担保。对违反本条例的账号,平台有权采取内容删除、功能限制、账号封禁等措施。</p>
<h3 style="font-size:15px;margin:16px 0 6px">六、条例修订</h3>
<p style="font-size:14px;line-height:1.8;color:var(--fg-secondary)">本条例可能适时修订并公示于本页面,继续使用本平台即视为接受最新版本。</p>
</div>
<footer class="footer">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
</footer>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

-53
View File
@@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes">
<title>树菜单 · 文件浏览</title>
<link rel="stylesheet" href="/css/view.css">
</head>
<body>
<header class="header">
<h1>📁 树菜单 · 文件浏览</h1>
<nav class="header-actions">
<a href="/" title="根目录">🏠</a>
<a href="#" id="btnRefresh" title="刷新">🔄</a>
</nav>
</header>
<nav class="breadcrumb" id="breadcrumb">
<a href="/">根目录</a>
</nav>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索文件…(快捷键 / 或 Ctrl+F)" autocomplete="off">
</div>
<div class="stats">
<span>📊 共 <span id="totalItems">0</span> 项</span>
<span>📁 <span id="totalDirs">0</span></span>
<span>📄 <span id="totalFiles">0</span></span>
<span>🎬 <span id="totalVideos">0</span></span>
<span>🎵 <span id="totalAudios">0</span></span>
</div>
<main class="container">
<div class="loading" id="loading">
<span class="spinner">⏳</span>
<div>加载中...</div>
</div>
<div id="fileList" class="file-grid"></div>
<div class="empty" id="empty" style="display:none;">
<span class="icon">📭</span>
<div>此目录为空</div>
</div>
</main>
<footer class="footer">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
</footer>
<script src="/js/validation.js"></script>
<script src="/js/view.js"></script>
</body>
</html>
-38
View File
@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<!-- viewport-fit=cover:配合 safe-area 适配刘海屏;maximum-scale 防输入框聚焦自动缩放 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no,viewport-fit=cover">
<title>aibox 智能体</title>
<link rel="stylesheet" href="css/theme.css">
<link rel="stylesheet" href="css/aibox.css">
</head>
<body>
<div id="app">
<header id="topbar">
<span class="title">🤖 aibox 智能体</span>
<span id="conn-status" class="disconnected"><span class="dot"></span><b id="status-text">未连接</b></span>
<button id="task-toggle" type="button">📋 任务<span id="task-badge"></span></button>
<button id="conn-btn" type="button">连接</button>
</header>
<div id="admin-tip" style="display:none"></div>
<main id="chat"></main>
<div id="input-bar">
<button id="interrupt-btn" type="button" style="display:none" title="中断当前生成">⏸</button>
<!-- enterkeyhint=send:移动端软键盘回车键显示"发送" -->
<textarea id="input" rows="1" enterkeyhint="send" placeholder="输入消息,/help 查看命令"></textarea>
<button id="send-btn" type="button">发送</button>
</div>
</div>
<!-- 任务面板:始终为抽屉式覆盖层(移动端唯一形态) -->
<aside id="task-panel">
<header>任务面板<button id="task-close" type="button" aria-label="关闭">×</button></header>
<div id="task-list"></div>
</aside>
<div id="backdrop"></div>
<script src="js/markdown-it-12_0_0.js"></script>
<script src="js/token_manager.js"></script>
<script src="js/aibox.js"></script>
</body>
</html>
-530
View File
@@ -1,530 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes, viewport-fit=cover">
<title>中国传统色彩 - 移动端适配版</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 12px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 24px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 32px 20px;
text-align: center;
}
.header h1 {
font-size: 1.9rem;
margin-bottom: 8px;
font-weight: 500;
letter-spacing: -0.3px;
}
.header p {
font-size: 0.9rem;
opacity: 0.9;
}
/* 移动端选项卡优化 */
.tabs {
display: flex;
background: #f8f9fa;
border-bottom: 2px solid #e9ecef;
position: sticky;
top: 0;
z-index: 10;
background: white;
}
.tab {
flex: 1;
padding: 14px 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.95rem;
font-weight: 500;
color: #6c757d;
border: none;
background: none;
position: relative;
white-space: nowrap;
}
.tab.active {
color: #667eea;
font-weight: 600;
}
.tab.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #667eea, #764ba2);
border-radius: 3px 3px 0 0;
}
.tab-content {
display: none;
padding: 20px 16px;
}
.tab-content.active {
display: block;
animation: fadeIn 0.25s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
/* 移动端:色彩网格改为2列布局,卡片更易点击 */
.color-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 14px;
}
/* 针对小屏手机强制2列 */
@media (max-width: 480px) {
.color-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.header h1 {
font-size: 1.6rem;
}
.tab {
font-size: 0.85rem;
padding: 12px 4px;
}
}
/* 中等屏幕自动调整 */
@media (min-width: 481px) and (max-width: 768px) {
.color-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 769px) {
.color-grid {
grid-template-columns: repeat(5, 1fr);
}
}
.color-card {
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.color-card:active {
transform: scale(0.97);
transition: 0.05s;
}
.color-swatch {
height: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
font-weight: 500;
color: white;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
background-size: cover;
}
.color-info {
padding: 10px;
}
.color-name {
font-size: 0.85rem;
font-weight: 500;
color: #1e293b;
margin-bottom: 4px;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
.color-code {
font-size: 0.7rem;
color: #6c757d;
font-family: monospace;
}
/* Toast 提示优化 - 移动端居中底部 */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: #1e293b;
color: white;
padding: 12px 24px;
border-radius: 40px;
font-size: 0.9rem;
font-weight: 500;
opacity: 0;
transition: all 0.25s ease;
z-index: 1000;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(4px);
background: rgba(0, 0, 0, 0.85);
white-space: nowrap;
pointer-events: none;
}
@media (max-width: 480px) {
.toast {
white-space: normal;
max-width: 80%;
text-align: center;
padding: 10px 18px;
font-size: 0.85rem;
}
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* 配色方案区域 - 移动端卡片布局优化 */
#palettes-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
@media (max-width: 640px) {
#palettes-container {
grid-template-columns: 1fr;
gap: 18px;
}
}
.palette-section {
margin-bottom: 0;
background: #fefefe;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: 0.2s;
}
.palette-title {
font-size: 0.95rem;
font-weight: 600;
color: #334155;
margin: 0;
padding: 12px 16px 8px 16px;
background: #fafcff;
border-bottom: 1px solid #eef2f6;
}
.color-palette {
display: flex;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 0 0 16px 16px;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
.color-palette:active {
transform: scale(0.98);
}
.palette-color {
flex: 1;
min-width: 0; /* 防止flex溢出 */
height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 0.7rem;
font-weight: 500;
transition: 0.1s;
}
/* 移动端让颜色代码折行更友好 */
.palette-color span:first-child {
font-size: 0.7rem;
font-family: monospace;
background: rgba(0,0,0,0.3);
padding: 2px 6px;
border-radius: 30px;
backdrop-filter: blur(2px);
}
.palette-color span:last-child {
font-size: 0.6rem;
opacity: 0.9;
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 20px;
}
/* 说明文字区域移动端优化 */
.palettes-desc {
background: #f1f5f9;
margin: 0 16px 16px 16px;
padding: 12px 14px;
border-radius: 20px;
font-size: 0.8rem;
color: #475569;
line-height: 1.4;
}
.palettes-desc strong {
color: #667eea;
}
/* 增加触摸反馈 */
.color-card, .color-palette {
cursor: pointer;
}
/* 滚动条隐藏但保持功能 (移动端美观) */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎨 中国传统色彩</h1>
<p>轻触颜色块,复制色值</p>
</div>
<div class="tabs">
<button class="tab active" data-tab="traditional">中国传统色</button>
<button class="tab" data-tab="chinese">中文色彩</button>
<button class="tab" data-tab="palettes">色彩搭配</button>
</div>
<div class="tab-content active" id="traditional">
<div class="color-grid" id="traditional-grid"></div>
</div>
<div class="tab-content" id="chinese">
<div class="color-grid" id="chinese-grid"></div>
</div>
<div class="tab-content" id="palettes">
<div class="palettes-desc">
💡 <strong>主色</strong>(第一色)品牌核心色 · <strong>辅助色</strong>(第二色)点缀强调 · <strong>中性色</strong>(第三色)文字/背景<br>
✨ 点击配色块,一键复制整套色彩代码(三个色值)。
</div>
<div id="palettes-container"></div>
</div>
</div>
<div class="toast" id="toast">🎨 颜色代码已复制</div>
<script>
// 原始数据保持不变(完全未修改接口/功能)
const traditionalColors = [
{ name: '蔚蓝', hex: '#70f3ff' }, { name: '蓝', hex: '#44cef6' }, { name: '碧蓝', hex: '#3eede7' }, { name: '石青', hex: '#1685a9' }, { name: '靛青', hex: '#177cb0' },
{ name: '靛蓝', hex: '#065279' }, { name: '花青', hex: '#003472' }, { name: '宝蓝', hex: '#4b5cc4' }, { name: '蓝灰色', hex: '#a1afc9' }, { name: '藏青', hex: '#2e4e7e' },
{ name: '藏蓝', hex: '#3b2e7e' }, { name: '黛', hex: '#4a4266' }, { name: '黛绿', hex: '#426666' }, { name: '黛蓝', hex: '#425066' }, { name: '黛紫', hex: '#574266' },
{ name: '紫色', hex: '#8d4bbb' }, { name: '紫酱', hex: '#815463' }, { name: '酱紫', hex: '#815476' }, { name: '紫檀', hex: '#4c221b' }, { name: '绀青', hex: '#003371' },
{ name: '紫棠', hex: '#56004f' }, { name: '青莲', hex: '#801dae' }, { name: '群青', hex: '#4c8dae' }, { name: '雪青', hex: '#b0a4e3' }, { name: '丁香色', hex: '#cca4e3' },
{ name: '藕色', hex: '#edd1d8' }, { name: '藕荷色', hex: '#e4c6d0' }, { name: '朱砂', hex: '#ff461f' }, { name: '火红', hex: '#ff2d51' }, { name: '朱膘', hex: '#f36838' },
{ name: '妃色', hex: '#ed5736' }, { name: '洋红', hex: '#ff4777' }, { name: '品红', hex: '#f00056' }, { name: '粉红', hex: '#ffb3a7' }, { name: '桃红', hex: '#f47983' },
{ name: '海棠红', hex: '#db5a6b' }, { name: '樱桃色', hex: '#c93756' }, { name: '酡颜', hex: '#f9906f' }, { name: '银红', hex: '#f05654' }, { name: '大红', hex: '#ff2121' },
{ name: '石榴红', hex: '#f20c00' }, { name: '绛紫', hex: '#8c4356' }, { name: '绯红', hex: '#c83c23' }, { name: '胭脂', hex: '#9d2933' }, { name: '朱红', hex: '#ff4c00' },
{ name: '丹', hex: '#ff4e20' }, { name: '彤', hex: '#f35336' }, { name: '酡红', hex: '#dc3023' }, { name: '炎', hex: '#ff3300' }, { name: '茜色', hex: '#cb3a56' },
{ name: '绾', hex: '#a98175' }, { name: '檀', hex: '#b36d61' }, { name: '嫣红', hex: '#ef7a82' }, { name: '枣红', hex: '#c32136' }, { name: '殷红', hex: '#be002f' },
{ name: '赫赤', hex: '#c91f37' }, { name: '银朱', hex: '#bf242a' }, { name: '赤', hex: '#c3272b' }, { name: '栗色', hex: '#60281e' }, { name: '玄色', hex: '#622a1d' },
{ name: '松花色', hex: '#bce672' }, { name: '柳黄', hex: '#c9dd22' }, { name: '嫩绿', hex: '#bddd22' }, { name: '柳绿', hex: '#afdd22' }, { name: '葱黄', hex: '#a3d900' },
{ name: '葱绿', hex: '#9ed900' }, { name: '豆绿', hex: '#9ed048' }, { name: '豆青', hex: '#96ce54' }, { name: '油绿', hex: '#00bc12' }, { name: '葱倩', hex: '#0eb83a' },
{ name: '葱青', hex: '#0eb83a' }, { name: '青葱', hex: '#0aa344' }, { name: '石绿', hex: '#16a951' }, { name: '松柏绿', hex: '#21a675' }, { name: '松花绿', hex: '#057748' },
{ name: '绿沈', hex: '#0c8918' }, { name: '绿色', hex: '#00e500' }, { name: '草绿', hex: '#40de5a' }, { name: '青翠', hex: '#00e079' }, { name: '青色', hex: '#00e09e' },
{ name: '翡翠色', hex: '#3de1ad' }, { name: '碧绿', hex: '#2add9c' }, { name: '玉色', hex: '#2edfa3' }, { name: '缥', hex: '#7fecad' }, { name: '艾绿', hex: '#a4e2c6' },
{ name: '碧色', hex: '#1bd1a5' }, { name: '青碧', hex: '#48c0a3' }, { name: '铜绿', hex: '#549688' }, { name: '竹青', hex: '#789262' }, { name: '墨灰', hex: '#758a99' },
{ name: '墨色', hex: '#50616d' }, { name: '鸦青', hex: '#424c50' }, { name: '黯', hex: '#41555d' }, { name: '樱草色', hex: '#eaff56' }, { name: '鹅黄', hex: '#fff143' },
{ name: '鸭黄', hex: '#faff72' }, { name: '杏黄', hex: '#ffa631' }, { name: '橙黄', hex: '#ffa400' }, { name: '橙色', hex: '#fa8c35' }, { name: '杏红', hex: '#ff8c31' },
{ name: '橘黄', hex: '#ff8936' }, { name: '橘红', hex: '#ff7500' }, { name: '藤黄', hex: '#ffb61e' }, { name: '姜黄', hex: '#ffc773' }, { name: '雌黄', hex: '#ffc64b' },
{ name: '赤金', hex: '#f2be45' }, { name: '缃色', hex: '#f0c239' }, { name: '雄黄', hex: '#e9bb1d' }, { name: '秋香色', hex: '#d9b611' }, { name: '金色', hex: '#eacd76' },
{ name: '牙色', hex: '#eedeb0' }, { name: '枯黄', hex: '#d3b17d' }, { name: '黄栌', hex: '#e29c45' }, { name: '乌金', hex: '#a78e44' }, { name: '昏黄', hex: '#c89b40' },
{ name: '棕黄', hex: '#ae7000' }, { name: '琥珀', hex: '#ca6924' }, { name: '棕色', hex: '#b25d25' }, { name: '茶色', hex: '#b35c44' }, { name: '棕红', hex: '#9b4400' },
{ name: '赭', hex: '#9c5333' }, { name: '驼色', hex: '#a88462' }, { name: '秋色', hex: '#896c39' }, { name: '棕绿', hex: '#827100' }, { name: '褐色', hex: '#6e511e' },
{ name: '棕黑', hex: '#7c4b00' }, { name: '赭色', hex: '#955539' }, { name: '赭石', hex: '#845a33' }, { name: '精白', hex: '#ffffff' }, { name: '银白', hex: '#e9e7ef' },
{ name: '铅白', hex: '#f0f0f4' }, { name: '霜色', hex: '#e9f1f6' }, { name: '雪白', hex: '#f0fcff' }, { name: '莹白', hex: '#e3f9fd' }, { name: '月白', hex: '#d6ecf0' },
{ name: '象牙白', hex: '#fffbf0' }, { name: '缟', hex: '#f2ecde' }, { name: '鱼肚白', hex: '#fcefe8' }, { name: '白粉', hex: '#fff2df' }, { name: '荼白', hex: '#f3f9f1' },
{ name: '鸭卵青', hex: '#e0eee8' }, { name: '素', hex: '#e0f0e9' }, { name: '青白', hex: '#c0ebd7' }, { name: '蟹壳青', hex: '#bbcdc5' }, { name: '花白', hex: '#c2ccd0' },
{ name: '老银', hex: '#bacac6' }, { name: '灰色', hex: '#808080' }, { name: '苍色', hex: '#75878a' }, { name: '水色', hex: '#88ada6' }, { name: '黝', hex: '#6b6882' },
{ name: '乌色', hex: '#725e82' }, { name: '玄青', hex: '#3d3b4f' }, { name: '乌黑', hex: '#392f41' }, { name: '黎', hex: '#75664d' }, { name: '黧', hex: '#5d513c' },
{ name: '黝黑', hex: '#665757' }, { name: '缁色', hex: '#493131' }, { name: '煤黑', hex: '#312520' }, { name: '漆黑', hex: '#161823' }, { name: '黑色', hex: '#000000' }
];
const chineseColors = [
{ name: '鸨色', hex: '#f7acbc' }, { name: '踯躅色', hex: '#ef5b9c' }, { name: '桜色', hex: '#feeeed' }, { name: '蔷薇色', hex: '#f05b72' }, { name: '韩红', hex: '#f15b6c' },
{ name: '珊瑚色', hex: '#f8aba6' }, { name: '红梅色', hex: '#f69c9f' }, { name: '桃色', hex: '#f58f98' }, { name: '薄柿', hex: '#ca8687' }, { name: '薄红梅', hex: '#f391a9' },
{ name: '曙色', hex: '#bd6758' }, { name: '红色', hex: '#d71345' }, { name: '赤丹', hex: '#d64f44' }, { name: '红赤', hex: '#d93a49' }, { name: '臙脂色', hex: '#b3424a' },
{ name: '真赭', hex: '#c76968' }, { name: '今様色', hex: '#bb505d' }, { name: '梅染', hex: '#987165' }, { name: '退红色', hex: '#ac6767' }, { name: '苏芳', hex: '#973c3f' },
{ name: '茜色', hex: '#b22c46' }, { name: '红', hex: '#a7324a' }, { name: '银朱', hex: '#aa363d' }, { name: '赤', hex: '#ed1941' }, { name: '朱色', hex: '#f26522' },
{ name: '洗朱', hex: '#d2553d' }, { name: '红桦色', hex: '#b4534b' }, { name: '红绯', hex: '#ef4136' }, { name: '桦色', hex: '#c63c26' }, { name: '铅丹色', hex: '#f3715c' },
{ name: '赭', hex: '#a7573b' }, { name: '绯色', hex: '#aa2116' }, { name: '丹', hex: '#b64533' }, { name: '土', hex: '#b54334' }, { name: '焦香', hex: '#853f04' },
{ name: '真红', hex: '#840228' }, { name: '绯', hex: '#7a1723' }, { name: '红海老茶', hex: '#a03939' }, { name: '浅苏芳', hex: '#8a2e3b' }, { name: '鸢色', hex: '#8e453f' },
{ name: '小豆色', hex: '#8f4b4a' }, { name: '弁柄色', hex: '#892f1b' }, { name: '栗梅', hex: '#6b2c25' }, { name: '海老茶', hex: '#733a31' }, { name: '深绯', hex: '#54211d' },
{ name: '赤铜色', hex: '#78331e' }, { name: '赤褐色', hex: '#53261f' }, { name: '金赤', hex: '#f15a22' }, { name: '赤茶', hex: '#b4533c' }, { name: '赤锖色', hex: '#84331f' },
{ name: '黄丹', hex: '#f47a55' }, { name: '赤橙', hex: '#f15a22' }, { name: '柿色', hex: '#f3704b' }, { name: '肉桂色', hex: '#da765b' }, { name: '炼瓦色', hex: '#ae5039' },
{ name: '锖色', hex: '#6a3427' }, { name: '桧皮色', hex: '#8f4b38' }, { name: '栗色', hex: '#8e3e1f' }, { name: '黄赤', hex: '#f36c21' }, { name: '代赭', hex: '#b4532a' },
{ name: '骆驼色', hex: '#b7704f' }, { name: '黄茶', hex: '#de773f' }, { name: '洗柿', hex: '#c99979' }, { name: '赤白橡', hex: '#deab8a' }, { name: '肌色', hex: '#fedcbd' },
{ name: '橙色', hex: '#f47920' }, { name: '灰茶', hex: '#905a3d' }, { name: '茶色', hex: '#8f4b2e' }, { name: '桦茶色', hex: '#87481f' }, { name: '枯茶', hex: '#5f3c23' },
{ name: '焦茶', hex: '#6b473c' }, { name: '柑子色', hex: '#faa755' }, { name: '杏色', hex: '#fab27b' }, { name: '蜜柑色', hex: '#f58220' }, { name: '褐色', hex: '#843900' },
{ name: '路考茶', hex: '#905d1d' }, { name: '饴色', hex: '#8a5d19' }, { name: '丁子色', hex: '#8c531b' }, { name: '丁子茶', hex: '#826858' }, { name: '黄栌', hex: '#64492b' },
{ name: '土器色', hex: '#ae6642' }, { name: '黄枯茶', hex: '#56452d' }, { name: '狐色', hex: '#96582a' }, { name: '黄橡', hex: '#705628' }, { name: '银煤竹', hex: '#4a3113' },
{ name: '涅色', hex: '#412f1f' }, { name: '胡桃色', hex: '#845538' }, { name: '香色', hex: '#8e7437' }, { name: '国防色', hex: '#69541b' }, { name: '练色', hex: '#d5c59f' },
{ name: '肉色', hex: '#cd9a5b' }, { name: '人色', hex: '#cd9a5b' }, { name: '土色', hex: '#b36d41' }, { name: '小麦色', hex: '#df9464' }, { name: '琥珀色', hex: '#b76f40' },
{ name: '木兰色', hex: '#ad8b3d' }, { name: '栀子色', hex: '#dea32c' }, { name: '朽叶', hex: '#d1923f' }, { name: '萱草色', hex: '#c88400' }, { name: '黄金', hex: '#c37e00' },
{ name: '金色', hex: '#c37e00' }, { name: '金茶', hex: '#e0861a' }, { name: '卵色', hex: '#ffce7b' }, { name: '山吹色', hex: '#fcaf17' }, { name: '黄土色', hex: '#ba8448' },
{ name: '朽叶色', hex: '#896a45' }, { name: '空五倍子色', hex: '#76624c' }, { name: '莺茶', hex: '#6d5826' }, { name: '向日葵色', hex: '#ffc20e' }, { name: '郁金色', hex: '#fdb933' },
{ name: '砂色', hex: '#d3c6a6' }, { name: '芥子色', hex: '#c7a252' }, { name: '淡黄', hex: '#dec674' }, { name: '亜麻色', hex: '#b69968' }, { name: '枯色', hex: '#c1a173' }
];
const palettes = [
{ title: '清新自然', colors: ['#FFFFCC', '#CCFFFF', '#FFCCCC'] }, { title: '雅致灰调', colors: ['#99CCCC', '#FFCC99', '#FFCCCC'] }, { title: '粉紫梦境', colors: ['#FF9999', '#996699', '#FFCCCC'] },
{ title: '大地色系', colors: ['#CC9999', '#FFFFCC', '#CCCC99'] }, { title: '柔和暖阳', colors: ['#FFCCCC', '#FFFF99', '#CCCCFF'] }, { title: '海蓝印象', colors: ['#0099CC', '#CCCCCC', '#FF6666'] },
{ title: '珊瑚橘粉', colors: ['#FF9966', '#FF6666', '#FFCCCC'] }, { title: '木质复古', colors: ['#CC9966', '#666666', '#CC9999'] }, { title: '翠绿春意', colors: ['#FF6666', '#FFFF66', '#99CC66'] },
{ title: '深海静寂', colors: ['#CC3333', '#CCCCCC', '#003366'] }, { title: '紫罗兰夜', colors: ['#993333', '#CCCC00', '#663366'] }, { title: '燕麦拿铁', colors: ['#CCCC99', '#666666', '#CC9999'] },
{ title: '柠檬水岸', colors: ['#FF6666', '#FFFF00', '#0066CC'] }, { title: '红黑经典', colors: ['#CC0033', '#333333', '#CCCC00'] }, { title: '森林与酒', colors: ['#336633', '#990033', '#FFCC99'] }
];
function showToast(msg) {
const toast = document.getElementById('toast');
toast.textContent = msg || '🎨 颜色代码已复制';
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1800);
}
function copyToClipboard(text, customMsg) {
navigator.clipboard.writeText(text).then(() => {
showToast(customMsg || `✅ 已复制: ${text}`);
}).catch(() => {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast(customMsg || `📋 已复制: ${text}`);
});
}
function getContrastColor(hex) {
let r = parseInt(hex.slice(1, 3), 16);
let g = parseInt(hex.slice(3, 5), 16);
let b = parseInt(hex.slice(5, 7), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 128 ? '#1e1e2f' : '#ffffff';
}
function createColorCard(color) {
const card = document.createElement('div');
card.className = 'color-card';
card.onclick = () => copyToClipboard(color.hex, `🎨 ${color.name} → ${color.hex}`);
const textColor = getContrastColor(color.hex);
card.innerHTML = `
<div class="color-swatch" style="background: ${color.hex}; color: ${textColor};">${color.hex}</div>
<div class="color-info">
<div class="color-name">${color.name}</div>
<div class="color-code">${color.hex}</div>
</div>
`;
return card;
}
function createPalette(palette) {
const section = document.createElement('div');
section.className = 'palette-section';
const title = document.createElement('div');
title.className = 'palette-title';
title.textContent = palette.title;
const paletteDiv = document.createElement('div');
paletteDiv.className = 'color-palette';
paletteDiv.onclick = () => {
const allCodes = palette.colors.join(' ');
copyToClipboard(allCodes, `🎨 配色方案「${palette.title}」已复制: ${allCodes}`);
};
const roles = ['主色', '辅色', '中性'];
palette.colors.forEach((color, idx) => {
const colorDiv = document.createElement('div');
colorDiv.className = 'palette-color';
colorDiv.style.background = color;
colorDiv.style.color = getContrastColor(color);
const codeSpan = document.createElement('span');
codeSpan.textContent = color;
const roleSpan = document.createElement('span');
roleSpan.textContent = roles[idx] || '';
colorDiv.appendChild(codeSpan);
colorDiv.appendChild(roleSpan);
paletteDiv.appendChild(colorDiv);
});
section.appendChild(title);
section.appendChild(paletteDiv);
return section;
}
function init() {
const traditionalGrid = document.getElementById('traditional-grid');
traditionalColors.forEach(c => traditionalGrid.appendChild(createColorCard(c)));
const chineseGrid = document.getElementById('chinese-grid');
chineseColors.forEach(c => chineseGrid.appendChild(createColorCard(c)));
const palettesContainer = document.getElementById('palettes-container');
palettes.forEach(p => palettesContainer.appendChild(createPalette(p)));
}
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const tabId = tab.dataset.tab;
document.getElementById(tabId).classList.add('active');
});
});
init();
</script>
</body>
</html>
-208
View File
@@ -1,208 +0,0 @@
/* ===== 移动端全屏应用布局(覆盖 theme.css 的居中卡片页默认) ===== */
html, body { height: 100%; }
body {
display: block;
padding: 0;
height: 100dvh; /* 移动端动态视口高度,规避地址栏遮挡 */
overflow: hidden;
overscroll-behavior: none; /* 禁止下拉刷新/回弹手势干扰聊天滚动 */
}
#app {
display: flex;
flex-direction: column;
height: 100dvh;
background: var(--bg);
}
/* 移动端触控优化:去点击高亮、去双击缩放延迟 */
button, textarea { -webkit-tap-highlight-color: transparent; }
button { touch-action: manipulation; }
/* ===== 顶栏 ===== */
#topbar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
padding-top: calc(10px + env(safe-area-inset-top));
background: var(--card);
border-bottom: 1px solid var(--border-light);
flex: none;
}
#topbar .title { font-size: 1rem; font-weight: 700; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#conn-status { display: flex; align-items: center; gap: 5px; font-size: 0.75rem; color: var(--fg-secondary); flex: none; }
#conn-status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }
#conn-status.connected .dot { background: var(--success); box-shadow: 0 0 6px var(--success); }
#conn-status.connecting .dot { background: var(--warning); }
#conn-status.disconnected .dot { background: var(--error); }
#topbar button { flex: none; padding: 0.45rem 0.8rem; font-size: 0.8rem; min-height: 36px; }
#task-toggle { background: var(--accent-soft); color: var(--accent-dark); position: relative; }
#task-badge {
display: none;
position: absolute;
top: -6px; right: -6px;
min-width: 16px; height: 16px;
border-radius: 8px;
background: var(--error);
color: #fff;
font-size: 0.62rem;
line-height: 16px;
text-align: center;
padding: 0 4px;
}
#conn-btn { background: var(--accent-soft); color: var(--accent-dark); }
#conn-btn.on { background: var(--error-bg); color: var(--error); }
/* ===== 非管理员提示条 ===== */
#admin-tip {
flex: none;
padding: 8px 12px;
font-size: 0.78rem;
background: var(--error-bg);
color: var(--error);
text-align: center;
}
/* ===== 聊天区 ===== */
#chat {
flex: 1;
overflow-y: auto;
padding: 14px 12px;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain; /* 滚动到底不连带拉动 body */
}
.msg { margin-bottom: 12px; overflow-wrap: break-word; }
.msg-user {
margin-left: 15%;
padding: 0.65rem 0.9rem;
background: var(--accent-soft);
border-radius: var(--radius-md) var(--radius-md) 4px var(--radius-md);
font-size: 0.92rem;
white-space: pre-wrap;
}
.msg-ai {
margin-right: 6%;
padding: 0.75rem 1rem;
background: var(--card);
border: 1px solid var(--border-light);
border-radius: var(--radius-md) var(--radius-md) var(--radius-md) 4px;
box-shadow: var(--shadow-sm);
font-size: 0.92rem;
line-height: 1.65;
}
.msg-ai p { margin: 0.4em 0; }
.msg-ai p:first-child { margin-top: 0; }
.msg-ai p:last-child { margin-bottom: 0; }
.msg-ai pre {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0.6rem;
overflow-x: auto;
font-size: 0.8rem;
}
.msg-ai code { font-size: 0.82em; background: var(--accent-soft); border-radius: 4px; padding: 0.1em 0.3em; }
.msg-ai pre code { background: none; padding: 0; }
.msg-ai a { color: var(--accent); }
.msg-ai.streaming::after {
content: '▌';
color: var(--accent);
animation: blink 0.9s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.msg-system, .msg-error { text-align: center; font-size: 0.78rem; color: var(--fg-secondary); }
.msg-error { color: var(--error); }
/* 思考区域:灰色斜体可折叠 */
.msg-reasoning {
margin-right: 6%;
border-left: 3px solid var(--border);
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
color: var(--muted);
font-style: italic;
}
.reasoning-head {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
font-style: normal;
font-size: 0.75rem;
color: var(--fg-secondary);
min-height: 28px;
}
.reasoning-body { white-space: pre-wrap; margin-top: 4px; }
.reasoning-body.folded { display: none; }
/* ===== 输入栏 ===== */
#input-bar {
display: flex;
align-items: flex-end;
gap: 8px;
padding: 10px 12px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
background: var(--card);
border-top: 1px solid var(--border-light);
flex: none;
}
#input {
flex: 1;
min-height: 44px;
max-height: 120px;
resize: none;
font-size: 16px; /* iOS <16px 会聚焦自动缩放 */
line-height: 1.4;
padding: 0.65rem 0.9rem;
}
#send-btn { flex: none; min-height: 44px; background: var(--accent); color: #fff; }
#interrupt-btn { flex: none; min-height: 44px; min-width: 44px; padding: 0.45rem 0.7rem; background: var(--error-bg); color: var(--error); }
#send-btn:disabled, #interrupt-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* ===== 任务面板:抽屉式覆盖层 ===== */
#task-panel {
position: fixed;
top: 0; right: 0; bottom: 0;
width: min(85vw, 340px);
background: var(--card);
border-left: 1px solid var(--border-light);
box-shadow: var(--shadow-md);
transform: translateX(100%);
transition: transform 0.25s;
display: flex;
flex-direction: column;
z-index: 100;
padding-top: env(safe-area-inset-top);
}
#task-panel.open { transform: translateX(0); }
#task-panel header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
font-size: 0.9rem;
font-weight: 600;
border-bottom: 1px solid var(--border-light);
flex: none;
}
#task-close { background: none; border: none; font-size: 1.3rem; color: var(--fg-secondary); padding: 4px 8px; min-height: 36px; min-width: 36px; }
#task-list {
flex: 1;
overflow-y: auto;
padding: 10px 12px;
font-size: 0.78rem;
line-height: 1.6;
white-space: pre-wrap;
overflow-wrap: break-word;
color: var(--fg-secondary);
}
#backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s;
z-index: 99;
}
#backdrop.show { opacity: 1; pointer-events: auto; }
-125
View File
@@ -1,125 +0,0 @@
/* auth.css — 注册/重置密码页专属美化(在 theme.css 之后加载,视觉语言与首页 index.css 对齐) */
::selection {
background: var(--accent-glow);
color: var(--fg);
}
/* ===== 页面背景:点阵底纹 + 暖色环境光(固定,不随滚动) ===== */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
background-image: radial-gradient(var(--border) 1.2px, transparent 1.2px);
background-size: 26px 26px;
-webkit-mask-image: radial-gradient(ellipse 78% 68% at 50% 38%, #000 18%, transparent 72%);
mask-image: radial-gradient(ellipse 78% 68% at 50% 38%, #000 18%, transparent 72%);
pointer-events: none;
}
body::after {
content: "";
position: fixed;
left: 50%;
top: 32%;
width: 560px;
height: 560px;
transform: translate(-50%, -50%);
z-index: -1;
background: radial-gradient(circle, var(--accent-glow), transparent 62%);
opacity: 0.35;
pointer-events: none;
}
/* ===== 卡片:顶部品牌渐变条 + 加深阴影 ===== */
.card {
position: relative;
overflow: hidden;
box-shadow: 0 10px 30px rgba(74, 55, 40, 0.1);
}
.card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--accent), var(--accent-dark));
}
@media (prefers-color-scheme: dark) {
.card { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35); }
}
/* ===== 品牌 eyebrow + 居中标题(渐变短杠,对齐首页章节标题) ===== */
.auth-eyebrow {
text-align: center;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.26em;
text-indent: 0.26em; /* 视觉居中补偿字距 */
color: var(--accent);
margin-bottom: 0.4rem;
}
.card h2 {
text-align: center;
}
.card h2::after {
content: "";
display: block;
width: 40px;
height: 3px;
margin: 0.55rem auto 0;
border-radius: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent-dark));
opacity: 0.9;
}
/* ===== 按钮:主按钮品牌渐变 + 上浮;次按钮描边反馈 ===== */
.card input[type="submit"] {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
color: #fff;
box-shadow: 0 2px 10px rgba(232, 133, 58, 0.32);
transition: transform 0.18s, box-shadow 0.22s, opacity 0.15s;
}
.card input[type="submit"]:not(:disabled):hover {
opacity: 1;
transform: translateY(-1px);
box-shadow: 0 6px 18px rgba(232, 133, 58, 0.4);
}
.card input[type="submit"]:not(:disabled):active { transform: scale(0.96); }
.card input[type="button"] {
border: 1px solid var(--accent-glow);
transition: transform 0.18s, border-color 0.18s, opacity 0.15s;
}
.card input[type="button"]:not(:disabled):hover {
opacity: 1;
transform: translateY(-1px);
border-color: var(--accent);
}
.card input[type="button"]:not(:disabled):active { transform: scale(0.96); }
.card input[type="reset"] { transition: transform 0.18s, color 0.18s, border-color 0.18s, opacity 0.15s; }
.card input[type="reset"]:hover {
opacity: 1;
color: var(--accent);
border-color: var(--accent);
}
/* ===== 图形验证码:hover 描边 + 阴影 ===== */
.code-box .captcha-img {
transition: border-color 0.18s, box-shadow 0.18s;
}
.code-box .captcha-img:hover {
border-color: var(--accent);
box-shadow: var(--shadow-md);
}
/* ===== 入场动画 ===== */
@keyframes auth-in {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: no-preference) {
.card { animation: auth-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) both; }
}
-1
View File
@@ -1 +0,0 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}pre code.hljs{font-family: 'JetBrains Maple Mono','Fira Code','Consolas',monospace;}code{font-family: 'JetBrains Maple Mono','Fira Code','Consolas',monospace;}
-127
View File
@@ -1,127 +0,0 @@
/* dnfs.css — 树状笔记页专属美化(在 theme.css / codeblock.css 之后加载,视觉语言与首页 index.css 对齐) */
::selection {
background: var(--accent-glow);
color: var(--fg);
}
/* ===== 页面底纹:极淡点阵 + 顶部暖光,增加层次不影响阅读 ===== */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
background-image: radial-gradient(var(--border) 1px, transparent 1px);
background-size: 26px 26px;
-webkit-mask-image: linear-gradient(180deg, #000 0%, transparent 55%);
mask-image: linear-gradient(180deg, #000 0%, transparent 55%);
pointer-events: none;
}
body::after {
content: "";
position: fixed;
left: 50%;
top: -160px;
width: 720px;
height: 480px;
transform: translateX(-50%);
z-index: -1;
background: radial-gradient(ellipse, var(--accent-glow), transparent 65%);
opacity: 0.3;
pointer-events: none;
}
/* ===== 节点卡片入场:逐张淡入(JS 动态插入时播放) =====
* 防御性约定:只动画 opacity、不带 transform——祖先 transform 会成为 fixed 后代的包含块。
* 全屏特效 iframe 现已直接挂载 body(见 dnfs.js renderSpJs),不再受卡片动画影响 */
@keyframes card-in {
from { opacity: 0; }
to { opacity: 1; }
}
/* 带位移的入场仅用于不含 fixed 后代的骨架元素;
* 不含 .minimap——它自身依赖 transform: translateY(-50%) 垂直居中,动画会覆盖 */
@keyframes rise-in {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: no-preference) {
.recommend-list .card { animation: card-in 0.45s ease both; }
.recommend-list .card:nth-of-type(2) { animation-delay: 0.05s; }
.recommend-list .card:nth-of-type(3) { animation-delay: 0.1s; }
.recommend-list .card:nth-of-type(4) { animation-delay: 0.15s; }
.recommend-list .card:nth-of-type(5) { animation-delay: 0.2s; }
.recommend-list .card:nth-of-type(6) { animation-delay: 0.25s; }
.recommend-list .card:nth-of-type(7) { animation-delay: 0.3s; }
.recommend-list .card:nth-of-type(n+8) { animation-delay: 0.35s; }
/* 首屏骨架元素入场 */
.searchbox, .node-form { animation: rise-in 0.4s ease both; }
.node-form { animation-delay: 0.1s; }
.userpanel, .minimap { animation: card-in 0.4s ease both; }
}
/* ===== 搜索框:hover 升温(focus 高亮由 theme.css 提供) ===== */
.searchbox:hover {
border-color: var(--border);
box-shadow: var(--shadow-md);
}
/* ===== 节点编辑卡:品牌渐变顶条 + 聚焦光环 ===== */
.node-form {
position: relative;
overflow: hidden;
}
.node-form::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--accent), var(--accent-dark));
opacity: 0.85;
}
.node-form:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-md);
}
/* ===== 编辑卡按钮:提交渐变 / 重置柔色 / 删除警示 ===== */
#input-note-submit {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
color: #fff;
box-shadow: 0 2px 10px rgba(232, 133, 58, 0.3);
transition: transform 0.18s, box-shadow 0.22s, opacity 0.15s;
}
#input-note-submit:not(:disabled):hover {
opacity: 1;
transform: translateY(-1px);
box-shadow: 0 5px 16px rgba(232, 133, 58, 0.38);
}
#input-note-submit:not(:disabled):active { transform: scale(0.96); }
#input-note-clear {
background: var(--accent-soft);
color: var(--accent-dark);
border: 1px solid var(--accent-glow);
transition: transform 0.18s, border-color 0.18s, opacity 0.15s;
}
#input-note-clear:not(:disabled):hover {
opacity: 1;
transform: translateY(-1px);
border-color: var(--accent);
}
#input-note-clear:not(:disabled):active { transform: scale(0.96); }
#delete-note {
background: var(--error-bg);
color: var(--error);
border: 1px solid transparent;
transition: transform 0.18s, border-color 0.18s, opacity 0.15s;
}
#delete-note:not(:disabled):hover {
opacity: 1;
transform: translateY(-1px);
border-color: var(--error);
}
#delete-note:not(:disabled):active { transform: scale(0.96); }
-773
View File
@@ -1,773 +0,0 @@
/* index.css — 社区首页样式(通用主题见 theme.css)
*
* 设计原则:
* 1. 统一版心 .wrap(max-width 1024px),所有区块对齐同一内容栏
* 2. 品牌节点树 motif 贯穿 Hero / CTA / About 装饰,形成视觉呼应
* 3. 柔和的间距节奏(4.5rem 基准间距,2.25rem 组件间距)
* 4. 动效克制:入场渐显 + 品牌树描边,尊重 prefers-reduced-motion
*/
body {
align-items: stretch;
padding: 0;
}
::selection {
background: var(--accent-glow);
color: var(--fg);
}
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
/* ===== 版心 ===== */
.wrap {
width: 100%;
max-width: 1024px;
margin: 0 auto;
padding: 0 1.25rem;
}
#main-page { width: 100%; }
/* ===== 按钮 ===== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.55rem 1.3rem;
font-size: 0.88rem;
font-weight: 600;
font-family: inherit;
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
text-decoration: none;
line-height: 1.4;
transition: transform 0.2s ease, background 0.2s, border-color 0.2s, box-shadow 0.25s;
}
.btn:active { transform: scale(0.96); }
.btn-primary {
position: relative;
overflow: hidden;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
color: #fff;
box-shadow: 0 2px 8px rgba(232, 133, 58, 0.25);
}
.btn-primary::after {
content: "";
position: absolute;
top: 0; left: -80%;
width: 50%; height: 100%;
background: linear-gradient(100deg, transparent, rgba(255,255,255,0.3), transparent);
transform: skewX(-20deg);
transition: left 0.5s ease;
pointer-events: none;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 5px 16px rgba(232, 133, 58, 0.35);
}
.btn-primary:hover::after { left: 130%; }
.btn-ghost {
background: var(--accent-soft);
color: var(--accent-dark);
border-color: var(--accent-glow);
}
.btn-ghost:hover { border-color: var(--accent); transform: translateY(-1px); }
.btn-lg { padding: 0.72rem 1.8rem; font-size: 0.95rem; border-radius: 999px; letter-spacing: 0.02em; }
/* ===== 头部 ===== */
.header {
position: sticky;
top: 0.75rem;
z-index: 100;
margin-top: 0.75rem;
}
.header-inner {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: var(--radius-lg);
background: rgba(253, 246, 238, 0.75);
border: 1px solid var(--border-light);
backdrop-filter: blur(18px) saturate(1.5);
-webkit-backdrop-filter: blur(18px) saturate(1.5);
box-shadow: var(--shadow-sm);
transition: background 0.25s, border-color 0.25s, box-shadow 0.25s;
}
@media (prefers-color-scheme: dark) {
.header-inner {
background: rgba(30, 23, 18, 0.78);
border-color: var(--border);
}
}
.header h1 {
display: flex;
align-items: center;
gap: 0.45rem;
font-size: 1.1rem;
font-weight: 700;
letter-spacing: 0.01em;
cursor: pointer;
user-select: none;
transition: color 0.2s;
}
.header h1 svg { color: var(--accent); flex: none; }
.header h1:hover { color: var(--accent); }
.right-group {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.4rem;
}
#avatar {
width: 34px;
height: 34px;
border-radius: 50%;
border: 1.5px solid var(--border);
object-fit: cover;
}
#name {
font-size: 0.85rem;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ===== Hero ===== */
.hero {
position: relative;
text-align: center;
padding: 5rem 0 6rem;
overflow: hidden;
}
/* 点阵底纹 */
.hero::before {
content: "";
position: absolute;
inset: 0;
background-image: radial-gradient(var(--border) 1px, transparent 1px);
background-size: 28px 28px;
-webkit-mask-image: radial-gradient(ellipse 65% 55% at 50% 45%, #000 20%, transparent 70%);
mask-image: radial-gradient(ellipse 65% 55% at 50% 45%, #000 20%, transparent 70%);
pointer-events: none;
opacity: 0.7;
}
/* 暖色光晕 */
.hero::after {
content: "";
position: absolute;
left: 50%; top: 44%;
width: 600px; height: 600px;
transform: translate(-50%, -50%);
background: radial-gradient(circle, var(--accent-glow), transparent 55%),
radial-gradient(ellipse 50% 35% at 50% 60%, var(--accent-soft), transparent 65%);
opacity: 0.45;
pointer-events: none;
}
.hero-inner { position: relative; z-index: 1; }
/* 品牌树装饰 */
.hero-bg {
position: absolute;
inset: 0;
display: flex;
align-items: flex-end;
justify-content: center;
pointer-events: none;
z-index: 0;
}
.hero-tree {
width: min(700px, 92%);
height: auto;
color: var(--accent);
opacity: 0.13;
margin-bottom: -2%;
}
@media (prefers-color-scheme: dark) {
.hero-tree { opacity: 0.18; }
}
/* 徽章 */
.hero-badge {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.32rem 0.9rem;
margin-bottom: 1.5rem;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--accent-dark);
background: var(--accent-soft);
border: 1px solid var(--accent-glow);
border-radius: 999px;
}
.badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
flex: none;
}
.hero-title {
font-size: clamp(2rem, 6vw, 3rem);
font-weight: 800;
line-height: 1.3;
letter-spacing: 0.02em;
text-wrap: balance;
margin-bottom: 1rem;
}
.accent-text {
background: linear-gradient(120deg, var(--accent) 15%, var(--accent-dark) 45%, var(--accent) 85%);
background-size: 220% auto;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.hero-sub {
font-size: 1rem;
letter-spacing: 0.02em;
color: var(--fg-secondary);
max-width: 30rem;
margin: 0 auto;
line-height: 1.85;
}
.hero-actions {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.85rem;
margin-top: 2.2rem;
}
.scroll-hint {
position: absolute;
left: 50%; bottom: 1.2rem;
transform: translateX(-50%);
z-index: 1;
display: flex;
color: var(--muted);
transition: color 0.2s;
}
.scroll-hint:hover { color: var(--accent); }
.scroll-hint svg { width: 20px; height: 20px; }
/* ===== 章节节奏与标题组 ===== */
.section { padding-top: 5rem; }
#features { scroll-margin-top: 5.5rem; }
.section-eyebrow {
text-align: center;
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.3em;
text-indent: 0.3em;
color: var(--accent);
text-transform: uppercase;
}
.section-title {
text-align: center;
margin-top: 0.65rem;
margin-bottom: 0;
font-size: 1.5rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.section-title::after {
content: "";
display: block;
width: 40px;
height: 2.5px;
margin: 0.6rem auto 0;
border-radius: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent-dark));
opacity: 0.85;
}
.section-sub {
text-align: center;
margin-top: 0.85rem;
font-size: 0.92rem;
letter-spacing: 0.03em;
color: var(--fg-secondary);
}
/* ===== 特性卡片 ===== */
.features {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.15rem;
margin-top: 2.5rem;
}
.feature-card {
position: relative;
display: flex;
flex-direction: column;
background: var(--card);
background-image: radial-gradient(240px circle at var(--mx, 50%) var(--my, 0%), var(--accent-soft), transparent 75%);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
padding: 1.6rem 1.4rem 1.3rem;
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: transform 0.25s ease, box-shadow 0.3s ease, border-color 0.3s ease;
}
/* 顶部渐变线(hover 显现) */
.feature-card::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 2.5px;
background: linear-gradient(90deg, var(--accent), var(--accent-dark));
opacity: 0;
transition: opacity 0.3s;
}
.feature-card:hover {
transform: translateY(-4px);
border-color: var(--accent-glow);
box-shadow: 0 8px 24px rgba(74, 55, 40, 0.08);
}
.feature-card:hover::before { opacity: 0.85; }
.feature-icon {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
margin-bottom: 0.85rem;
border-radius: 12px;
background: linear-gradient(135deg, var(--accent-soft), rgba(232, 133, 58, 0.12));
color: var(--accent-dark);
transition: transform 0.3s, background 0.3s, color 0.3s, box-shadow 0.3s;
}
.feature-icon svg { width: 20px; height: 20px; }
.feature-card:hover .feature-icon {
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
color: #fff;
transform: translateY(-2px) scale(1.05);
box-shadow: 0 5px 12px rgba(232, 133, 58, 0.3);
}
.feature-body { flex: 1; }
.feature-body h3 { margin-bottom: 0.35rem; font-size: 1rem; letter-spacing: 0.01em; }
.feature-body p {
font-size: 0.86rem;
line-height: 1.75;
color: var(--fg-secondary);
}
.feature-tag {
align-self: flex-start;
margin-top: 0.9rem;
padding: 0.2rem 0.65rem;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.08em;
color: var(--accent-dark);
background: var(--accent-soft);
border: 1px solid var(--accent-glow);
border-radius: 999px;
transition: color 0.25s, border-color 0.25s;
}
.feature-card:hover .feature-tag {
color: var(--accent);
border-color: var(--accent);
}
/* ===== 关于本站 ===== */
.about-section { position: relative; }
.about-panel {
position: relative;
text-align: center;
background: var(--card);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: 2.2rem 2.2rem 1.8rem;
margin-top: 2.5rem;
overflow: hidden;
transition: border-color 0.3s, box-shadow 0.3s;
}
.about-panel:hover {
border-color: var(--border);
box-shadow: var(--shadow-md);
}
/* 顶部品牌线 */
.about-panel::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 2.5px;
background: linear-gradient(90deg, var(--accent), var(--accent-dark));
opacity: 0.8;
}
/* 装饰同心圆 */
.about-deco {
position: absolute;
right: -40px; bottom: -40px;
width: 180px; height: 180px;
color: var(--accent);
pointer-events: none;
opacity: 0.6;
}
.site-intro {
position: relative;
max-width: 38rem;
margin: 0 auto;
font-size: 0.94rem;
line-height: 2;
color: var(--fg-secondary);
}
.about-stats {
list-style: none;
display: flex;
justify-content: space-around;
gap: 1rem;
margin-top: 1.6rem;
padding-top: 1.4rem;
border-top: 1px solid var(--border-light);
}
.about-stats li {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
min-width: 0;
}
.about-stats b {
font-size: 1.4rem;
font-weight: 800;
line-height: 1.1;
letter-spacing: 0.02em;
background: linear-gradient(120deg, var(--accent), var(--accent-dark));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.about-stats span {
font-size: 0.76rem;
letter-spacing: 0.02em;
color: var(--muted);
}
.quick-links {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.85rem;
margin-top: 1.8rem;
}
.quick-links a {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.48rem 1.1rem;
font-size: 0.84rem;
font-weight: 600;
color: var(--fg-secondary);
background: var(--card);
border: 1px solid var(--border);
border-radius: 999px;
text-decoration: none;
box-shadow: var(--shadow-sm);
transition: color 0.2s, border-color 0.2s, transform 0.2s, box-shadow 0.2s;
}
.quick-links a::after {
content: "→";
font-size: 0.85em;
transition: transform 0.2s;
}
.quick-links a:hover {
color: var(--accent);
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.quick-links a:hover::after { transform: translateX(3px); }
/* ===== CTA 横幅 ===== */
.cta-band {
position: relative;
overflow: hidden;
text-align: center;
padding: 3.2rem 1.5rem 3.4rem;
border-radius: var(--radius-lg);
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
color: #fff;
box-shadow: 0 12px 30px rgba(232, 133, 58, 0.22);
}
/* 底部点阵 */
.cta-band::before {
content: "";
position: absolute;
inset: 0;
background-image: radial-gradient(rgba(255,255,255,0.18) 1px, transparent 1px);
background-size: 24px 24px;
-webkit-mask-image: radial-gradient(ellipse 70% 75% at 50% 110%, #000 15%, transparent 68%);
mask-image: radial-gradient(ellipse 70% 75% at 50% 110%, #000 15%, transparent 68%);
pointer-events: none;
}
/* 顶部高光线 */
.cta-band::after {
content: "";
position: absolute;
left: 0; right: 0; top: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.45), transparent);
}
.cta-tree {
position: absolute;
right: -10px; bottom: -16px;
width: min(300px, 50%);
height: auto;
color: #fff;
opacity: 0.12;
pointer-events: none;
}
.cta-title {
position: relative;
font-size: clamp(1.4rem, 3.5vw, 1.85rem);
font-weight: 800;
letter-spacing: 0.03em;
margin-bottom: 0;
}
.cta-sub {
position: relative;
margin-top: 0.6rem;
font-size: 0.92rem;
letter-spacing: 0.04em;
opacity: 0.9;
}
.btn-cta {
position: relative;
margin-top: 1.5rem;
background: #fff;
color: var(--accent-dark);
box-shadow: 0 5px 14px rgba(0,0,0,0.12);
}
.btn-cta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0,0,0,0.16);
}
@media (prefers-color-scheme: dark) {
.cta-band {
color: #38220f;
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
}
.cta-band::before {
background-image: radial-gradient(rgba(56,34,15,0.14) 1px, transparent 1px);
}
.cta-band::after {
background: linear-gradient(90deg, transparent, rgba(56,34,15,0.3), transparent);
}
.cta-tree { color: #38220f; opacity: 0.14; }
.btn-cta { background: #38220f; color: #f7ead9; }
}
/* ===== 页脚 ===== */
.footer {
width: 100%;
margin-top: auto;
padding-top: 5rem;
padding-bottom: 1.5rem;
}
.footer-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.8rem;
padding-top: 1.6rem;
border-top: 1px solid var(--border-light);
text-align: center;
font-size: 0.75rem;
color: var(--muted);
}
.footer-brand {
display: flex;
align-items: center;
gap: 0.45rem;
font-size: 0.92rem;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--fg);
}
.footer-brand svg { color: var(--accent); }
.footer-nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.35rem 1.5rem;
}
.footer-nav a {
font-size: 0.82rem;
color: var(--fg-secondary);
text-decoration: none;
transition: color 0.15s;
}
.footer-nav a:hover { color: var(--accent); }
.footer-icp { margin-top: 0.1rem; }
.footer-icp a {
color: var(--muted);
text-decoration: none;
transition: color 0.15s;
}
.footer-icp a:hover { color: var(--accent); }
/* ===== 动画 ===== */
@keyframes fade-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: none; }
}
@keyframes tree-draw { to { stroke-dashoffset: 0; } }
@keyframes node-pop {
from { opacity: 0; transform: scale(0); }
to { opacity: 1; transform: scale(1); }
}
@keyframes tree-sway {
0%, 100% { transform: rotate(0deg); }
50% { transform: rotate(0.5deg); }
}
@keyframes text-shine { to { background-position: 220% center; } }
@keyframes badge-pulse {
0%, 100% { box-shadow: 0 0 0 0 var(--accent-glow); }
50% { box-shadow: 0 0 0 4px transparent; }
}
@keyframes hint-bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(4px); }
}
@media (prefers-reduced-motion: no-preference) {
/* Hero 入场 */
.hero-inner > * {
animation: fade-up 0.5s ease both;
}
.hero-badge { animation-delay: 0s; }
.hero-title { animation-delay: 0.06s; }
.hero-sub { animation-delay: 0.12s; }
.hero-actions { animation-delay: 0.2s; }
.accent-text { animation: text-shine 8s linear infinite; }
.badge-dot { animation: badge-pulse 2.8s ease-in-out infinite; }
.scroll-hint svg { animation: hint-bounce 2.2s ease-in-out infinite; }
/* 品牌树描绘 */
.hero-tree {
animation: tree-sway 12s ease-in-out 3s infinite;
transform-origin: 50% 100%;
}
.tree-branches path {
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: tree-draw 0.8s ease-out forwards;
}
.tree-branches path:nth-of-type(1) { animation-delay: 0.1s; }
.tree-branches path:nth-of-type(2),
.tree-branches path:nth-of-type(3) { animation-delay: 0.6s; }
.tree-branches path:nth-of-type(n+4) { animation-delay: 1.1s; }
.tree-nodes circle {
opacity: 0;
transform-box: fill-box;
transform-origin: center;
animation: node-pop 0.4s ease-out forwards;
}
.tree-nodes circle:nth-of-type(1) { animation-delay: 0.15s; }
.tree-nodes circle:nth-of-type(2) { animation-delay: 0.7s; }
.tree-nodes circle:nth-of-type(3),
.tree-nodes circle:nth-of-type(4) { animation-delay: 1.2s; }
.tree-nodes circle:nth-of-type(n+5) { animation-delay: 1.7s; }
}
/* ===== 滚动渐显 ===== */
.reveal {
opacity: 0;
transform: translateY(16px);
transition: opacity 0.55s ease, transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}
.reveal.reveal-in { opacity: 1; transform: none; }
/* ===== 响应式 ===== */
@media (hover: none) and (pointer: coarse) {
.btn { min-height: 44px; }
.quick-links a { padding: 0.55rem 1rem; }
}
@media (max-width: 768px) {
.hero { padding: 4rem 0 4.5rem; }
.hero::after { width: 400px; height: 400px; opacity: 0.35; }
.hero-tree { opacity: 0.1; }
.section { padding-top: 4rem; }
.footer { padding-top: 4rem; }
}
@media (max-width: 640px) {
.features { grid-template-columns: 1fr; }
.feature-card:hover { transform: none; }
}
@media (max-width: 480px) {
.wrap { padding: 0 0.85rem; }
.header { top: 0.5rem; margin-top: 0.5rem; }
.header-inner { padding: 0.4rem 0.8rem; gap: 0.35rem; }
.header h1 { font-size: 0.98rem; }
.header h1 svg { display: none; }
.right-group { gap: 0.3rem; }
.right-group .btn { padding: 0.48rem 0.9rem; font-size: 0.82rem; }
#name { max-width: 72px; }
.hero { padding: 2.8rem 0 2.8rem; }
.hero::after { width: 280px; height: 280px; opacity: 0.3; }
.hero-tree { opacity: 0.08; }
.hero-badge { font-size: 0.72rem; padding: 0.28rem 0.75rem; margin-bottom: 1.1rem; }
.hero-title { margin-bottom: 0.7rem; }
.hero-sub { font-size: 0.92rem; }
.hero-sub br { display: none; }
.hero-actions {
flex-direction: column;
align-items: stretch;
gap: 0.6rem;
}
.hero-actions .btn-lg { width: 100%; padding: 0.82rem 1.4rem; }
.scroll-hint { display: none; }
.section { padding-top: 3rem; }
.section-title { font-size: 1.25rem; }
.features { margin-top: 1.8rem; gap: 0.9rem; }
.feature-card { padding: 1.3rem 1.2rem 1.1rem; }
.about-panel { padding: 1.5rem 1.2rem 1.3rem; margin-top: 1.8rem; }
.about-stats { margin-top: 1.3rem; padding-top: 1.1rem; gap: 0.5rem; }
.quick-links { gap: 0.55rem 0.7rem; margin-top: 1.4rem; }
.cta-band { padding: 2.5rem 1.1rem 2.6rem; }
.cta-tree { width: 70%; opacity: 0.08; }
.footer { padding-top: 3rem; }
}
@media (max-width: 360px) {
.header h1 { font-size: 0.92rem; }
.right-group .btn { padding: 0.42rem 0.75rem; font-size: 0.78rem; }
.hero { padding: 2.2rem 0 2.2rem; }
}
-686
View File
@@ -1,686 +0,0 @@
/* ===== Design Tokens ===== */
:root {
--bg: #fdf6ee;
--fg: #4a3728;
--fg-secondary: #8c7260;
--card: #ffffff;
--card-hover: #fff7ef;
--border: #f0e0cc;
--border-light: #f7ecdd;
--muted: #b89b7a;
--accent: #e8853a;
--accent-dark: #cf6b1f;
--accent-soft: rgba(232, 133, 58, 0.10);
--accent-glow: rgba(232, 133, 58, 0.25);
--error: #d94f3d;
--error-bg: #fef0ee;
--success: #5a9e6f;
--warning: #d4922a;
--shadow-sm: 0 1px 3px rgba(74, 55, 40, 0.06);
--shadow-md: 0 4px 12px rgba(74, 55, 40, 0.08);
--radius-sm: 10px;
--radius-md: 14px;
--radius-lg: 20px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1e1712;
--fg: #f0e4d8;
--fg-secondary: #a08c78;
--card: #2a2119;
--card-hover: #342a20;
--border: #3e3228;
--border-light: #342a20;
--muted: #8c7a66;
--accent: #e8934a;
--accent-dark: #f0a060;
--accent-soft: rgba(232, 147, 74, 0.12);
--accent-glow: rgba(232, 147, 74, 0.20);
--error: #e8685a;
--error-bg: #3a2220;
--success: #6fb87f;
--warning: #e0a84a;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.3);
}
}
/* ===== Base ===== */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
background: var(--bg);
color: var(--fg);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', Roboto, Arial, sans-serif;
padding: 2rem 1rem;
-webkit-font-smoothing: antialiased;
}
body > .card,
body > .box { margin: auto 0; }
/* ===== Layout ===== */
.box { width: 90%; max-width: 720px; }
.card {
background: var(--card);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
padding: 1.75rem 1.5rem;
box-shadow: var(--shadow-sm);
color: var(--fg);
transition: box-shadow 0.2s, border-color 0.2s;
overflow-wrap: break-word;
width: 90%;
max-width: 440px;
}
.card:hover {
border-color: var(--border);
box-shadow: var(--shadow-md);
}
.box > .card { width: 100%; max-width: none; }
/* ===== Typography ===== */
h1 { font-size: 1.4rem; font-weight: 700; }
h2 { font-size: 1.25rem; font-weight: 700; margin-bottom: 1.25rem; }
h3 { font-size: 1.05rem; font-weight: 600; margin-bottom: 0.5rem; }
/* ===== Form Elements ===== */
label {
display: block;
font-size: 0.85rem;
font-weight: 600;
color: var(--fg-secondary);
margin-top: 1rem;
margin-bottom: 0.3rem;
}
form > div > label:first-child { margin-top: 0; }
input:not([type="checkbox"]):not([type="submit"]):not([type="reset"]):not([type="button"]),
textarea {
display: block;
width: 100%;
padding: 0.6rem 0.9rem;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
font-size: 0.9rem;
font-family: inherit;
background: var(--bg);
color: var(--fg);
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:not([type="checkbox"]):not([type="submit"]):not([type="reset"]):not([type="button"]):focus,
textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
input::placeholder, textarea::placeholder { color: var(--muted); }
textarea { min-height: 80px; resize: vertical; }
select {
display: block;
width: 100%;
padding: 0.6rem 0.9rem;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
font-size: 0.9rem;
font-family: inherit;
background: var(--bg);
color: var(--fg);
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
/* ===== Buttons ===== */
button, input[type="submit"], input[type="reset"], input[type="button"] {
padding: 0.55rem 1.3rem;
font-size: 0.88rem;
font-weight: 600;
font-family: inherit;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: opacity 0.15s, transform 0.1s;
line-height: 1.4;
}
button:hover, input[type="submit"]:hover,
input[type="reset"]:hover, input[type="button"]:hover { opacity: 0.85; }
button:active, input[type="submit"]:active,
input[type="reset"]:active, input[type="button"]:active { transform: scale(0.96); }
input[type="submit"] { background: var(--accent); color: #fff; }
input[type="reset"] { background: transparent; color: var(--muted); border: 1.5px solid var(--border); }
button[type="button"], input[type="button"] { background: var(--accent-soft); color: var(--accent-dark); }
#emlSub { display: inline-block; margin-top: 0.4rem; }
#emlSub input[type="submit"] {
background: var(--accent-soft);
color: var(--accent-dark);
font-size: 0.82rem;
padding: 0.45rem 1rem;
}
/* ===== Form Layout ===== */
.form-actions {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
margin-top: 1.25rem;
}
/* ===== 防重复点击:请求进行中按钮禁用态反馈 ===== */
button:disabled,
input[type="submit"]:disabled,
input[type="button"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.form-check {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.form-check input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); }
.form-check label {
display: inline;
margin: 0;
font-size: 0.82rem;
font-weight: 500;
color: var(--fg-secondary);
}
/* ===== Code Box (register) ===== */
.code-box {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.4rem;
font-size: 0.85rem;
color: var(--fg-secondary);
}
.code-box input[type="text"] { width: auto; flex: 1; min-width: 100px; display: inline-block; }
.code-box input[type="button"] { white-space: nowrap; }
/* 图形验证码图片(register 图形/邮箱验证码同排布局) */
.code-box .captcha-img {
width: 120px;
height: 38px;
flex: none;
cursor: pointer;
border: 1.5px solid var(--border);
border-radius: 10px;
box-sizing: border-box;
object-fit: cover;
background: var(--bg);
}
/* ===== Validation Tips ===== */
.tip-red { color: var(--error); font-size: 0.78rem; }
.tip-yellow { color: var(--warning); font-size: 0.78rem; }
.tip-green { color: var(--success); font-size: 0.78rem; }
/* ===== Article (dnfs) ===== */
.article-header { text-align: center; margin-bottom: 1.25rem; }
.article-header h1 { margin-bottom: 0.5rem; }
.article-meta { font-size: 0.82rem; color: var(--muted); margin-bottom: 0.3rem; }
.article-stats {
font-size: 0.82rem;
color: var(--muted);
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.article-stats span { font-weight: 600; color: var(--fg-secondary); }
#article-content { font-size: 0.95rem; line-height: 1.7; margin-bottom: 1.25rem; }
/* ===== Search Box (dnfs 顶部搜索) ===== */
.searchbox {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.45rem 0.55rem 0.45rem 0.9rem;
background: var(--card);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
margin-bottom: 1rem;
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
}
.searchbox:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-md);
}
.searchbox-icon {
width: 18px;
height: 18px;
flex: none;
color: var(--muted);
transition: color 0.2s;
}
.searchbox:focus-within .searchbox-icon {
color: var(--accent);
}
.searchbox-input {
flex: 1;
min-width: 0;
border: none !important;
background: transparent !important;
padding: 0.45rem 0.25rem !important;
font-size: 0.92rem !important;
box-shadow: none !important;
border-radius: 0 !important;
}
.searchbox-input:focus {
box-shadow: none !important;
}
.searchbox-btn {
flex: none;
padding: 0.5rem 1rem !important;
font-size: 0.85rem !important;
font-weight: 600;
background: var(--accent) !important;
color: #fff !important;
border: none !important;
border-radius: var(--radius-sm) !important;
cursor: pointer;
transition: opacity 0.15s, transform 0.1s, background 0.2s;
line-height: 1.2;
}
.searchbox-btn:hover { opacity: 0.88; }
.searchbox-btn:active { transform: scale(0.96); }
@media (max-width: 480px) {
.searchbox { padding: 0.35rem 0.4rem 0.35rem 0.75rem; gap: 0.35rem; }
.searchbox-icon { width: 16px; height: 16px; }
.searchbox-input { font-size: 0.88rem !important; }
.searchbox-btn { padding: 0.45rem 0.75rem !important; font-size: 0.82rem !important; }
}
/* ===== Recommend List (dnfs) ===== */
/* 卡片尺寸随内容自适应:宽度包裹内容(fit-content),上限为容器宽度;高度天然随内容伸缩 */
.recommend-list {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
width: 100%;
}
.recommend-list br { display: none; } /* JS 追加的 <br> 分隔符,改由 flex gap 统一控制间距 */
.recommend-list .card {
width: fit-content;
min-width: min(240px, 100%);
max-width: 100%;
margin-bottom: 0;
}
/* 防止内容中的大图撑破自适应宽度 */
.recommend-list .card img {
max-width: 100%;
height: auto;
border-radius: var(--radius-sm);
}
.recommend-list center {
padding: 0.4rem;
border-radius: var(--radius-sm);
}
/* 仅标题可点击进入子节点:手型光标 + hover 高亮集中在标题 h3 上 */
.recommend-list .card h3 {
cursor: pointer;
transition: color 0.15s;
}
.recommend-list .card h3:hover { color: var(--accent); }
.recommend-list .card p { font-size: 0.9rem; line-height: 1.6; color: var(--fg-secondary); }
/* ===== Owner Chip (dnfs 发帖用户) ===== */
.card-owner {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.2rem;
}
.owner-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--accent-soft);
color: var(--accent-dark);
border: 1px solid var(--accent);
font-size: 0.7rem;
font-weight: 700;
flex-shrink: 0;
user-select: none;
}
.owner-name {
font-size: 0.8rem;
font-weight: 600;
color: var(--fg-secondary);
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ===== Card Stats (dnfs 互动统计:观看/点赞/分享) ===== */
.card-stats {
display: flex;
align-items: center;
justify-content: center;
gap: 1.1rem;
margin-top: 0.45rem;
}
.stat-item {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.78rem;
color: var(--muted);
}
.stat-item svg {
width: 14px;
height: 14px;
flex-shrink: 0;
}
.stat-num {
font-weight: 600;
color: var(--fg-secondary);
font-variant-numeric: tabular-nums;
}
/* ===== Card Actions (dnfs 编辑/删除按钮) ===== */
.card-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.6rem;
}
.act-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.7rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: var(--radius-sm);
border: 1px solid transparent;
background: var(--accent-soft);
color: var(--accent-dark);
cursor: pointer;
transition: border-color 0.15s, opacity 0.15s, transform 0.1s;
}
.act-btn svg { width: 12px; height: 12px; flex-shrink: 0; }
.act-btn:hover { border-color: var(--accent); }
.act-del { background: var(--error-bg); color: var(--error); }
.act-del:hover { border-color: var(--error); }
.act-move { background: transparent; color: var(--fg-secondary); border-color: var(--border); }
.act-move:hover { border-color: var(--accent); color: var(--accent-dark); }
/* ===== Move Bar (dnfs 节点移动条) ===== */
.movebar {
position: fixed;
left: 50%;
bottom: 1.25rem;
transform: translateX(-50%);
z-index: 110;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.5rem 0.8rem;
border-radius: var(--radius-md);
background: rgba(253, 246, 238, 0.85);
border: 1px solid var(--accent);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: var(--shadow-md);
font-size: 0.82rem;
max-width: 92vw;
}
@media (prefers-color-scheme: dark) {
.movebar { background: rgba(30, 23, 18, 0.85); }
}
.movebar:empty { display: none; }
.move-title {
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--fg-secondary);
font-weight: 600;
}
/* ===== User Panel (dnfs 左上角用户信息,镜像 minimap 样式) ===== */
.userpanel {
position: fixed;
left: 1rem;
top: 4.5rem;
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 80vh;
overflow-y: auto;
padding: 0.6rem 0.75rem;
border-radius: var(--radius-md);
background: rgba(253, 246, 238, 0.72);
border: 1px solid var(--border-light);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: var(--shadow-md);
transition: background 0.2s, border-color 0.2s;
}
@media (prefers-color-scheme: dark) {
.userpanel {
background: rgba(30, 23, 18, 0.72);
border-color: var(--border);
}
}
/* ===== Minimap (dnfs) ===== */
.minimap {
position: fixed;
right: 1rem;
top: 50%;
transform: translateY(-50%);
z-index: 100;
display: flex;
flex-direction: column;
gap: 0.4rem;
max-height: 80vh;
overflow-y: auto;
padding: 0.6rem 0.75rem;
border-radius: var(--radius-md);
background: rgba(253, 246, 238, 0.72);
border: 1px solid var(--border-light);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: var(--shadow-md);
transition: background 0.2s, border-color 0.2s;
}
@media (prefers-color-scheme: dark) {
.minimap {
background: rgba(30, 23, 18, 0.72);
border-color: var(--border);
}
}
.minimap:empty, .userpanel:empty { display: none; }
/* userpanel 纯展示项(用户名/角色):不可点击,无手型光标与 hover 变色 */
.userpanel .tag.no-hover {
cursor: default;
}
.userpanel .tag.no-hover:hover {
color: var(--fg-secondary);
background: rgba(232, 133, 58, 0.08);
border-color: transparent;
}
.minimap .tag, .userpanel .tag {
display: inline-block;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0.25rem 0.6rem;
font-size: 0.78rem;
font-weight: 500;
color: var(--fg-secondary);
background: rgba(232, 133, 58, 0.08);
border: 1px solid transparent;
border-radius: var(--radius-sm);
cursor: pointer;
white-space: nowrap;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.minimap::-webkit-scrollbar { width: 4px; }
.minimap::-webkit-scrollbar-track { background: transparent; }
.minimap::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
.minimap::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.minimap .tag:hover, .userpanel .tag:hover {
color: var(--accent-dark);
background: var(--accent-soft);
border-color: var(--accent);
}
.minimap .tag.back, .userpanel .tag.back {
color: var(--accent);
background: var(--accent-soft);
border-color: var(--accent);
margin-bottom: 0.25rem;
}
.minimap .tag.fold-btn, .userpanel .tag.fold-btn {
color: var(--fg-secondary);
background: transparent;
border: 1px solid var(--border);
font-weight: 600;
margin-bottom: 0.3rem;
text-align: center;
max-width: none;
cursor: pointer;
transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.minimap .tag.fold-btn:hover, .userpanel .tag.fold-btn:hover {
color: var(--accent-dark);
background: var(--accent-soft);
border-color: var(--accent);
}
/* ===== Special Nodes (dnfs,管理员发布的特殊节点) ===== */
.sp-link { display: flex; align-items: center; gap: 0.5rem; }
.sp-favicon { width: 20px; height: 20px; flex: none; border-radius: 4px; }
.sp-anchor { color: var(--accent); word-break: break-all; }
.sp-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.4rem; }
.sp-code {
max-height: 320px;
overflow: auto;
margin-top: 0.5rem;
padding: 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 0.78rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
.sp-image { display: block; max-width: 100%; border-radius: var(--radius-sm); }
.sp-run {
margin-top: 0.5rem;
padding: 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.sp-log {
margin-top: 0.5rem;
max-height: 320px;
overflow: auto;
padding: 0.5rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 0.78rem;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
}
.sp-note { font-size: 0.82rem; color: var(--fg-secondary); margin-top: 0.4rem; }
#sp-panel { margin-top: 0.4rem; }
#sp-panel select { margin-bottom: 0.4rem; }
#sp-file-tip { font-size: 0.78rem; }
/* ===== Node Form (dnfs) ===== */
.node-form { margin-top: 1rem; }
.node-form .form-actions { margin-top: 0.75rem; }
.node-form #input-note-title {
padding: 0.8rem 1rem;
font-size: 1rem;
}
.node-form #input-note-content {
padding: 0.9rem 1rem;
font-size: 0.95rem;
min-height: 120px;
max-height: 400px;
line-height: 1.7;
overflow-y: hidden;
resize: none;
}
/* ===== Markdown Content ===== */
code {
color: var(--error);
background: var(--error-bg);
padding: 2px 6px;
border-radius: 5px;
font-size: 0.88em;
}
/*
pre > code {
display: block;
color: #f0e4d8;
background: #3d3229;
padding: 12px;
border-radius: var(--radius-sm);
overflow-x: auto;
} */
blockquote {
margin-right: 0;
padding: 0.5rem 1rem;
background: var(--accent-soft);
border-left: 3px solid var(--accent);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
color: var(--fg-secondary);
}
/* ===== Footer ===== */
.footer {
margin-top: auto;
padding-top: 1.5rem;
text-align: center;
font-size: 0.75rem;
color: var(--muted);
}
.footer a {
color: var(--muted);
text-decoration: none;
transition: color 0.15s;
}
.footer a:hover { color: var(--accent); }
/* ===== Responsive ===== */
@media (max-width: 480px) {
body { padding: 1rem 0.8rem; }
.card { padding: 1.25rem 1rem; border-radius: 16px; width: 95%; }
.box { width: 95%; }
h2 { font-size: 1.1rem; }
.form-actions button, .form-actions input { flex: 1; padding: 0.5rem 0.6rem; font-size: 0.82rem; }
}
@media (max-width: 360px) {
.card { padding: 1rem 0.8rem; border-radius: var(--radius-sm); }
}
-374
View File
@@ -1,374 +0,0 @@
:root {
--bg: #fdf6ee;
--fg: #4a3728;
--fg-secondary: #8c7260;
--card: #ffffff;
--card-hover: #fff7ef;
--card-active: #ffeedd;
--border: #f0e0cc;
--border-light: #f7ecdd;
--muted: #b89b7a;
--accent: #e8853a;
--accent-dark: #cf6b1f;
--accent-soft: rgba(232, 133, 58, 0.10);
--accent-glow: rgba(232, 133, 58, 0.25);
--play: #f0a030;
--download: #e8853a;
--error: #d94f3d;
--error-bg: #fef0ee;
--icon-bg: #faf0e4;
--icon-border: #f0e0cc;
--shadow-sm: 0 1px 3px rgba(74, 55, 40, 0.06);
--shadow-md: 0 4px 12px rgba(74, 55, 40, 0.08);
--shadow-lg: 0 8px 24px rgba(74, 55, 40, 0.12);
--radius-sm: 10px;
--radius-md: 14px;
--radius-lg: 20px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1e1712;
--fg: #f0e4d8;
--fg-secondary: #a08c78;
--card: #2a2119;
--card-hover: #342a20;
--card-active: #3e3228;
--border: #3e3228;
--border-light: #342a20;
--muted: #8c7a66;
--accent: #e8934a;
--accent-dark: #f0a060;
--accent-soft: rgba(232, 147, 74, 0.12);
--accent-glow: rgba(232, 147, 74, 0.20);
--play: #f0a848;
--download: #e8934a;
--error: #e8685a;
--error-bg: #3a2220;
--icon-bg: #342a20;
--icon-border: #4a3c30;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.2);
--shadow-md: 0 4px 12px rgba(0,0,0,0.3);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.4);
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', Roboto, Arial, sans-serif;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
/* ===== 顶栏 ===== */
.header {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-dark) 100%);
color: #fff;
padding: 0 20px;
height: 54px;
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(207, 107, 31, 0.3);
}
.header h1 {
font-size: 17px;
font-weight: 600;
letter-spacing: 0.3px;
display: flex;
align-items: center;
gap: 8px;
}
.header-actions { display: flex; gap: 4px; }
.header-actions a {
color: #fff;
text-decoration: none;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: 16px;
transition: background 0.15s;
}
.header-actions a:hover { background: rgba(255,255,255,0.18); }
.header-actions a:active { background: rgba(255,255,255,0.30); }
/* ===== 面包屑 ===== */
.breadcrumb {
padding: 10px 20px;
background: var(--card);
font-size: 13px;
overflow-x: auto;
white-space: nowrap;
border-bottom: 1px solid var(--border-light);
scrollbar-width: none;
}
.breadcrumb::-webkit-scrollbar { display: none; }
.breadcrumb a {
color: var(--accent);
text-decoration: none;
padding: 4px 10px;
border-radius: 8px;
transition: background 0.15s, color 0.15s;
font-weight: 500;
}
.breadcrumb a:hover { background: var(--accent-soft); }
.breadcrumb .sep { color: var(--muted); margin: 0 2px; font-size: 11px; }
.breadcrumb .current {
color: var(--fg);
font-weight: 600;
padding: 4px 10px;
}
/* ===== 搜索栏 ===== */
.search-box {
padding: 12px 20px;
background: var(--card);
border-bottom: 1px solid var(--border-light);
}
.search-box input {
width: 100%;
max-width: 560px;
padding: 10px 18px;
border: 1.5px solid var(--border);
border-radius: 12px;
font-size: 14px;
outline: none;
background: var(--bg);
color: var(--fg);
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-box input::placeholder { color: var(--muted); }
.search-box input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow);
}
/* ===== 统计条 ===== */
.stats {
padding: 10px 20px;
font-size: 12px;
color: var(--fg-secondary);
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.stats span {
padding: 4px 12px;
background: var(--card);
border-radius: 20px;
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
font-variant-numeric: tabular-nums;
}
/* ===== 主容器 ===== */
.container {
padding: 16px 20px 40px;
max-width: 1200px;
margin: 0 auto;
}
/* ===== 文件网格 ===== */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(155px, 1fr));
gap: 14px;
}
.item {
background: var(--card);
border-radius: var(--radius-lg);
padding: 18px 14px 14px;
cursor: pointer;
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s, transform 0.15s, border-color 0.2s, background 0.15s;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 10px;
position: relative;
}
.item:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
border-color: var(--border);
background: var(--card-hover);
}
.item:active {
transform: translateY(0) scale(0.98);
background: var(--card-active);
}
/* 方块图标 */
.item .icon {
font-size: 34px;
width: 62px;
height: 62px;
display: flex;
align-items: center;
justify-content: center;
background: var(--icon-bg);
border-radius: var(--radius-md);
border: 1px solid var(--icon-border);
transition: transform 0.15s;
flex-shrink: 0;
}
.item:hover .icon { transform: scale(1.06); }
.item .info {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px;
}
.item .name {
font-size: 13px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
line-height: 1.3;
}
.item .meta {
font-size: 11px;
color: var(--muted);
display: flex;
justify-content: center;
gap: 6px;
flex-wrap: wrap;
line-height: 1.4;
}
/* 操作按钮 */
.item .actions {
display: flex;
gap: 6px;
justify-content: center;
margin-top: 2px;
}
.item .actions button {
padding: 5px 14px;
font-size: 12px;
border: none;
border-radius: 10px;
cursor: pointer;
color: #fff;
font-weight: 500;
transition: opacity 0.15s, transform 0.1s;
line-height: 1.4;
}
.item .actions button:hover { opacity: 0.85; }
.item .actions button:active { transform: scale(0.93); }
.btn-play { background: var(--play); }
.btn-download { background: var(--download); }
/* ===== 加载态 ===== */
.loading {
text-align: center;
padding: 60px 20px;
color: var(--muted);
font-size: 14px;
}
.loading .spinner {
display: inline-flex;
align-items: center;
justify-content: center;
animation: spin 1.2s cubic-bezier(0.45, 0.05, 0.55, 0.95) infinite;
font-size: 28px;
width: 56px;
height: 56px;
background: var(--card);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
margin-bottom: 12px;
}
@keyframes spin { 100% { transform: rotate(360deg); } }
/* ===== 空态 ===== */
.empty {
text-align: center;
padding: 80px 20px;
color: var(--muted);
font-size: 14px;
}
.empty .icon {
font-size: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
margin-bottom: 16px;
}
/* ===== 错误态 ===== */
.error {
text-align: center;
padding: 40px 24px;
color: var(--error);
background: var(--error-bg);
border-radius: var(--radius-lg);
border: 1px solid var(--error);
margin: 20px 0;
font-size: 14px;
line-height: 1.6;
}
/* ===== 页脚 ===== */
.footer {
text-align: center;
padding: 24px 20px 32px;
font-size: 12px;
color: var(--muted);
}
.footer a {
color: var(--muted);
text-decoration: none;
transition: color 0.15s;
}
.footer a:hover { color: var(--accent); }
/* ===== 响应式 ===== */
@media (max-width: 480px) {
.header { padding: 0 14px; height: 50px; }
.header h1 { font-size: 15px; }
.breadcrumb { padding: 8px 14px; }
.search-box { padding: 10px 14px; }
.stats { padding: 8px 14px; gap: 6px; }
.container { padding: 12px 14px 32px; }
.file-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.item {
padding: 14px 10px 12px;
border-radius: 16px;
gap: 8px;
}
.item .icon {
font-size: 28px;
width: 52px;
height: 52px;
border-radius: var(--radius-sm);
}
.item .name { font-size: 12px; }
.item .meta { font-size: 10px; gap: 4px; }
.item .actions button { padding: 4px 10px; font-size: 11px; border-radius: 8px; }
}
-62
View File
@@ -1,62 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- 移动端适配核心标签 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>树状笔记</title>
<link rel="stylesheet" href="css/theme.css">
<link rel="stylesheet" href="css/codeblock.css">
<link rel="stylesheet" href="css/dnfs.css">
</head>
<body>
<div class="box">
<div id="searchbox" class="searchbox">
<svg class="searchbox-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input type="text" placeholder="在当前节点内搜索..." id="searchtext" class="searchbox-input" autocomplete="off">
<button type="button" id="searchsubmit" class="searchbox-btn" aria-label="搜索">搜索</button>
</div>
<div id="userpanel" class="userpanel"></div>
<div id="minimap" class="minimap"></div>
<div id="movebar" class="movebar"></div>
<div class="recommend-list" id="recommend-list"></div>
<div class="card node-form">
<h3>节点操作</h3>
<label>新增节点</label><input id="input-note-title">
<!-- 特殊节点面板:仅管理员可见(dnfs.js 按 /whoami is_admin 显示) -->
<div id="sp-panel" style="display:none">
<label>节点类型</label>
<select id="sp-type">
<option value="text">普通文本</option>
<option value="link">链接(自动显示站点图标)</option>
<option value="js">前端代码(可审计/可运行)</option>
<option value="image">图片(自动显示)</option>
<option value="file">文件(可下载)</option>
</select>
<input type="file" id="sp-file" style="display:none">
<span id="sp-file-tip"></span>
</div>
<label>节点内容</label><textarea id="input-note-content"></textarea>
<div class="form-actions">
<button id="input-note-submit">提交</button>
<button id="input-note-clear">重置</button>
<button id="delete-note">删除当前节点</button>
</div>
</div>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.0/markdown-it.min.js"></script> -->
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> -->
<script src="js/highlight_11_9_0.js"></script>
<script src="js/markdown-it-12_0_0.js"></script>
<script src="js/validation.js"></script>
<script src="js/token_manager.js"></script>
<script src="js/dnfs.js"></script>
</div>
<footer class="footer">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
</footer>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

-340
View File
@@ -1,340 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>重置密码</title>
<link rel="stylesheet" href="css/theme.css">
<link rel="stylesheet" href="css/auth.css">
</head>
<body>
<div class="card">
<p class="auth-eyebrow">DNFS 树状笔记</p>
<h2>重置密码页面</h2>
<form id="form">
<div>
<label>邮箱号:</label>
<input type="email" id="regemail" placeholder="请输入已注册的邮箱号">
<span id="emlTip"></span>
<div class="code-box">图形验证码:
<input type="text" id="captchaAnswer" placeholder="请输入图形验证码" maxlength="4" autocomplete="off">
<img id="captchaImg" class="captcha-img" alt="加载中…" title="点击刷新验证码">
<span id="captchaTip"></span>
</div>
<div class="code-box">邮箱验证码:
<input type="text" id="code" placeholder="请输入6位验证码" inputmode="numeric" maxlength="6">
<input type="button" id="getCode" value="获取验证码">
<span id="codeTip"></span>
</div>
<label>新密码:</label>
<input type="password" id="regpsw" placeholder="至少6位,需含大小写/数字/特殊字符中至少3类">
<label>确认密码:</label>
<input type="password" id="repwd" placeholder="请再次输入新密码">
<span id="pwdTip"></span>
</div>
<div class="form-actions">
<input id="formsub" type="submit" value="重置密码">
<input id="resetUser" type="reset" value="重置">
<input type="button" value="返回登录" onclick="location.href='login.html'">
</div>
</form>
</div>
<footer class="footer">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
</footer>
<script src="js/validation.js"></script>
<script>
const form = document.getElementById('form');
const emailInput = document.getElementById('regemail');
const codeInput = document.getElementById('code');
const getCodeButton = document.getElementById('getCode');
const passwordInput = document.getElementById('regpsw');
const repeatPasswordInput = document.getElementById('repwd');
const emlTip = document.getElementById('emlTip');
const pwdTip = document.getElementById('pwdTip');
const codeTip = document.getElementById('codeTip');
const captchaImg = document.getElementById('captchaImg');
const captchaInput = document.getElementById('captchaAnswer');
const captchaTip = document.getElementById('captchaTip');
const submitButton = document.getElementById('formsub');
let codeTarget = ''; // 已发送验证码的目标邮箱(验证码本体由服务端校验,前端不经手)
let cooldownTimer = null; // 重发倒计时(与服务端 smtp 同邮箱 60s 发送冷却对齐)
/* ── 图形验证码(gateway 代理 captcha 服务:GET /captcha/image) ── */
let captchaToken = '';
let captchaLoading = false; // 防重复点击:加载中忽略再次触发
async function refreshCaptcha(preserveInput) {
if (captchaLoading) return;
captchaLoading = true;
captchaToken = '';
if (!preserveInput) captchaInput.value = ''; // 默认清空输入;preserveInput=true 时保留(获取验证码后)
captchaImg.style.opacity = '0.5'; // 视觉提示:正在刷新
try {
const res = await fetch('/captcha/image', { cache: 'no-store' });
if (res.status === 403) { // WAF 拦截:body 非图片,提示后待用户点击重试
captchaImg.removeAttribute('src');
captchaImg.alt = '验证码加载被安全规则拦截,点击重试';
return;
}
if (!res.ok) throw new Error('captcha load failed');
captchaToken = res.headers.get('X-Captcha-Token') || '';
const blob = await res.blob();
if (captchaImg.src && captchaImg.src.startsWith('blob:')) URL.revokeObjectURL(captchaImg.src);
captchaImg.src = URL.createObjectURL(blob);
captchaImg.alt = '点击刷新';
captchaImg.style.opacity = ''; // 恢复透明度
} catch (e) {
captchaImg.removeAttribute('src');
captchaImg.alt = '加载失败,点击重试';
} finally {
captchaLoading = false;
}
}
captchaImg.addEventListener('click', refreshCaptcha);
refreshCaptcha();
// 发送成功后进入倒计时,避免用户触发服务端 smtp 同邮箱 60s 发送冷却
function startResendCooldown(seconds) {
clearInterval(cooldownTimer);
let remain = seconds;
getCodeButton.disabled = true;
getCodeButton.value = `${remain}s 后重发`;
cooldownTimer = setInterval(function () {
remain -= 1;
if (remain <= 0) {
clearInterval(cooldownTimer);
cooldownTimer = null;
getCodeButton.disabled = false;
getCodeButton.value = '获取验证码';
} else {
getCodeButton.value = `${remain}s 后重发`;
}
}, 1000);
}
form.addEventListener('reset', function () {
clearTipStyle();
codeTarget = '';
captchaInput.value = '';
refreshCaptcha();
});
function clearTipStyle() {
emlTip.innerHTML = '';
pwdTip.innerHTML = '';
codeTip.innerHTML = '';
captchaTip.innerHTML = '';
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
captchaTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
}
clearTipStyle();
function checkEmail() {
const result = FormValidators.email(emailInput.value);
emlTip.innerHTML = '';
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
emlTip.textContent = result.message;
emlTip.classList.add(result.className);
return result.valid ? result.value : false;
}
function checkPassword() {
const result = FormValidators.password(passwordInput.value, 6);
pwdTip.innerHTML = '';
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
if (!result.valid) {
pwdTip.textContent = result.message;
pwdTip.classList.add(result.className);
return false;
}
if (result.value !== repeatPasswordInput.value) {
pwdTip.textContent = '两次输入的密码不一致!';
pwdTip.classList.add('tip-yellow');
return false;
}
pwdTip.textContent = result.message;
pwdTip.classList.add(result.className);
return result.value;
}
function checkCode() {
const code = codeInput.value.trim();
codeTip.innerHTML = '';
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
if (!codeTarget) {
codeTip.textContent = '请先获取验证码!';
codeTip.classList.add('tip-red');
return false;
}
if (emailInput.value.trim() !== codeTarget) {
codeTip.textContent = '邮箱已修改,请重新获取验证码!';
codeTip.classList.add('tip-red');
return false;
}
if (!/^\d{6}$/.test(code)) {
codeTip.textContent = '验证码必须是6位数字!';
codeTip.classList.add('tip-yellow');
return false;
}
// 验证码正确性由服务端在重置提交时校验(一次性消费)
return code;
}
function checkCaptcha() {
captchaTip.innerHTML = '';
captchaTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
const v = captchaInput.value.trim();
if (!v) {
captchaTip.textContent = '请输入图形验证码!';
captchaTip.classList.add('tip-red');
return false;
}
if (!captchaToken) {
captchaTip.textContent = '图形验证码未加载,请点击图片刷新';
captchaTip.classList.add('tip-red');
return false;
}
return v;
}
passwordInput.addEventListener('focus', checkPassword);
repeatPasswordInput.addEventListener('focus', checkPassword);
passwordInput.addEventListener('input', checkPassword);
repeatPasswordInput.addEventListener('input', checkPassword);
passwordInput.addEventListener('blur', checkPassword);
repeatPasswordInput.addEventListener('blur', checkPassword);
emailInput.addEventListener('focus', checkEmail);
emailInput.addEventListener('input', function () {
checkEmail();
if (codeTarget) {
checkCode();
}
});
emailInput.addEventListener('blur', checkEmail);
codeInput.addEventListener('focus', checkCode);
codeInput.addEventListener('input', checkCode);
codeInput.addEventListener('blur', checkCode);
// 发送重置密码验证码:/reset/send_code(CAPTCHA 必填,gateway 强制校验防邮件轰炸)
getCodeButton.addEventListener('click', async function () {
if (getCodeButton.disabled) return; // 防重复点击
const email = checkEmail();
if (!email) return;
const captchaAnswer = checkCaptcha();
if (!captchaAnswer) return;
getCodeButton.disabled = true;
codeTip.textContent = '验证码发送中…';
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
codeTip.classList.add('tip-yellow');
let httpStatus = 0;
try {
const res = await fetch('/reset/send_code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
captcha_token: captchaToken,
captcha_answer: captchaAnswer
})
});
httpStatus = res.status;
const responseText = await res.text();
if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE);
let result;
try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); }
if (!res.ok || !result.success) throw new Error(result.message || '验证码发送失败');
codeTarget = email;
codeInput.value = '';
codeTip.textContent = '验证码已发送至邮箱,请查收(10 分钟内有效)';
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
codeTip.classList.add('tip-green');
startResendCooldown(60);
} catch (err) {
codeTip.textContent = err.message || '验证码发送失败,请稍后重试';
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
codeTip.classList.add('tip-red');
if (httpStatus === 429) startResendCooldown(60); // 命中冷却/限流,倒计时对齐
} finally {
// CAPTCHA Token 一次性消费,无论成败都刷新图形验证码;
// 刷新时保留已输入的图形验证码答案,避免误清空用户输入
refreshCaptcha(true);
if (!cooldownTimer) getCodeButton.disabled = false;
}
});
// 提交重置密码:/reset_password(创建用户改为更新密码;用户不存在时服务端统一返回成功防枚举)
form.addEventListener('submit', async function (e) {
e.preventDefault();
const email = checkEmail();
const password = checkPassword();
const code = checkCode();
if (!email || !password || !code) {
return;
}
const jsonBody = JSON.stringify({
username: email,
password,
email_code: code
});
try {
submitButton.disabled = true;
const res = await fetch('/reset_password', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: jsonBody
});
const responseText = await res.text();
if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE);
let result;
try {
result = JSON.parse(responseText);
} catch {
console.error(`重置密码接口返回非 JSON 内容(HTTP ${res.status}):`, responseText);
throw new Error('服务器响应格式异常,请稍后重试');
}
if (!res.ok) {
throw new Error(result.message || `重置密码失败(HTTP ${res.status})`);
}
if (result.success) {
alert('重置成功,请使用新密码登录');
form.reset();
codeTarget = '';
location.href = 'login.html';
return;
}
alert(result.message || '重置失败');
} catch (err) {
console.error('重置密码请求异常:', err);
alert(err.message || '接口请求失败,请检查后端服务是否启动');
} finally {
submitButton.disabled = false;
}
});
</script>
</body>
</html>
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 卿松
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-345
View File
@@ -1,345 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆翻牌游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Arial Rounded MT Bold', 'Segoe UI', sans-serif;
}
body {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: white;
}
.container {
max-width: 800px;
width: 100%;
text-align: center;
}
h1 {
font-size: 2.8rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.game-info {
display: flex;
justify-content: space-between;
margin: 20px 0;
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 15px;
backdrop-filter: blur(10px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.info-item {
font-size: 1.2rem;
font-weight: bold;
}
.game-board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 15px;
margin: 0 auto;
max-width: 500px;
}
.card {
height: 120px;
perspective: 1000px;
cursor: pointer;
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s;
transform-style: preserve-3d;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.card.flipped .card-inner {
transform: rotateY(180deg);
}
.card-front, .card-back {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
}
.card-front {
background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
color: #333;
}
.card-back {
background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%);
color: white;
transform: rotateY(180deg);
}
.card.matched .card-back {
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
}
.controls {
margin-top: 30px;
}
button {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
border: none;
padding: 12px 30px;
font-size: 1.1rem;
border-radius: 50px;
cursor: pointer;
color: #333;
font-weight: bold;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
margin: 0 10px;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
}
button:active {
transform: translateY(1px);
}
.message {
margin-top: 20px;
font-size: 1.3rem;
font-weight: bold;
min-height: 30px;
}
@media (max-width: 600px) {
.game-board {
grid-template-columns: repeat(3, 1fr);
}
.card {
height: 100px;
}
h1 {
font-size: 2.2rem;
}
}
@media (max-width: 400px) {
.game-board {
grid-template-columns: repeat(2, 1fr);
}
.card {
height: 90px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>记忆翻牌游戏</h1>
<p>找出所有匹配的卡片对,考验你的记忆力!</p>
<div class="game-info">
<div class="info-item">时间: <span id="timer">0</span>秒</div>
<div class="info-item">移动次数: <span id="moves">0</span></div>
<div class="info-item">匹配: <span id="matches">0</span>/8</div>
</div>
<div class="game-board" id="gameBoard"></div>
<div class="message" id="message"></div>
<div class="controls">
<button id="startBtn">开始游戏</button>
<button id="resetBtn">重新开始</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const gameBoard = document.getElementById('gameBoard');
const startBtn = document.getElementById('startBtn');
const resetBtn = document.getElementById('resetBtn');
const timerEl = document.getElementById('timer');
const movesEl = document.getElementById('moves');
const matchesEl = document.getElementById('matches');
const messageEl = document.getElementById('message');
let cards = [];
let flippedCards = [];
let matchedPairs = 0;
let moves = 0;
let timer = 0;
let timerInterval;
let gameStarted = false;
// 卡片符号
const cardSymbols = ['🍎', '🍌', '🍒', '🍇', '🍊', '🍓', '🍑', '🥭'];
// 初始化游戏
function initGame() {
// 创建卡片数组(每对两张)
cards = [...cardSymbols, ...cardSymbols];
// 洗牌算法
for (let i = cards.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[cards[i], cards[j]] = [cards[j], cards[i]];
}
// 清空游戏板
gameBoard.innerHTML = '';
// 创建卡片元素
cards.forEach((symbol, index) => {
const card = document.createElement('div');
card.className = 'card';
card.dataset.index = index;
card.dataset.symbol = symbol;
card.innerHTML = `
<div class="card-inner">
<div class="card-front">?</div>
<div class="card-back">${symbol}</div>
</div>
`;
card.addEventListener('click', flipCard);
gameBoard.appendChild(card);
});
// 重置游戏状态
flippedCards = [];
matchedPairs = 0;
moves = 0;
timer = 0;
gameStarted = false;
// 更新显示
movesEl.textContent = moves;
matchesEl.textContent = matchedPairs;
timerEl.textContent = timer;
messageEl.textContent = '点击"开始游戏"按钮开始';
}
// 翻转卡片
function flipCard() {
if (!gameStarted) return;
const card = this;
// 如果卡片已经匹配或已经翻转,则忽略点击
if (card.classList.contains('matched') || card.classList.contains('flipped')) {
return;
}
// 翻转卡片
card.classList.add('flipped');
flippedCards.push(card);
// 如果翻转了两张卡片,检查是否匹配
if (flippedCards.length === 2) {
moves++;
movesEl.textContent = moves;
const card1 = flippedCards[0];
const card2 = flippedCards[1];
if (card1.dataset.symbol === card2.dataset.symbol) {
// 匹配成功
card1.classList.add('matched');
card2.classList.add('matched');
matchedPairs++;
matchesEl.textContent = matchedPairs;
flippedCards = [];
// 检查游戏是否结束
if (matchedPairs === cardSymbols.length) {
endGame();
}
} else {
// 不匹配,翻转回去
setTimeout(() => {
card1.classList.remove('flipped');
card2.classList.remove('flipped');
flippedCards = [];
}, 1000);
}
}
}
// 开始游戏
function startGame() {
if (gameStarted) return;
gameStarted = true;
messageEl.textContent = '游戏进行中...';
// 启动计时器
timerInterval = setInterval(() => {
timer++;
timerEl.textContent = timer;
}, 1000);
// 禁用开始按钮
startBtn.disabled = true;
}
// 结束游戏
function endGame() {
clearInterval(timerInterval);
messageEl.textContent = `恭喜!你用了${moves}步和${timer}秒完成了游戏!`;
startBtn.disabled = false;
}
// 重置游戏
function resetGame() {
clearInterval(timerInterval);
initGame();
startBtn.disabled = false;
}
// 事件监听
startBtn.addEventListener('click', startGame);
resetBtn.addEventListener('click', resetGame);
// 初始化游戏
initGame();
});
</script>
</body>
</html>
-813
View File
@@ -1,813 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>太空入侵者</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: white;
overflow-x: hidden;
padding: 20px;
}
.container {
max-width: 800px;
width: 100%;
text-align: center;
}
h1 {
font-size: 3rem;
margin-bottom: 10px;
text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff;
letter-spacing: 3px;
background: linear-gradient(to right, #00ffff, #ff00ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 1.2rem;
margin-bottom: 20px;
color: #a0e7ff;
}
.game-container {
position: relative;
margin: 20px auto;
border: 3px solid #00ffff;
border-radius: 10px;
box-shadow: 0 0 20px #00ffff, inset 0 0 20px rgba(0, 255, 255, 0.1);
overflow: hidden;
max-width: 800px;
}
#gameCanvas {
background: #000;
display: block;
margin: 0 auto;
width: 100%;
max-width: 800px;
}
.game-ui {
display: flex;
justify-content: space-between;
margin: 15px 0;
padding: 15px;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
border: 1px solid #00ffff;
}
.ui-item {
font-size: 1.2rem;
font-weight: bold;
text-shadow: 0 0 5px #00ffff;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin: 20px 0;
flex-wrap: wrap;
}
button {
background: linear-gradient(135deg, #00ffff, #ff00ff);
border: none;
padding: 12px 25px;
font-size: 1.1rem;
border-radius: 50px;
cursor: pointer;
color: #000;
font-weight: bold;
box-shadow: 0 0 15px rgba(0, 255, 255, 0.7);
transition: all 0.3s ease;
min-width: 140px;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 0 20px rgba(0, 255, 255, 0.9);
}
button:active {
transform: translateY(1px);
}
button:disabled {
background: #555;
color: #888;
box-shadow: none;
cursor: not-allowed;
}
.instructions {
margin-top: 25px;
padding: 15px;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
border: 1px solid #00ffff;
max-width: 800px;
}
.instructions h3 {
color: #00ffff;
margin-bottom: 10px;
}
.instructions p {
margin: 8px 0;
color: #a0e7ff;
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 30px;
border-radius: 15px;
border: 2px solid #ff00ff;
text-align: center;
box-shadow: 0 0 30px #ff00ff;
z-index: 10;
display: none;
width: 80%;
max-width: 400px;
}
.game-over h2 {
font-size: 2.2rem;
color: #ff00ff;
margin-bottom: 15px;
text-shadow: 0 0 10px #ff00ff;
}
.game-over p {
font-size: 1.2rem;
margin: 10px 0;
}
.stars {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
.star {
position: absolute;
background: white;
border-radius: 50%;
animation: twinkle 5s infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.2; }
50% { opacity: 1; }
}
@media (max-width: 700px) {
h1 {
font-size: 2.2rem;
}
.game-ui {
flex-direction: column;
gap: 10px;
}
.controls {
flex-direction: column;
align-items: center;
}
button {
width: 100%;
max-width: 250px;
}
.instructions {
font-size: 0.9rem;
}
}
@media (max-width: 500px) {
h1 {
font-size: 1.8rem;
}
.subtitle {
font-size: 1rem;
}
.ui-item {
font-size: 1rem;
}
.game-over h2 {
font-size: 1.8rem;
}
.game-over p {
font-size: 1rem;
}
}
</style>
</head>
<body>
<div class="stars" id="stars"></div>
<div class="container">
<h1>太空入侵者</h1>
<p class="subtitle">保卫地球,击退外星入侵者!</p>
<div class="game-ui">
<div class="ui-item">分数: <span id="score">0</span></div>
<div class="ui-item">生命: <span id="lives">3</span></div>
<div class="ui-item">等级: <span id="level">1</span></div>
</div>
<div class="game-container">
<canvas id="gameCanvas" width="800" height="500"></canvas>
<div class="game-over" id="gameOver">
<h2>游戏结束</h2>
<p>你的最终分数: <span id="finalScore">0</span></p>
<p id="gameMessage">地球需要你,再试一次!</p>
<button id="restartBtn">重新开始</button>
</div>
</div>
<div class="controls">
<button id="startBtn">开始游戏</button>
<button id="pauseBtn">暂停游戏</button>
</div>
<div class="instructions">
<h3>游戏说明</h3>
<p>← → 键: 移动飞船</p>
<p>空格键: 发射激光</p>
<p>躲避敌人的攻击,击落所有外星飞船!</p>
<p>每关敌人数量和速度会增加,坚持越久得分越高!</p>
<p>每得1000分升一级,敌人速度会加快!</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// 创建星空背景
createStars();
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const restartBtn = document.getElementById('restartBtn');
const scoreEl = document.getElementById('score');
const livesEl = document.getElementById('lives');
const levelEl = document.getElementById('level');
const gameOverEl = document.getElementById('gameOver');
const finalScoreEl = document.getElementById('finalScore');
const gameMessageEl = document.getElementById('gameMessage');
// 游戏状态
let game = {
running: false,
paused: false,
score: 0,
lives: 3,
level: 1,
enemies: [],
bullets: [],
enemyBullets: [],
particles: [],
lastEnemySpawn: 0,
lastEnemyShoot: 0
};
// 玩家飞船
const player = {
x: canvas.width / 2 - 25,
y: canvas.height - 70,
width: 50,
height: 60,
speed: 8,
color: '#00ffff',
lastShot: 0,
shotDelay: 300
};
// 键盘状态
const keys = {};
// 初始化游戏
function initGame() {
game = {
running: true,
paused: false,
score: 0,
lives: 3,
level: 1,
enemies: [],
bullets: [],
enemyBullets: [],
particles: [],
lastEnemySpawn: 0,
lastEnemyShoot: 0
};
player.x = canvas.width / 2 - 25;
player.y = canvas.height - 70;
updateUI();
gameOverEl.style.display = 'none';
pauseBtn.textContent = '暂停游戏';
// 开始游戏循环
if (!game.paused) {
requestAnimationFrame(gameLoop);
}
}
// 创建星空背景
function createStars() {
const starsContainer = document.getElementById('stars');
const starCount = 150;
for (let i = 0; i < starCount; i++) {
const star = document.createElement('div');
star.className = 'star';
const size = Math.random() * 3;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
star.style.animationDelay = `${Math.random() * 5}s`;
starsContainer.appendChild(star);
}
}
// 绘制玩家飞船
function drawPlayer() {
ctx.fillStyle = player.color;
// 飞船主体
ctx.beginPath();
ctx.moveTo(player.x + player.width / 2, player.y);
ctx.lineTo(player.x + player.width, player.y + player.height);
ctx.lineTo(player.x, player.y + player.height);
ctx.closePath();
ctx.fill();
// 飞船引擎
ctx.fillStyle = '#ff9900';
ctx.fillRect(player.x + player.width / 2 - 10, player.y + player.height, 20, 10);
// 飞船细节
ctx.fillStyle = '#ffffff';
ctx.fillRect(player.x + player.width / 2 - 5, player.y + 10, 10, 15);
}
// 生成敌人
function spawnEnemy() {
const now = Date.now();
if (now - game.lastEnemySpawn > 1000 - game.level * 50) {
const enemy = {
x: Math.random() * (canvas.width - 40),
y: -40,
width: 40,
height: 40,
speed: 1 + game.level * 0.2,
color: `hsl(${Math.random() * 360}, 100%, 60%)`,
health: 1
};
game.enemies.push(enemy);
game.lastEnemySpawn = now;
}
}
// 绘制敌人
function drawEnemies() {
game.enemies.forEach(enemy => {
ctx.fillStyle = enemy.color;
// 敌人飞船主体
ctx.beginPath();
ctx.moveTo(enemy.x + enemy.width / 2, enemy.y + enemy.height);
ctx.lineTo(enemy.x + enemy.width, enemy.y);
ctx.lineTo(enemy.x, enemy.y);
ctx.closePath();
ctx.fill();
// 敌人飞船细节
ctx.fillStyle = '#ff0000';
ctx.fillRect(enemy.x + enemy.width / 2 - 5, enemy.y + 10, 10, 10);
});
}
// 绘制子弹
function drawBullets() {
// 玩家子弹
ctx.fillStyle = '#00ffff';
game.bullets.forEach(bullet => {
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
});
// 敌人子弹
ctx.fillStyle = '#ff0000';
game.enemyBullets.forEach(bullet => {
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
});
}
// 绘制粒子效果
function drawParticles() {
game.particles.forEach(particle => {
ctx.fillStyle = particle.color;
ctx.globalAlpha = particle.alpha;
ctx.fillRect(particle.x, particle.y, particle.size, particle.size);
});
ctx.globalAlpha = 1;
}
// 玩家射击
function playerShoot() {
const now = Date.now();
if (now - player.lastShot > player.shotDelay) {
const bullet = {
x: player.x + player.width / 2 - 2,
y: player.y,
width: 4,
height: 15,
speed: 10,
color: '#00ffff'
};
game.bullets.push(bullet);
player.lastShot = now;
// 射击音效
playLaserSound();
}
}
// 敌人射击
function enemyShoot() {
const now = Date.now();
if (now - game.lastEnemyShoot > 2000 - game.level * 100 && game.enemies.length > 0) {
const shooter = game.enemies[Math.floor(Math.random() * game.enemies.length)];
const bullet = {
x: shooter.x + shooter.width / 2 - 2,
y: shooter.y + shooter.height,
width: 4,
height: 15,
speed: 5,
color: '#ff0000'
};
game.enemyBullets.push(bullet);
game.lastEnemyShoot = now;
}
}
// 创建爆炸粒子
function createExplosion(x, y, color) {
for (let i = 0; i < 15; i++) {
const particle = {
x: x,
y: y,
size: Math.random() * 5 + 2,
speedX: (Math.random() - 0.5) * 5,
speedY: (Math.random() - 0.5) * 5,
color: color,
alpha: 1,
decay: 0.02
};
game.particles.push(particle);
}
}
// 更新游戏对象位置
function updatePositions() {
// 更新玩家子弹
game.bullets = game.bullets.filter(bullet => {
bullet.y -= bullet.speed;
return bullet.y > 0;
});
// 更新敌人子弹
game.enemyBullets = game.enemyBullets.filter(bullet => {
bullet.y += bullet.speed;
return bullet.y < canvas.height;
});
// 更新敌人
game.enemies = game.enemies.filter(enemy => {
enemy.y += enemy.speed;
// 敌人超出屏幕
if (enemy.y > canvas.height) {
game.lives--;
updateUI();
return false;
}
return true;
});
// 更新粒子
game.particles = game.particles.filter(particle => {
particle.x += particle.speedX;
particle.y += particle.speedY;
particle.alpha -= particle.decay;
return particle.alpha > 0;
});
}
// 检测碰撞
function checkCollisions() {
// 玩家子弹与敌人碰撞
game.bullets.forEach((bullet, bulletIndex) => {
game.enemies.forEach((enemy, enemyIndex) => {
if (
bullet.x < enemy.x + enemy.width &&
bullet.x + bullet.width > enemy.x &&
bullet.y < enemy.y + enemy.height &&
bullet.y + bullet.height > enemy.y
) {
// 击中敌人
game.bullets.splice(bulletIndex, 1);
game.enemies.splice(enemyIndex, 1);
game.score += 100;
createExplosion(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2, enemy.color);
updateUI();
// 爆炸音效
playExplosionSound();
}
});
});
// 敌人子弹与玩家碰撞
game.enemyBullets.forEach((bullet, bulletIndex) => {
if (
bullet.x < player.x + player.width &&
bullet.x + bullet.width > player.x &&
bullet.y < player.y + player.height &&
bullet.y + bullet.height > player.y
) {
// 玩家被击中
game.enemyBullets.splice(bulletIndex, 1);
game.lives--;
createExplosion(player.x + player.width / 2, player.y + player.height / 2, player.color);
updateUI();
if (game.lives <= 0) {
endGame(false);
}
}
});
// 敌人与玩家碰撞
game.enemies.forEach((enemy, enemyIndex) => {
if (
enemy.x < player.x + player.width &&
enemy.x + enemy.width > player.x &&
enemy.y < player.y + player.height &&
enemy.y + enemy.height > player.y
) {
// 玩家与敌人相撞
game.enemies.splice(enemyIndex, 1);
game.lives--;
createExplosion(player.x + player.width / 2, player.y + player.height / 2, player.color);
updateUI();
if (game.lives <= 0) {
endGame(false);
}
}
});
}
// 更新等级
function updateLevel() {
if (game.score >= game.level * 1000) {
game.level++;
levelEl.textContent = game.level;
// 升级音效
playLevelUpSound();
}
}
// 游戏结束
function endGame(win) {
game.running = false;
finalScoreEl.textContent = game.score;
if (win) {
gameMessageEl.textContent = "恭喜!你成功保卫了地球!";
} else {
gameMessageEl.textContent = "地球需要你,再试一次!";
}
gameOverEl.style.display = 'block';
}
// 更新UI
function updateUI() {
scoreEl.textContent = game.score;
livesEl.textContent = game.lives;
levelEl.textContent = game.level;
}
// 游戏主循环
function gameLoop() {
if (!game.running || game.paused) return;
// 清空画布
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成敌人
spawnEnemy();
// 敌人射击
enemyShoot();
// 更新位置
updatePositions();
// 检测碰撞
checkCollisions();
// 更新等级
updateLevel();
// 绘制游戏对象
drawEnemies();
drawBullets();
drawParticles();
drawPlayer();
// 处理玩家移动
if (keys['ArrowLeft'] && player.x > 0) {
player.x -= player.speed;
}
if (keys['ArrowRight'] && player.x < canvas.width - player.width) {
player.x += player.speed;
}
if (keys[' ']) {
playerShoot();
}
// 继续游戏循环
requestAnimationFrame(gameLoop);
}
// 音效函数
function playLaserSound() {
// 在实际应用中,这里会播放音效文件
// 为了简化,我们只创建音频上下文并生成音效
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'square';
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
} catch (e) {
// 音频上下文不支持时的降级处理
console.log("音效播放失败");
}
}
function playExplosionSound() {
// 类似上面的实现,但使用不同的音调
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 100;
oscillator.type = 'sawtooth';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
} catch (e) {
console.log("音效播放失败");
}
}
function playLevelUpSound() {
// 类似上面的实现
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(300, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(600, audioContext.currentTime + 0.2);
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
} catch (e) {
console.log("音效播放失败");
}
}
// 事件监听
window.addEventListener('keydown', (e) => {
keys[e.key] = true;
// 防止空格键滚动页面
if (e.key === ' ') {
e.preventDefault();
}
});
window.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
startBtn.addEventListener('click', () => {
if (!game.running) {
initGame();
startBtn.disabled = true;
setTimeout(() => {
startBtn.disabled = false;
}, 1000);
}
});
pauseBtn.addEventListener('click', () => {
if (game.running) {
game.paused = !game.paused;
pauseBtn.textContent = game.paused ? '继续游戏' : '暂停游戏';
if (!game.paused) {
requestAnimationFrame(gameLoop);
}
}
});
restartBtn.addEventListener('click', () => {
initGame();
});
// 初始UI更新
updateUI();
});
</script>
</body>
</html>
-707
View File
@@ -1,707 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇大作战</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: white;
overflow-x: hidden;
padding: 20px;
}
.container {
max-width: 800px;
width: 100%;
text-align: center;
}
h1 {
font-size: 2.8rem;
margin-bottom: 10px;
text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00;
letter-spacing: 2px;
background: linear-gradient(to right, #00ff00, #ffff00);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 1.1rem;
margin-bottom: 15px;
color: #a0ffa0;
}
.game-container {
position: relative;
margin: 15px auto;
border: 3px solid #00ff00;
border-radius: 10px;
box-shadow: 0 0 20px #00ff00, inset 0 0 20px rgba(0, 255, 0, 0.1);
overflow: hidden;
max-width: 600px;
}
#gameCanvas {
background: #000;
display: block;
margin: 0 auto;
width: 100%;
max-width: 600px;
}
.game-ui {
display: flex;
justify-content: space-between;
margin: 10px 0;
padding: 12px;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
border: 1px solid #00ff00;
flex-wrap: wrap;
}
.ui-item {
font-size: 1.1rem;
font-weight: bold;
text-shadow: 0 0 5px #00ff00;
margin: 5px 0;
}
.controls {
display: flex;
justify-content: center;
gap: 10px;
margin: 15px 0;
flex-wrap: wrap;
}
button {
background: linear-gradient(135deg, #00ff00, #ffff00);
border: none;
padding: 10px 20px;
font-size: 1rem;
border-radius: 50px;
cursor: pointer;
color: #000;
font-weight: bold;
box-shadow: 0 0 15px rgba(0, 255, 0, 0.7);
transition: all 0.3s ease;
min-width: 120px;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 0 20px rgba(0, 255, 0, 0.9);
}
button:active {
transform: translateY(1px);
}
button:disabled {
background: #555;
color: #888;
box-shadow: none;
cursor: not-allowed;
}
.instructions {
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
border: 1px solid #00ff00;
max-width: 600px;
}
.instructions h3 {
color: #00ff00;
margin-bottom: 10px;
font-size: 1.3rem;
}
.instructions p {
margin: 8px 0;
color: #a0ffa0;
font-size: 0.95rem;
line-height: 1.4;
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 25px;
border-radius: 15px;
border: 2px solid #ff0000;
text-align: center;
box-shadow: 0 0 30px #ff0000;
z-index: 10;
display: none;
width: 85%;
max-width: 350px;
}
.game-over h2 {
font-size: 1.8rem;
color: #ff0000;
margin-bottom: 12px;
text-shadow: 0 0 10px #ff0000;
}
.game-over p {
font-size: 1.1rem;
margin: 8px 0;
}
.mobile-controls {
display: none;
margin-top: 15px;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 8px;
max-width: 180px;
}
.mobile-controls button {
width: 55px;
height: 55px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
padding: 0;
min-width: auto;
}
.mobile-controls .up {
grid-column: 2;
grid-row: 1;
}
.mobile-controls .left {
grid-column: 1;
grid-row: 2;
}
.mobile-controls .right {
grid-column: 3;
grid-row: 2;
}
.mobile-controls .down {
grid-column: 2;
grid-row: 3;
}
@media (max-width: 700px) {
h1 {
font-size: 2.2rem;
}
.game-ui {
flex-direction: column;
gap: 8px;
}
.controls {
flex-direction: column;
align-items: center;
gap: 8px;
}
button {
width: 100%;
max-width: 220px;
padding: 8px 15px;
}
.mobile-controls {
display: grid;
}
.instructions {
margin-top: 15px;
padding: 12px;
}
}
@media (max-width: 500px) {
h1 {
font-size: 1.8rem;
}
.subtitle {
font-size: 0.95rem;
}
.ui-item {
font-size: 1rem;
}
.game-over h2 {
font-size: 1.5rem;
}
.game-over p {
font-size: 1rem;
}
.instructions h3 {
font-size: 1.2rem;
}
.instructions p {
font-size: 0.9rem;
}
}
@media (max-width: 400px) {
body {
padding: 15px;
}
h1 {
font-size: 1.6rem;
}
.game-container {
margin: 10px auto;
}
.controls {
margin: 10px 0;
}
button {
font-size: 0.9rem;
padding: 7px 12px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>贪吃蛇大作战</h1>
<p class="subtitle">控制小蛇吃食物,变得越来越长!</p>
<div class="game-ui">
<div class="ui-item">分数: <span id="score">0</span></div>
<div class="ui-item">长度: <span id="length">1</span></div>
<div class="ui-item">速度: <span id="speed">1</span></div>
</div>
<div class="game-container">
<canvas id="gameCanvas" width="600" height="400"></canvas>
<div class="game-over" id="gameOver">
<h2>游戏结束</h2>
<p>你的最终分数: <span id="finalScore">0</span></p>
<p>蛇的长度: <span id="finalLength">0</span></p>
<p id="gameMessage">再试一次,创造新纪录!</p>
<button id="restartBtn">重新开始</button>
</div>
</div>
<div class="mobile-controls">
<button class="up">↑</button>
<button class="left">←</button>
<button class="right">→</button>
<button class="down">↓</button>
</div>
<div class="controls">
<button id="startBtn">开始游戏</button>
<button id="pauseBtn">暂停游戏</button>
<button id="speedUpBtn">加速</button>
<button id="speedDownBtn">减速</button>
</div>
<div class="instructions">
<h3>游戏说明</h3>
<p>使用方向键或屏幕按钮控制蛇的移动方向</p>
<p>吃到食物可以增加蛇的长度和得分</p>
<p>撞到墙壁或自己的身体会导致游戏结束</p>
<p>每得100分速度会增加一级</p>
<p>看看你能让蛇变得多长!</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const restartBtn = document.getElementById('restartBtn');
const speedUpBtn = document.getElementById('speedUpBtn');
const speedDownBtn = document.getElementById('speedDownBtn');
const scoreEl = document.getElementById('score');
const lengthEl = document.getElementById('length');
const speedEl = document.getElementById('speed');
const gameOverEl = document.getElementById('gameOver');
const finalScoreEl = document.getElementById('finalScore');
const finalLengthEl = document.getElementById('finalLength');
const gameMessageEl = document.getElementById('gameMessage');
// 移动控制按钮
const upBtn = document.querySelector('.mobile-controls .up');
const leftBtn = document.querySelector('.mobile-controls .left');
const rightBtn = document.querySelector('.mobile-controls .right');
const downBtn = document.querySelector('.mobile-controls .down');
// 游戏配置
const gridSize = 20;
const gridWidth = canvas.width / gridSize;
const gridHeight = canvas.height / gridSize;
// 游戏状态
let game = {
running: false,
paused: false,
score: 0,
speed: 1,
baseSpeed: 8,
gameLoop: null
};
// 蛇的状态
let snake = {
body: [],
direction: 'right',
nextDirection: 'right',
color: '#00ff00',
headColor: '#ffff00'
};
// 食物状态
let food = {
x: 0,
y: 0,
color: '#ff0000'
};
// 初始化游戏
function initGame() {
// 初始化蛇
snake.body = [
{x: Math.floor(gridWidth / 2), y: Math.floor(gridHeight / 2)}
];
snake.direction = 'right';
snake.nextDirection = 'right';
// 生成第一个食物
generateFood();
// 重置游戏状态
game.running = true;
game.paused = false;
game.score = 0;
game.speed = 1;
updateUI();
gameOverEl.style.display = 'none';
pauseBtn.textContent = '暂停游戏';
// 开始游戏循环
if (game.gameLoop) clearInterval(game.gameLoop);
game.gameLoop = setInterval(updateGame, 1000 / (game.baseSpeed * game.speed));
// 初始绘制
drawGame();
}
// 生成食物
function generateFood() {
let newFoodPosition;
let foodOnSnake;
do {
foodOnSnake = false;
newFoodPosition = {
x: Math.floor(Math.random() * gridWidth),
y: Math.floor(Math.random() * gridHeight)
};
// 检查食物是否生成在蛇身上
for (let segment of snake.body) {
if (segment.x === newFoodPosition.x && segment.y === newFoodPosition.y) {
foodOnSnake = true;
break;
}
}
} while (foodOnSnake);
food.x = newFoodPosition.x;
food.y = newFoodPosition.y;
}
// 更新游戏状态
function updateGame() {
if (!game.running || game.paused) return;
// 更新蛇的方向
snake.direction = snake.nextDirection;
// 计算新的头部位置
const head = {...snake.body[0]};
switch (snake.direction) {
case 'up':
head.y -= 1;
break;
case 'down':
head.y += 1;
break;
case 'left':
head.x -= 1;
break;
case 'right':
head.x += 1;
break;
}
// 检查游戏结束条件
if (checkCollision(head)) {
endGame();
return;
}
// 添加新的头部
snake.body.unshift(head);
// 检查是否吃到食物
if (head.x === food.x && head.y === food.y) {
// 吃到食物,增加分数
game.score += 10;
// 每100分增加速度
if (game.score % 100 === 0) {
game.speed = Math.min(game.speed + 0.5, 5);
clearInterval(game.gameLoop);
game.gameLoop = setInterval(updateGame, 1000 / (game.baseSpeed * game.speed));
}
// 生成新食物
generateFood();
} else {
// 没吃到食物,移除尾部
snake.body.pop();
}
// 绘制游戏
drawGame();
updateUI();
}
// 检查碰撞
function checkCollision(head) {
// 检查墙壁碰撞
if (head.x < 0 || head.x >= gridWidth || head.y < 0 || head.y >= gridHeight) {
return true;
}
// 检查自身碰撞
for (let i = 1; i < snake.body.length; i++) {
if (head.x === snake.body[i].x && head.y === snake.body[i].y) {
return true;
}
}
return false;
}
// 绘制游戏
function drawGame() {
// 清空画布
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制网格
ctx.strokeStyle = '#222';
ctx.lineWidth = 0.5;
for (let x = 0; x < canvas.width; x += gridSize) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.stroke();
}
for (let y = 0; y < canvas.height; y += gridSize) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(canvas.width, y);
ctx.stroke();
}
// 绘制食物
ctx.fillStyle = food.color;
ctx.beginPath();
ctx.arc(
food.x * gridSize + gridSize / 2,
food.y * gridSize + gridSize / 2,
gridSize / 2 - 1,
0,
Math.PI * 2
);
ctx.fill();
// 绘制蛇
snake.body.forEach((segment, index) => {
if (index === 0) {
// 蛇头
ctx.fillStyle = snake.headColor;
} else {
// 蛇身
ctx.fillStyle = snake.color;
}
ctx.fillRect(
segment.x * gridSize + 1,
segment.y * gridSize + 1,
gridSize - 2,
gridSize - 2
);
// 绘制蛇身圆角
ctx.fillStyle = index === 0 ? snake.headColor : snake.color;
ctx.beginPath();
ctx.arc(
segment.x * gridSize + gridSize / 2,
segment.y * gridSize + gridSize / 2,
gridSize / 2 - 1,
0,
Math.PI * 2
);
ctx.fill();
});
}
// 更新UI
function updateUI() {
scoreEl.textContent = game.score;
lengthEl.textContent = snake.body.length;
speedEl.textContent = game.speed.toFixed(1);
}
// 游戏结束
function endGame() {
game.running = false;
clearInterval(game.gameLoop);
finalScoreEl.textContent = game.score;
finalLengthEl.textContent = snake.body.length;
// 根据分数给出不同消息
if (game.score >= 200) {
gameMessageEl.textContent = "太厉害了!你是贪吃蛇大师!";
} else if (game.score >= 100) {
gameMessageEl.textContent = "很棒!继续努力!";
} else {
gameMessageEl.textContent = "再试一次,创造新纪录!";
}
gameOverEl.style.display = 'block';
}
// 改变方向
function changeDirection(newDirection) {
// 防止直接反向移动
if (
(newDirection === 'up' && snake.direction !== 'down') ||
(newDirection === 'down' && snake.direction !== 'up') ||
(newDirection === 'left' && snake.direction !== 'right') ||
(newDirection === 'right' && snake.direction !== 'left')
) {
snake.nextDirection = newDirection;
}
}
// 改变速度
function changeSpeed(change) {
if (!game.running) return;
game.speed = Math.max(0.5, Math.min(game.speed + change, 5));
speedEl.textContent = game.speed.toFixed(1);
clearInterval(game.gameLoop);
game.gameLoop = setInterval(updateGame, 1000 / (game.baseSpeed * game.speed));
}
// 事件监听
window.addEventListener('keydown', (e) => {
if (!game.running) return;
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
changeDirection('up');
break;
case 'ArrowDown':
e.preventDefault();
changeDirection('down');
break;
case 'ArrowLeft':
e.preventDefault();
changeDirection('left');
break;
case 'ArrowRight':
e.preventDefault();
changeDirection('right');
break;
}
});
// 移动控制按钮事件
upBtn.addEventListener('click', () => changeDirection('up'));
downBtn.addEventListener('click', () => changeDirection('down'));
leftBtn.addEventListener('click', () => changeDirection('left'));
rightBtn.addEventListener('click', () => changeDirection('right'));
// 控制按钮事件
startBtn.addEventListener('click', () => {
if (!game.running) {
initGame();
startBtn.disabled = true;
setTimeout(() => {
startBtn.disabled = false;
}, 1000);
}
});
pauseBtn.addEventListener('click', () => {
if (game.running) {
game.paused = !game.paused;
pauseBtn.textContent = game.paused ? '继续游戏' : '暂停游戏';
}
});
restartBtn.addEventListener('click', () => {
initGame();
});
speedUpBtn.addEventListener('click', () => changeSpeed(0.5));
speedDownBtn.addEventListener('click', () => changeSpeed(-0.5));
// 初始绘制
drawGame();
});
</script>
</body>
</html>
-846
View File
@@ -1,846 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048数字拼图</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: white;
overflow-x: hidden;
padding: 20px;
}
.container {
max-width: 500px;
width: 100%;
text-align: center;
}
h1 {
font-size: 3rem;
margin-bottom: 10px;
text-shadow: 0 0 10px #ff9900, 0 0 20px #ff9900;
letter-spacing: 2px;
background: linear-gradient(to right, #ff9900, #ffff00);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 1.1rem;
margin-bottom: 15px;
color: #ffcc80;
}
.game-info {
display: flex;
justify-content: space-between;
margin: 15px 0;
padding: 12px;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
border: 1px solid #ff9900;
flex-wrap: wrap;
}
.info-item {
font-size: 1.1rem;
font-weight: bold;
text-shadow: 0 0 5px #ff9900;
margin: 5px 0;
}
.game-container {
position: relative;
margin: 15px auto;
border: 3px solid #ff9900;
border-radius: 10px;
box-shadow: 0 0 20px #ff9900, inset 0 0 20px rgba(255, 153, 0, 0.1);
overflow: hidden;
max-width: 450px;
background: rgba(0, 0, 0, 0.3);
padding: 15px;
}
.grid-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 10px;
width: 100%;
aspect-ratio: 1/1;
background: rgba(187, 173, 160, 0.3);
border-radius: 6px;
padding: 10px;
position: relative;
}
.grid-cell {
background: rgba(238, 228, 218, 0.35);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
font-weight: bold;
color: #776e65;
}
.tile {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
font-weight: bold;
border-radius: 3px;
transition: all 0.15s ease;
z-index: 10;
}
.tile-2 { background: #eee4da; color: #776e65; }
.tile-4 { background: #ede0c8; color: #776e65; }
.tile-8 { background: #f2b179; color: white; }
.tile-16 { background: #f59563; color: white; }
.tile-32 { background: #f67c5f; color: white; }
.tile-64 { background: #f65e3b; color: white; }
.tile-128 { background: #edcf72; color: white; font-size: 1.5rem; }
.tile-256 { background: #edcc61; color: white; font-size: 1.5rem; }
.tile-512 { background: #edc850; color: white; font-size: 1.5rem; }
.tile-1024 { background: #edc53f; color: white; font-size: 1.2rem; }
.tile-2048 { background: #edc22e; color: white; font-size: 1.2rem; }
.controls {
display: flex;
justify-content: center;
gap: 10px;
margin: 15px 0;
flex-wrap: wrap;
}
button {
background: linear-gradient(135deg, #ff9900, #ffff00);
border: none;
padding: 10px 20px;
font-size: 1rem;
border-radius: 50px;
cursor: pointer;
color: #000;
font-weight: bold;
box-shadow: 0 0 15px rgba(255, 153, 0, 0.7);
transition: all 0.3s ease;
min-width: 120px;
}
button:hover {
transform: translateY(-3px);
box-shadow: 0 0 20px rgba(255, 153, 0, 0.9);
}
button:active {
transform: translateY(1px);
}
button:disabled {
background: #555;
color: #888;
box-shadow: none;
cursor: not-allowed;
}
.instructions {
margin-top: 20px;
padding: 15px;
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
border: 1px solid #ff9900;
max-width: 500px;
}
.instructions h3 {
color: #ff9900;
margin-bottom: 10px;
font-size: 1.3rem;
}
.instructions p {
margin: 8px 0;
color: #ffcc80;
font-size: 0.95rem;
line-height: 1.4;
}
.game-over {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 25px;
border-radius: 15px;
border: 2px solid #ff0000;
text-align: center;
box-shadow: 0 0 30px #ff0000;
z-index: 20;
display: none;
width: 85%;
max-width: 350px;
}
.game-over h2 {
font-size: 1.8rem;
color: #ff0000;
margin-bottom: 12px;
text-shadow: 0 0 10px #ff0000;
}
.game-over p {
font-size: 1.1rem;
margin: 8px 0;
}
.win-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 25px;
border-radius: 15px;
border: 2px solid #00ff00;
text-align: center;
box-shadow: 0 0 30px #00ff00;
z-index: 20;
display: none;
width: 85%;
max-width: 350px;
}
.win-message h2 {
font-size: 1.8rem;
color: #00ff00;
margin-bottom: 12px;
text-shadow: 0 0 10px #00ff00;
}
.win-message p {
font-size: 1.1rem;
margin: 8px 0;
}
.mobile-controls {
display: none;
margin-top: 15px;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 8px;
max-width: 180px;
}
.mobile-controls button {
width: 55px;
height: 55px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
padding: 0;
min-width: auto;
}
.mobile-controls .up {
grid-column: 2;
grid-row: 1;
}
.mobile-controls .left {
grid-column: 1;
grid-row: 2;
}
.mobile-controls .right {
grid-column: 3;
grid-row: 2;
}
.mobile-controls .down {
grid-column: 2;
grid-row: 3;
}
@media (max-width: 600px) {
h1 {
font-size: 2.2rem;
}
.game-info {
flex-direction: column;
gap: 8px;
}
.controls {
flex-direction: column;
align-items: center;
gap: 8px;
}
button {
width: 100%;
max-width: 220px;
padding: 8px 15px;
}
.mobile-controls {
display: grid;
}
.instructions {
margin-top: 15px;
padding: 12px;
}
.tile {
font-size: 1.5rem;
}
.tile-128, .tile-256, .tile-512 {
font-size: 1.2rem;
}
.tile-1024, .tile-2048 {
font-size: 1rem;
}
}
@media (max-width: 400px) {
body {
padding: 15px;
}
h1 {
font-size: 1.8rem;
}
.subtitle {
font-size: 0.95rem;
}
.game-container {
margin: 10px auto;
padding: 10px;
}
.controls {
margin: 10px 0;
}
button {
font-size: 0.9rem;
padding: 7px 12px;
}
.grid-cell, .tile {
font-size: 1.3rem;
}
}
</style>
</head>
<body>
<div class="container">
<h1>2048数字拼图</h1>
<p class="subtitle">合并相同数字,挑战2048!</p>
<div class="game-info">
<div class="info-item">分数: <span id="score">0</span></div>
<div class="info-item">步数: <span id="moves">0</span></div>
<div class="info-item">最高分: <span id="best">0</span></div>
</div>
<div class="game-container">
<div class="grid-container" id="grid">
<!-- 网格将由JavaScript生成 -->
</div>
<div class="game-over" id="gameOver">
<h2>游戏结束</h2>
<p>你的最终分数: <span id="finalScore">0</span></p>
<p id="gameMessage">再试一次,创造新纪录!</p>
<button id="restartBtn">重新开始</button>
</div>
<div class="win-message" id="winMessage">
<h2>恭喜你赢了!</h2>
<p>你成功合成了2048!</p>
<p>分数: <span id="winScore">0</span></p>
<button id="continueBtn">继续游戏</button>
</div>
</div>
<div class="mobile-controls">
<button class="up">↑</button>
<button class="left">←</button>
<button class="right">→</button>
<button class="down">↓</button>
</div>
<div class="controls">
<button id="startBtn">开始游戏</button>
<button id="undoBtn">撤销</button>
<button id="hintBtn">提示</button>
</div>
<div class="instructions">
<h3>游戏说明</h3>
<p>使用方向键或屏幕按钮移动方块</p>
<p>相同数字的方块相撞时会合并成为它们的和</p>
<p>每次移动后会在空白处随机生成一个2或4的方块</p>
<p>当棋盘被填满且无法移动时游戏结束</p>
<p>成功合成2048即可获胜,但可以继续挑战更高分!</p>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const gridContainer = document.getElementById('grid');
const startBtn = document.getElementById('startBtn');
const undoBtn = document.getElementById('undoBtn');
const hintBtn = document.getElementById('hintBtn');
const restartBtn = document.getElementById('restartBtn');
const continueBtn = document.getElementById('continueBtn');
const scoreEl = document.getElementById('score');
const movesEl = document.getElementById('moves');
const bestEl = document.getElementById('best');
const gameOverEl = document.getElementById('gameOver');
const winMessageEl = document.getElementById('winMessage');
const finalScoreEl = document.getElementById('finalScore');
const winScoreEl = document.getElementById('winScore');
const gameMessageEl = document.getElementById('gameMessage');
// 移动控制按钮
const upBtn = document.querySelector('.mobile-controls .up');
const leftBtn = document.querySelector('.mobile-controls .left');
const rightBtn = document.querySelector('.mobile-controls .right');
const downBtn = document.querySelector('.mobile-controls .down');
// 游戏状态
let game = {
grid: [],
score: 0,
moves: 0,
best: localStorage.getItem('2048-best') || 0,
gameOver: false,
won: false,
history: []
};
// 初始化游戏
function initGame() {
// 创建4x4网格
game.grid = Array(4).fill().map(() => Array(4).fill(0));
game.score = 0;
game.moves = 0;
game.gameOver = false;
game.won = false;
game.history = [];
// 生成网格HTML
gridContainer.innerHTML = '';
for (let i = 0; i < 16; i++) {
const cell = document.createElement('div');
cell.className = 'grid-cell';
gridContainer.appendChild(cell);
}
// 添加两个初始方块
addRandomTile();
addRandomTile();
updateUI();
gameOverEl.style.display = 'none';
winMessageEl.style.display = 'none';
// 保存初始状态
saveState();
// 渲染方块
renderTiles();
}
// 添加随机方块
function addRandomTile() {
const emptyCells = [];
// 找到所有空格子
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (game.grid[i][j] === 0) {
emptyCells.push({x: i, y: j});
}
}
}
if (emptyCells.length > 0) {
// 随机选择一个空格子
const cell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
// 90%概率生成2,10%概率生成4
game.grid[cell.x][cell.y] = Math.random() < 0.9 ? 2 : 4;
}
}
// 保存游戏状态
function saveState() {
game.history.push({
grid: JSON.parse(JSON.stringify(game.grid)),
score: game.score,
moves: game.moves
});
// 只保留最近10步的历史
if (game.history.length > 10) {
game.history.shift();
}
}
// 撤销上一步
function undoMove() {
if (game.history.length > 1) {
game.history.pop(); // 移除当前状态
const prevState = game.history[game.history.length - 1];
game.grid = JSON.parse(JSON.stringify(prevState.grid));
game.score = prevState.score;
game.moves = prevState.moves;
game.gameOver = false;
game.won = false;
updateUI();
renderTiles();
}
}
// 移动方块
function move(direction) {
if (game.gameOver) return;
const prevGrid = JSON.parse(JSON.stringify(game.grid));
let moved = false;
// 根据方向处理移动
switch (direction) {
case 'up':
moved = moveUp();
break;
case 'down':
moved = moveDown();
break;
case 'left':
moved = moveLeft();
break;
case 'right':
moved = moveRight();
break;
}
// 如果有移动,更新游戏状态
if (moved) {
game.moves++;
addRandomTile();
saveState();
updateUI();
renderTiles();
// 检查游戏是否结束
if (isGameOver()) {
game.gameOver = true;
finalScoreEl.textContent = game.score;
gameOverEl.style.display = 'block';
}
// 检查是否获胜
if (!game.won && hasWon()) {
game.won = true;
winScoreEl.textContent = game.score;
winMessageEl.style.display = 'block';
}
}
}
// 向上移动
function moveUp() {
let moved = false;
for (let j = 0; j < 4; j++) {
for (let i = 1; i < 4; i++) {
if (game.grid[i][j] !== 0) {
let row = i;
while (row > 0 && game.grid[row - 1][j] === 0) {
game.grid[row - 1][j] = game.grid[row][j];
game.grid[row][j] = 0;
row--;
moved = true;
}
if (row > 0 && game.grid[row - 1][j] === game.grid[row][j]) {
game.grid[row - 1][j] *= 2;
game.score += game.grid[row - 1][j];
game.grid[row][j] = 0;
moved = true;
}
}
}
}
return moved;
}
// 向下移动
function moveDown() {
let moved = false;
for (let j = 0; j < 4; j++) {
for (let i = 2; i >= 0; i--) {
if (game.grid[i][j] !== 0) {
let row = i;
while (row < 3 && game.grid[row + 1][j] === 0) {
game.grid[row + 1][j] = game.grid[row][j];
game.grid[row][j] = 0;
row++;
moved = true;
}
if (row < 3 && game.grid[row + 1][j] === game.grid[row][j]) {
game.grid[row + 1][j] *= 2;
game.score += game.grid[row + 1][j];
game.grid[row][j] = 0;
moved = true;
}
}
}
}
return moved;
}
// 向左移动
function moveLeft() {
let moved = false;
for (let i = 0; i < 4; i++) {
for (let j = 1; j < 4; j++) {
if (game.grid[i][j] !== 0) {
let col = j;
while (col > 0 && game.grid[i][col - 1] === 0) {
game.grid[i][col - 1] = game.grid[i][col];
game.grid[i][col] = 0;
col--;
moved = true;
}
if (col > 0 && game.grid[i][col - 1] === game.grid[i][col]) {
game.grid[i][col - 1] *= 2;
game.score += game.grid[i][col - 1];
game.grid[i][col] = 0;
moved = true;
}
}
}
}
return moved;
}
// 向右移动
function moveRight() {
let moved = false;
for (let i = 0; i < 4; i++) {
for (let j = 2; j >= 0; j--) {
if (game.grid[i][j] !== 0) {
let col = j;
while (col < 3 && game.grid[i][col + 1] === 0) {
game.grid[i][col + 1] = game.grid[i][col];
game.grid[i][col] = 0;
col++;
moved = true;
}
if (col < 3 && game.grid[i][col + 1] === game.grid[i][col]) {
game.grid[i][col + 1] *= 2;
game.score += game.grid[i][col + 1];
game.grid[i][col] = 0;
moved = true;
}
}
}
}
return moved;
}
// 检查游戏是否结束
function isGameOver() {
// 检查是否有空格子
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (game.grid[i][j] === 0) {
return false;
}
}
}
// 检查是否有可以合并的相邻格子
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const value = game.grid[i][j];
if (
(i < 3 && game.grid[i + 1][j] === value) ||
(j < 3 && game.grid[i][j + 1] === value)
) {
return false;
}
}
}
return true;
}
// 检查是否获胜
function hasWon() {
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (game.grid[i][j] === 2048) {
return true;
}
}
}
return false;
}
// 渲染方块
function renderTiles() {
// 移除所有现有的方块
const existingTiles = document.querySelectorAll('.tile');
existingTiles.forEach(tile => tile.remove());
// 获取网格容器的实际尺寸
const containerWidth = gridContainer.offsetWidth;
const containerHeight = gridContainer.offsetHeight;
// 计算每个格子的实际尺寸
const cellWidth = (containerWidth - 50) / 4; // 减去内边距和间隙
const cellHeight = (containerHeight - 50) / 4;
// 添加新的方块
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const value = game.grid[i][j];
if (value !== 0) {
const tile = document.createElement('div');
tile.className = `tile tile-${value}`;
tile.textContent = value;
// 使用像素精确定位
const left = 10 + j * (cellWidth + 10);
const top = 10 + i * (cellHeight + 10);
tile.style.left = `${left}px`;
tile.style.top = `${top}px`;
tile.style.width = `${cellWidth}px`;
tile.style.height = `${cellHeight}px`;
gridContainer.appendChild(tile);
}
}
}
}
// 更新UI
function updateUI() {
scoreEl.textContent = game.score;
movesEl.textContent = game.moves;
// 更新最高分
if (game.score > game.best) {
game.best = game.score;
localStorage.setItem('2048-best', game.best);
}
bestEl.textContent = game.best;
}
// 事件监听
window.addEventListener('keydown', (e) => {
if (game.gameOver && !game.won) return;
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
move('up');
break;
case 'ArrowDown':
e.preventDefault();
move('down');
break;
case 'ArrowLeft':
e.preventDefault();
move('left');
break;
case 'ArrowRight':
e.preventDefault();
move('right');
break;
}
});
// 移动控制按钮事件
upBtn.addEventListener('click', () => move('up'));
downBtn.addEventListener('click', () => move('down'));
leftBtn.addEventListener('click', () => move('left'));
rightBtn.addEventListener('click', () => move('right'));
// 控制按钮事件
startBtn.addEventListener('click', () => {
initGame();
});
undoBtn.addEventListener('click', () => {
undoMove();
});
hintBtn.addEventListener('click', () => {
alert('提示:尽量保持最大数字在角落,按顺序合并小数字!');
});
restartBtn.addEventListener('click', () => {
initGame();
});
continueBtn.addEventListener('click', () => {
winMessageEl.style.display = 'none';
});
// 窗口大小改变时重新渲染
window.addEventListener('resize', () => {
if (game.grid && game.grid.length > 0) {
renderTiles();
}
});
// 初始化游戏
initGame();
});
</script>
</body>
</html>
File diff suppressed because it is too large. Load diff
-739
View File
@@ -1,739 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>末日生存 · 二维射击游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #0a1f1a 0%, #1a2f1f 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', 'Arial', sans-serif;
padding: 20px;
margin: 0;
}
.game-container {
background: #2d3a2c;
border-radius: 28px;
box-shadow: 0 20px 30px rgba(0, 0, 0, 0.6), inset 0 0 15px rgba(0,0,0,0.4);
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
display: block;
border-radius: 20px;
box-shadow: 0 0 25px rgba(0, 180, 0, 0.3);
background: #1e2b1c;
width: 100%;
height: auto;
max-width: 800px;
aspect-ratio: 800 / 600;
cursor: crosshair;
}
.info-panel {
width: 100%;
margin-top: 15px;
display: flex;
justify-content: space-between;
align-items: center;
color: #e0f0d8;
font-weight: bold;
background: #1e2b1c;
border-radius: 40px;
padding: 12px 25px;
box-shadow: inset 0 2px 8px rgba(0,0,0,0.5);
flex-wrap: wrap;
gap: 15px;
}
.health-bar {
display: flex;
align-items: center;
gap: 8px;
}
.health-fill {
width: 180px;
height: 14px;
background: #3a2e2e;
border-radius: 20px;
overflow: hidden;
box-shadow: inset 0 2px 5px rgba(0,0,0,0.7);
}
.fill-level {
height: 100%;
width: 100%;
background: linear-gradient(90deg, #e74c3c, #f1c40f);
border-radius: 20px;
transition: width 0.15s ease;
}
.score-board {
background: #0f1a0f;
padding: 6px 20px;
border-radius: 30px;
font-size: 1.2rem;
letter-spacing: 1px;
color: #d4ed5e;
box-shadow: 0 0 12px #5a8f4a;
}
button {
background: #d4b34c;
border: none;
color: #1e2b1c;
font-weight: bold;
font-size: 1rem;
padding: 10px 25px;
border-radius: 30px;
cursor: pointer;
letter-spacing: 1px;
background: linear-gradient(180deg, #e9d584 0%, #b38b40 100%);
box-shadow: 0 5px 0 #6b4e1d, 0 8px 15px rgba(0,0,0,0.4);
transition: all 0.08s ease;
border: 1px solid #f3e3a7;
text-transform: uppercase;
}
button:active {
transform: translateY(3px);
box-shadow: 0 2px 0 #4b3a1a, 0 5px 10px rgba(0,0,0,0.5);
}
.controls {
display: flex;
gap: 10px;
align-items: center;
color: #b9d0a0;
font-size: 0.9rem;
}
@media (max-width: 650px) {
.info-panel {
flex-direction: column;
align-items: center;
}
}
</style>
</head>
<body>
<div style="display: flex; flex-direction: column; align-items: center;">
<div class="game-container">
<canvas id="gameCanvas" width="800" height="600" aria-label="二维射击游戏地图"></canvas>
<div class="info-panel">
<div class="health-bar">
<span style="color:#f1c40f;">❤️ 生命</span>
<div class="health-fill">
<div id="healthFillLevel" class="fill-level" style="width: 100%;"></div>
</div>
<span id="healthText" style="min-width: 50px;">100</span>
</div>
<div class="score-board">
🎯 <span id="scoreDisplay">0</span>
</div>
<div class="controls">
<span>🖱️ 鼠标瞄准 | 左键射击</span>
<button id="restartButton">↺ 重新开始</button>
</div>
</div>
</div>
<div style="color:#8aa87d; margin-top: 8px; font-size: 0.9rem;">
⌨️ WASD / 方向键 移动人物
</div>
</div>
<script>
(function () {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// UI元素
const healthFill = document.getElementById('healthFillLevel');
const healthText = document.getElementById('healthText');
const scoreSpan = document.getElementById('scoreDisplay');
// 固定尺寸
const W = 800, H = 600;
// ----- 游戏状态 -----
let player = {
x: W / 2,
y: H / 2,
radius: 18,
health: 100,
maxHealth: 100,
speed: 4.8,
};
let monsters = [];
let bullets = [];
let score = 0;
// 射击冷却 (防止按住左键瞬间连射太夸张)
const SHOOT_COOLDOWN_MAX = 18; // 帧数 (约60fps => 0.3秒)
let shootCooldown = 0;
// 怪物生成控制
let monsterSpawnTimer = 0;
const SPAWN_DELAY = 45; // 大约每0.75秒生成一只 (60fps)
const MAX_MONSTERS = 14;
// 控制标志
let keys = {
w: false, up: false,
s: false, down: false,
a: false, left: false,
d: false, right: false
};
// 鼠标坐标 (用于瞄准方向)
let mouseX = player.x + 30;
let mouseY = player.y;
let mouseInside = true;
// 游戏循环控制
let gameRunning = true;
let animationFrameId = null;
// ----- 辅助函数:随机范围 -----
const rand = (min, max) => Math.random() * (max - min) + min;
const randInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
// ----- 重置游戏 -----
function restartGame() {
player = {
x: W / 2,
y: H / 2,
radius: 18,
health: 100,
maxHealth: 100,
speed: 4.8,
};
monsters = [];
bullets = [];
score = 0;
shootCooldown = 0;
monsterSpawnTimer = 0;
gameRunning = true;
// 重置鼠标位置到玩家附近
mouseX = player.x + 20;
mouseY = player.y;
updateUI();
}
// ----- 生成怪物 (在屏幕边缘附近) -----
function spawnMonster() {
if (monsters.length >= MAX_MONSTERS) return;
// 随机选择边缘:0=上, 1=下, 2=左, 3=右
const edge = randInt(0, 3);
let spawnX, spawnY;
const padding = 25; // 略微在屏幕外生成,自然走入
switch (edge) {
case 0: // 上
spawnX = rand(padding, W - padding);
spawnY = -padding;
break;
case 1: // 下
spawnX = rand(padding, W - padding);
spawnY = H + padding;
break;
case 2: // 左
spawnX = -padding;
spawnY = rand(padding, H - padding);
break;
case 3: // 右
spawnX = W + padding;
spawnY = rand(padding, H - padding);
break;
default:
spawnX = -padding;
spawnY = rand(padding, H - padding);
}
// 怪物类型:基础速度 + 大小变化
const type = Math.random();
let radius, speed, health, color;
if (type < 0.6) {
// 普通僵尸
radius = 16 + rand(0, 6);
speed = 1.1 + rand(0, 0.9);
health = 1;
color = '#6b8c5c';
} else if (type < 0.88) {
// 快速怪物
radius = 13 + rand(0, 5);
speed = 2.0 + rand(0, 1.3);
health = 1;
color = '#b5876a';
} else {
// 大型怪物 (需要两枪)
radius = 22 + rand(0, 7);
speed = 0.7 + rand(0, 0.7);
health = 2;
color = '#8b3a3a';
}
monsters.push({
x: spawnX,
y: spawnY,
radius: radius,
speed: speed,
health: health,
maxHealth: health,
color: color,
});
}
// ----- 玩家移动 (基于键盘) -----
function movePlayer() {
if (!gameRunning) return;
let dx = 0, dy = 0;
if (keys.w || keys.up) dy -= 1;
if (keys.s || keys.down) dy += 1;
if (keys.a || keys.left) dx -= 1;
if (keys.d || keys.right) dx += 1;
if (dx !== 0 || dy !== 0) {
const len = Math.sqrt(dx * dx + dy * dy);
dx = (dx / len) * player.speed;
dy = (dy / len) * player.speed;
let newX = player.x + dx;
let newY = player.y + dy;
// 边界限制 (考虑玩家半径)
const minBound = player.radius;
const maxBoundX = W - player.radius;
const maxBoundY = H - player.radius;
newX = Math.max(minBound, Math.min(maxBoundX, newX));
newY = Math.max(minBound, Math.min(maxBoundY, newY));
player.x = newX;
player.y = newY;
}
}
// ----- 怪物移动 (朝向玩家) -----
function moveMonsters() {
for (let m of monsters) {
const dx = player.x - m.x;
const dy = player.y - m.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 0.5) continue; // 已重叠
const moveX = (dx / dist) * m.speed;
const moveY = (dy / dist) * m.speed;
m.x += moveX;
m.y += moveY;
}
}
// ----- 玩家射击 (生成子弹) -----
function shootBullet(targetX, targetY) {
if (!gameRunning) return;
// 计算方向向量 (从玩家指向鼠标)
const dx = targetX - player.x;
const dy = targetY - player.y;
const len = Math.sqrt(dx * dx + dy * dy);
if (len < 0.001) return; // 避免零向量
const normX = dx / len;
const normY = dy / len;
bullets.push({
x: player.x,
y: player.y,
vx: normX * 7.5, // 子弹速度
vy: normY * 7.5,
radius: 4.5,
active: true,
damage: 1,
});
}
// ----- 更新子弹位置与碰撞-----
function updateBullets() {
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx;
b.y += b.vy;
// 边界消除 (超出画布太远)
if (b.x < -20 || b.x > W + 20 || b.y < -20 || b.y > H + 20) {
bullets.splice(i, 1);
continue;
}
let bulletHit = false;
// 检测子弹与怪物碰撞
for (let j = monsters.length - 1; j >= 0; j--) {
const m = monsters[j];
const d = Math.sqrt((b.x - m.x) ** 2 + (b.y - m.y) ** 2);
if (d < m.radius + b.radius) {
// 击中怪物
m.health -= b.damage;
bulletHit = true;
if (m.health <= 0) {
// 击杀怪物,增加分数
score += (m.maxHealth === 2 ? 15 : 10);
monsters.splice(j, 1);
}
break; // 一颗子弹只击中一个怪物
}
}
if (bulletHit) {
bullets.splice(i, 1);
}
}
}
// ----- 怪物攻击玩家 (碰撞检测) -----
function checkMonsterCollision() {
if (!gameRunning) return;
for (let i = monsters.length - 1; i >= 0; i--) {
const m = monsters[i];
const dist = Math.sqrt((m.x - player.x) ** 2 + (m.y - player.y) ** 2);
if (dist < player.radius + m.radius) {
// 怪物对玩家造成伤害 (每次碰撞扣血,并推开怪物防止连续帧多次伤害)
const damageAmount = 8 + (m.maxHealth === 2 ? 4 : 0); // 大型怪物伤害更高
player.health -= damageAmount;
// 推开怪物:沿连线方向移动怪物到刚好不重叠
const angle = Math.atan2(m.y - player.y, m.x - player.x);
const overlap = player.radius + m.radius - dist;
if (overlap > 0) {
m.x += Math.cos(angle) * overlap;
m.y += Math.sin(angle) * overlap;
}
// 检查玩家死亡
if (player.health <= 0) {
player.health = 0;
gameRunning = false;
updateUI();
break; // 停止检测其他怪物
}
}
}
// 再次确保玩家生命值不超限
if (player.health <= 0) {
player.health = 0;
gameRunning = false;
}
updateUI();
}
// ----- 更新UI元素-----
function updateUI() {
let healthPercent = Math.max(0, player.health) / player.maxHealth * 100;
healthFill.style.width = `${healthPercent}%`;
healthText.textContent = `${Math.max(0, player.health)}`;
scoreSpan.textContent = score;
}
// ----- 游戏主循环更新-----
function update() {
if (gameRunning) {
// 减少射击冷却
if (shootCooldown > 0) shootCooldown--;
// 移动玩家
movePlayer();
// 怪物移动
moveMonsters();
// 子弹更新与碰撞
updateBullets();
// 怪物攻击玩家
checkMonsterCollision();
// 怪物生成逻辑
if (gameRunning) {
monsterSpawnTimer--;
if (monsterSpawnTimer <= 0 && monsters.length < MAX_MONSTERS) {
spawnMonster();
monsterSpawnTimer = SPAWN_DELAY + randInt(-8, 10);
}
}
// 再次检查玩家死亡 (可能由碰撞导致)
if (player.health <= 0) {
gameRunning = false;
player.health = 0;
}
updateUI();
}
}
// ----- 绘制图形 (所有视觉元素) -----
function draw() {
ctx.clearRect(0, 0, W, H);
// 绘制网格/地面纹理
ctx.strokeStyle = '#3a4a2e';
ctx.lineWidth = 0.6;
for (let i = 0; i < W; i += 40) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, H);
ctx.strokeStyle = '#3a5535';
ctx.stroke();
}
for (let j = 0; j < H; j += 40) {
ctx.beginPath();
ctx.moveTo(0, j);
ctx.lineTo(W, j);
ctx.stroke();
}
// ----- 绘制怪物 -----
for (let m of monsters) {
// 怪物身体
ctx.beginPath();
ctx.arc(m.x, m.y, m.radius, 0, Math.PI * 2);
ctx.fillStyle = m.color;
ctx.fill();
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 2.2;
ctx.stroke();
// 怪物眼睛 (白色+瞳孔朝向玩家)
const eyeOffset = m.radius * 0.5;
const angleToPlayer = Math.atan2(player.y - m.y, player.x - m.x);
const leftEyeX = m.x + Math.cos(angleToPlayer - 0.5) * eyeOffset;
const leftEyeY = m.y + Math.sin(angleToPlayer - 0.5) * eyeOffset;
const rightEyeX = m.x + Math.cos(angleToPlayer + 0.5) * eyeOffset;
const rightEyeY = m.y + Math.sin(angleToPlayer + 0.5) * eyeOffset;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(leftEyeX, leftEyeY, m.radius * 0.27, 0, 2 * Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(rightEyeX, rightEyeY, m.radius * 0.27, 0, 2 * Math.PI);
ctx.fill();
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.arc(leftEyeX + Math.cos(angleToPlayer) * 1.8, leftEyeY + Math.sin(angleToPlayer) * 1.8, m.radius * 0.12, 0, 2 * Math.PI);
ctx.fill();
ctx.beginPath();
ctx.arc(rightEyeX + Math.cos(angleToPlayer) * 1.8, rightEyeY + Math.sin(angleToPlayer) * 1.8, m.radius * 0.12, 0, 2 * Math.PI);
ctx.fill();
// 如果生命值>1 显示护甲标记
if (m.maxHealth === 2 && m.health === 2) {
ctx.fillStyle = '#f0d050';
ctx.beginPath();
ctx.arc(m.x, m.y - m.radius - 4, 4, 0, 2 * Math.PI);
ctx.fill();
} else if (m.maxHealth === 2 && m.health === 1) {
ctx.fillStyle = '#b5773a';
ctx.beginPath();
ctx.arc(m.x, m.y - m.radius - 4, 4, 0, 2 * Math.PI);
ctx.fill();
}
}
// ----- 绘制子弹 -----
for (let b of bullets) {
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, 2 * Math.PI);
ctx.fillStyle = '#f1e05a';
ctx.shadowColor = '#ffbb00';
ctx.shadowBlur = 8;
ctx.fill();
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.strokeStyle = '#b38b20';
ctx.lineWidth = 1.2;
ctx.stroke();
}
// ----- 绘制玩家 (英雄) -----
ctx.save();
ctx.translate(player.x, player.y);
// 玩家面向鼠标方向
const aimAngle = Math.atan2(mouseY - player.y, mouseX - player.x);
ctx.rotate(aimAngle);
// 身体
ctx.beginPath();
ctx.arc(0, 0, player.radius, 0, 2 * Math.PI);
ctx.fillStyle = '#3b7bb7';
ctx.fill();
ctx.strokeStyle = '#102840';
ctx.lineWidth = 2.5;
ctx.stroke();
// 护甲/装备线条
ctx.beginPath();
ctx.arc(0, 0, player.radius - 3, 0, Math.PI * 2);
ctx.strokeStyle = '#c0daf5';
ctx.lineWidth = 2;
ctx.stroke();
// 武器/枪口
ctx.fillStyle = '#4a4a5a';
ctx.fillRect(8, -3.5, 15, 7);
ctx.fillStyle = '#2b2b35';
ctx.fillRect(19, -2.8, 7, 5.6);
// 眼睛
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(5, -7, 5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(5, 7, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#0a0a14';
ctx.beginPath();
ctx.arc(7, -7, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(7, 7, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// 玩家血条 (头顶)
if (gameRunning) {
const healthPercent = player.health / player.maxHealth;
ctx.fillStyle = '#333';
ctx.fillRect(player.x - 22, player.y - player.radius - 13, 44, 8);
ctx.fillStyle = healthPercent > 0.6 ? '#6f9' : (healthPercent > 0.25 ? '#f4c542' : '#e03a3a');
ctx.fillRect(player.x - 22, player.y - player.radius - 13, 44 * healthPercent, 8);
}
// 游戏结束画面
if (!gameRunning) {
ctx.globalAlpha = 0.8;
ctx.fillStyle = '#0f1a0f';
ctx.fillRect(0, 0, W, H);
ctx.globalAlpha = 1;
ctx.fillStyle = '#e0d7b0';
ctx.font = 'bold 38px "Segoe UI", Arial';
ctx.textAlign = 'center';
ctx.fillText('💀 你倒下了', W/2, H/2-20);
ctx.font = '18px "Segoe UI"';
ctx.fillText('点击"重新开始"继续战斗', W/2, H/2+50);
}
}
// ----- 动画循环 -----
function gameLoop() {
update();
draw();
animationFrameId = requestAnimationFrame(gameLoop);
}
// ----- 事件监听 -----
function bindEvents() {
// 键盘按下
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
// 防止页面滚动 / 默认行为
if (key === 'w' || key === 'a' || key === 's' || key === 'd' ||
key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright') {
e.preventDefault();
}
if (key === 'w') keys.w = true;
if (key === 'a') keys.a = true;
if (key === 's') keys.s = true;
if (key === 'd') keys.d = true;
if (key === 'arrowup') keys.up = true;
if (key === 'arrowdown') keys.down = true;
if (key === 'arrowleft') keys.left = true;
if (key === 'arrowright') keys.right = true;
});
window.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
if (key === 'w' || key === 'a' || key === 's' || key === 'd' ||
key === 'arrowup' || key === 'arrowdown' || key === 'arrowleft' || key === 'arrowright') {
e.preventDefault();
}
if (key === 'w') keys.w = false;
if (key === 'a') keys.a = false;
if (key === 's') keys.s = false;
if (key === 'd') keys.d = false;
if (key === 'arrowup') keys.up = false;
if (key === 'arrowdown') keys.down = false;
if (key === 'arrowleft') keys.left = false;
if (key === 'arrowright') keys.right = false;
});
// 鼠标移动 (追踪瞄准位置)
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width; // canvas 物理像素比例
const scaleY = H / rect.height;
const relativeX = (e.clientX - rect.left) * scaleX;
const relativeY = (e.clientY - rect.top) * scaleY;
mouseX = Math.min(W, Math.max(0, relativeX));
mouseY = Math.min(H, Math.max(0, relativeY));
mouseInside = true;
});
canvas.addEventListener('mouseleave', () => {
// 离开画布时瞄准点设为玩家右侧默认方向
mouseX = player.x + 30;
mouseY = player.y;
mouseInside = false;
});
// 射击:鼠标按下 (左键)
canvas.addEventListener('mousedown', (e) => {
e.preventDefault();
if (e.button === 0) { // 左键
if (gameRunning && shootCooldown <= 0) {
shootBullet(mouseX, mouseY);
shootCooldown = SHOOT_COOLDOWN_MAX;
}
}
});
// 防止右键菜单弹出
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// 重新开始按钮
document.getElementById('restartButton').addEventListener('click', () => {
restartGame();
});
// 窗口失去焦点时重置按键状态 (防止卡方向)
window.addEventListener('blur', () => {
keys.w = keys.a = keys.s = keys.d = false;
keys.up = keys.down = keys.left = keys.right = false;
});
}
// 启动一切
bindEvents();
restartGame(); // 初始状态
gameLoop();
// 清理动画 (非必须,但良好实践)
window.addEventListener('beforeunload', () => {
if (animationFrameId) cancelAnimationFrame(animationFrameId);
});
})();
</script>
</body>
</html>
-718
View File
@@ -1,718 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>俄罗斯方块</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
-webkit-tap-highlight-color: transparent;
}
body {
background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: white;
overflow-x: hidden;
padding: 20px;
}
.container {
max-width: 860px;
width: 100%;
text-align: center;
}
h1 {
font-size: 2.6rem;
margin-bottom: 6px;
letter-spacing: 4px;
background: linear-gradient(to right, #00e5ff, #7c4dff, #ff4081);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 30px rgba(0, 229, 255, .35);
}
.subtitle {
font-size: 1rem;
margin-bottom: 16px;
color: #8fd8ff;
}
.layout {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 18px;
flex-wrap: wrap;
}
.panel {
background: rgba(255, 255, 255, .06);
border: 1px solid rgba(255, 255, 255, .12);
border-radius: 14px;
padding: 14px 16px;
backdrop-filter: blur(6px);
min-width: 130px;
}
.panel h3 {
font-size: .85rem;
letter-spacing: 2px;
color: #00e5ff;
margin-bottom: 8px;
text-transform: uppercase;
}
.stat-value {
font-size: 1.6rem;
font-weight: 700;
color: #fff;
text-shadow: 0 0 12px rgba(0, 229, 255, .5);
}
.stat-label {
font-size: .72rem;
color: #9fb3c8;
letter-spacing: 1px;
}
.board-wrap {
position: relative;
border-radius: 14px;
padding: 8px;
background: rgba(0, 0, 0, .45);
border: 1px solid rgba(0, 229, 255, .25);
box-shadow: 0 0 40px rgba(0, 229, 255, .15), inset 0 0 30px rgba(0, 0, 0, .6);
}
canvas#board {
display: block;
border-radius: 8px;
background: #05080f;
}
canvas.preview {
display: block;
margin: 0 auto;
background: rgba(0, 0, 0, .35);
border-radius: 8px;
}
.side {
display: flex;
flex-direction: column;
gap: 12px;
}
.btn {
display: inline-block;
border: none;
cursor: pointer;
border-radius: 10px;
padding: 10px 22px;
font-size: 1rem;
font-weight: 600;
color: #04121a;
background: linear-gradient(135deg, #00e5ff, #2979ff);
box-shadow: 0 4px 18px rgba(0, 229, 255, .35);
transition: transform .12s ease, box-shadow .12s ease, filter .12s;
}
.btn:hover { transform: translateY(-2px); filter: brightness(1.1); }
.btn:active { transform: translateY(1px) scale(.97); }
.btn.ghost {
background: rgba(255, 255, 255, .08);
color: #cfeaff;
border: 1px solid rgba(255, 255, 255, .2);
box-shadow: none;
}
.controls-hint {
margin-top: 16px;
font-size: .82rem;
color: #9fb3c8;
line-height: 1.9;
}
kbd {
background: rgba(255, 255, 255, .1);
border: 1px solid rgba(255, 255, 255, .25);
border-bottom-width: 3px;
border-radius: 6px;
padding: 2px 8px;
font-family: inherit;
font-size: .78rem;
color: #fff;
}
/* 覆盖层(开始 / 暂停 / 结束) */
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
background: rgba(3, 8, 15, .85);
border-radius: 14px;
backdrop-filter: blur(4px);
z-index: 10;
transition: opacity .25s ease;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
.overlay h2 {
font-size: 1.8rem;
letter-spacing: 3px;
color: #00e5ff;
text-shadow: 0 0 18px rgba(0, 229, 255, .6);
}
.overlay p { color: #cfeaff; font-size: .95rem; }
/* 手机触屏按键 */
.touch-controls {
display: none;
margin-top: 16px;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
max-width: 420px;
margin-left: auto;
margin-right: auto;
}
.touch-controls button {
border: 1px solid rgba(0, 229, 255, .35);
background: rgba(0, 229, 255, .1);
color: #aef;
font-size: 1.4rem;
border-radius: 12px;
padding: 14px 0;
cursor: pointer;
user-select: none;
touch-action: manipulation;
}
.touch-controls button:active {
background: rgba(0, 229, 255, .35);
transform: scale(.95);
}
@media (max-width: 768px) {
h1 { font-size: 1.9rem; }
.touch-controls { display: grid; }
.controls-hint { display: none; }
.side { flex-direction: row; flex-wrap: wrap; justify-content: center; }
.panel { min-width: 105px; padding: 10px 12px; }
.stat-value { font-size: 1.25rem; }
}
</style>
</head>
<body>
<div class="container">
<h1>俄罗斯方块</h1>
<p class="subtitle">经典消除 · 挑战极限手速</p>
<div class="layout">
<!-- 左侧面板 -->
<div class="side">
<div class="panel">
<h3>暂存 Hold</h3>
<canvas class="preview" id="holdCanvas" width="96" height="72"></canvas>
</div>
<div class="panel">
<div class="stat-value" id="score">0</div>
<div class="stat-label">分数 SCORE</div>
</div>
<div class="panel">
<div class="stat-value" id="highScore">0</div>
<div class="stat-label">最高分 BEST</div>
</div>
</div>
<!-- 游戏主区域 -->
<div class="board-wrap">
<canvas id="board" width="300" height="600"></canvas>
<div class="overlay" id="startOverlay">
<h2>俄罗斯方块</h2>
<p>消除整行得分,连续消除分数更高!</p>
<button class="btn" id="startBtn">开始游戏</button>
</div>
<div class="overlay hidden" id="pauseOverlay">
<h2>已暂停</h2>
<p>按 P 或点击按钮继续</p>
<button class="btn" id="resumeBtn">继续游戏</button>
</div>
<div class="overlay hidden" id="overOverlay">
<h2>游戏结束</h2>
<p id="finalScore">本局得分:0</p>
<p id="newRecord" style="color:#ffd54f;display:none;">🏆 新纪录!</p>
<button class="btn" id="restartBtn">再来一局</button>
</div>
</div>
<!-- 右侧面板 -->
<div class="side">
<div class="panel">
<h3>下一个 Next</h3>
<canvas class="preview" id="nextCanvas" width="96" height="216"></canvas>
</div>
<div class="panel">
<div class="stat-value" id="lines">0</div>
<div class="stat-label">消除行数 LINES</div>
</div>
<div class="panel">
<div class="stat-value" id="level">1</div>
<div class="stat-label">等级 LEVEL</div>
</div>
</div>
</div>
<div style="margin-top:18px;">
<button class="btn ghost" id="pauseBtn">暂停 (P)</button>
<button class="btn ghost" id="resetBtn">重新开始</button>
</div>
<!-- 触屏按键(移动端显示) -->
<div class="touch-controls">
<button data-act="left">◀</button>
<button data-act="rotate">⟳</button>
<button data-act="right">▶</button>
<button data-act="drop">⤓</button>
<button data-act="hold">暂存</button>
<button data-act="down">▼</button>
<button data-act="pause">⏸</button>
<button data-act="reset">↺</button>
</div>
<p class="controls-hint">
<kbd>←</kbd><kbd>→</kbd> 移动 &nbsp; <kbd>↓</kbd> 软降 &nbsp; <kbd>↑</kbd>/<kbd>X</kbd> 旋转 &nbsp;
<kbd>空格</kbd> 硬降 &nbsp; <kbd>C</kbd> 暂存 &nbsp; <kbd>P</kbd> 暂停
</p>
</div>
<script>
(() => {
'use strict';
// ---------- 常量 ----------
const COLS = 10, ROWS = 20, BLOCK = 30;
const boardCanvas = document.getElementById('board');
const bctx = boardCanvas.getContext('2d');
const nextCanvas = document.getElementById('nextCanvas');
const nctx = nextCanvas.getContext('2d');
const holdCanvas = document.getElementById('holdCanvas');
const hctx = holdCanvas.getContext('2d');
// 7 种方块:I J L O S T Z(形状矩阵 + 颜色)
const SHAPES = [
[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I
[[1,0,0],[1,1,1],[0,0,0]], // J
[[0,0,1],[1,1,1],[0,0,0]], // L
[[1,1],[1,1]], // O
[[0,1,1],[1,1,0],[0,0,0]], // S
[[0,1,0],[1,1,1],[0,0,0]], // T
[[1,1,0],[0,1,1],[0,0,0]] // Z
];
const COLORS = ['#00e5ff', '#2979ff', '#ff9100', '#ffd600', '#00e676', '#b388ff', '#ff4081'];
const LINE_SCORES = [0, 100, 300, 500, 800]; // 0~4 行基础分
// ---------- 状态 ----------
let grid, piece, nextQueue, holdType, canHold;
let score, lines, level, highScore;
let dropCounter, dropInterval, lastTime;
let running = false, paused = false, gameOver = false;
let rafId = null;
highScore = parseInt(localStorage.getItem('tetris_best') || '0', 10);
document.getElementById('highScore').textContent = highScore;
// ---------- 工具 ----------
function rotate(matrix) {
const n = matrix.length;
const res = Array.from({length: n}, () => Array(n).fill(0));
for (let y = 0; y < n; y++)
for (let x = 0; x < n; x++)
res[x][n - 1 - y] = matrix[y][x];
return res;
}
// 7-bag 随机发牌,保证每种方块均匀出现
let bag = [];
function nextType() {
if (bag.length === 0) {
bag = [0,1,2,3,4,5,6];
for (let i = bag.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[bag[i], bag[j]] = [bag[j], bag[i]];
}
}
return bag.pop();
}
function spawn(type) {
const shape = SHAPES[type].map(row => row.slice());
return {
type,
shape,
color: COLORS[type],
x: Math.floor((COLS - shape[0].length) / 2),
y: 0
};
}
function collides(shape, ox, oy) {
for (let y = 0; y < shape.length; y++) {
for (let x = 0; x < shape[y].length; x++) {
if (!shape[y][x]) continue;
const nx = ox + x, ny = oy + y;
if (nx < 0 || nx >= COLS || ny >= ROWS) return true;
if (ny >= 0 && grid[ny][nx] !== 0) return true;
}
}
return false;
}
function merge() {
piece.shape.forEach((row, y) => row.forEach((v, x) => {
if (v && piece.y + y >= 0) grid[piece.y + y][piece.x + x] = piece.type + 1;
}));
}
function clearLines() {
let cleared = 0;
outer: for (let y = ROWS - 1; y >= 0; y--) {
for (let x = 0; x < COLS; x++) if (!grid[y][x]) continue outer;
grid.splice(y, 1);
grid.unshift(Array(COLS).fill(0));
cleared++;
y++; // 同一行再检查一次
}
if (cleared > 0) {
score += LINE_SCORES[cleared] * level;
lines += cleared;
const newLevel = Math.floor(lines / 10) + 1;
if (newLevel !== level) {
level = newLevel;
dropInterval = Math.max(60, 1000 * Math.pow(0.8, level - 1));
}
updateStats();
}
}
// ---------- 操作 ----------
function move(dir) {
if (!canPlay()) return;
if (!collides(piece.shape, piece.x + dir, piece.y)) piece.x += dir;
}
function softDrop() {
if (!canPlay()) return;
if (!collides(piece.shape, piece.x, piece.y + 1)) {
piece.y++;
score += 1;
updateStats();
} else {
lockPiece();
}
dropCounter = 0;
}
function hardDrop() {
if (!canPlay()) return;
let d = 0;
while (!collides(piece.shape, piece.x, piece.y + 1)) { piece.y++; d++; }
score += d * 2;
lockPiece();
}
function rotatePiece() {
if (!canPlay()) return;
const rotated = rotate(piece.shape);
// 简单踢墙:依次尝试原地、左1、右1、左2、右2、上1
const kicks = [0, -1, 1, -2, 2];
for (const k of kicks) {
if (!collides(rotated, piece.x + k, piece.y)) {
piece.shape = rotated;
piece.x += k;
return;
}
}
if (!collides(rotated, piece.x, piece.y - 1)) {
piece.shape = rotated;
piece.y -= 1;
}
}
function holdPiece() {
if (!canPlay() || !canHold) return;
if (holdType === null) {
holdType = piece.type;
piece = spawn(nextQueue.shift());
nextQueue.push(nextType());
} else {
[holdType, piece] = [piece.type, spawn(holdType)];
}
canHold = false;
drawHold();
drawNext();
if (collides(piece.shape, piece.x, piece.y)) endGame();
}
function lockPiece() {
merge();
clearLines();
piece = spawn(nextQueue.shift());
nextQueue.push(nextType());
canHold = true;
drawNext();
if (collides(piece.shape, piece.x, piece.y)) endGame();
}
function ghostY() {
let gy = piece.y;
while (!collides(piece.shape, piece.x, gy + 1)) gy++;
return gy;
}
function canPlay() { return running && !paused && !gameOver; }
// ---------- 绘制 ----------
function drawBlock(ctx, x, y, size, color, ghost = false) {
if (ghost) {
ctx.strokeStyle = color;
ctx.globalAlpha = 0.35;
ctx.lineWidth = 2;
ctx.strokeRect(x + 1, y + 1, size - 2, size - 2);
ctx.globalAlpha = 1;
return;
}
const g = ctx.createLinearGradient(x, y, x, y + size);
g.addColorStop(0, lighten(color, 30));
g.addColorStop(1, color);
ctx.fillStyle = g;
ctx.fillRect(x + 1, y + 1, size - 2, size - 2);
ctx.fillStyle = 'rgba(255,255,255,.25)';
ctx.fillRect(x + 3, y + 3, size - 6, 4);
}
function lighten(hex, amt) {
const n = parseInt(hex.slice(1), 16);
const r = Math.min(255, (n >> 16) + amt);
const g = Math.min(255, ((n >> 8) & 255) + amt);
const b = Math.min(255, (n & 255) + amt);
return `rgb(${r},${g},${b})`;
}
function draw() {
bctx.clearRect(0, 0, boardCanvas.width, boardCanvas.height);
// 网格线
bctx.strokeStyle = 'rgba(255,255,255,.05)';
bctx.lineWidth = 1;
for (let x = 1; x < COLS; x++) {
bctx.beginPath(); bctx.moveTo(x * BLOCK, 0); bctx.lineTo(x * BLOCK, ROWS * BLOCK); bctx.stroke();
}
for (let y = 1; y < ROWS; y++) {
bctx.beginPath(); bctx.moveTo(0, y * BLOCK); bctx.lineTo(COLS * BLOCK, y * BLOCK); bctx.stroke();
}
// 已固定方块
for (let y = 0; y < ROWS; y++)
for (let x = 0; x < COLS; x++)
if (grid[y][x]) drawBlock(bctx, x * BLOCK, y * BLOCK, BLOCK, COLORS[grid[y][x] - 1]);
if (piece && !gameOver) {
// 落点虚影
const gy = ghostY();
piece.shape.forEach((row, y) => row.forEach((v, x) => {
if (v && gy + y >= 0) drawBlock(bctx, (piece.x + x) * BLOCK, (gy + y) * BLOCK, BLOCK, piece.color, true);
}));
// 当前方块
piece.shape.forEach((row, y) => row.forEach((v, x) => {
if (v && piece.y + y >= 0) drawBlock(bctx, (piece.x + x) * BLOCK, (piece.y + y) * BLOCK, BLOCK, piece.color);
}));
}
}
function drawPreviewShape(ctx, canvas, type, offsetY, cellSize) {
const shape = SHAPES[type];
const color = COLORS[type];
let minX = 4, maxX = -1, minY = 4, maxY = -1;
shape.forEach((row, y) => row.forEach((v, x) => {
if (v) { minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y); }
}));
const w = (maxX - minX + 1) * cellSize, h = (maxY - minY + 1) * cellSize;
const ox = (canvas.width - w) / 2, oy = offsetY + (72 - h) / 2;
shape.forEach((row, y) => row.forEach((v, x) => {
if (v) drawBlock(ctx, ox + (x - minX) * cellSize, oy + (y - minY) * cellSize, cellSize, color);
}));
}
function drawNext() {
nctx.clearRect(0, 0, nextCanvas.width, nextCanvas.height);
nextQueue.slice(0, 3).forEach((t, i) => drawPreviewShape(nctx, nextCanvas, t, i * 72, 18));
}
function drawHold() {
hctx.clearRect(0, 0, holdCanvas.width, holdCanvas.height);
if (holdType !== null) {
hctx.globalAlpha = canHold ? 1 : 0.35;
drawPreviewShape(hctx, holdCanvas, holdType, 0, 18);
hctx.globalAlpha = 1;
}
}
function updateStats() {
document.getElementById('score').textContent = score;
document.getElementById('lines').textContent = lines;
document.getElementById('level').textContent = level;
}
// ---------- 主循环 ----------
function loop(time = 0) {
rafId = requestAnimationFrame(loop);
if (!running || paused || gameOver) { draw(); return; }
const delta = time - lastTime;
lastTime = time;
dropCounter += delta;
if (dropCounter > dropInterval) {
if (!collides(piece.shape, piece.x, piece.y + 1)) {
piece.y++;
} else {
lockPiece();
}
dropCounter = 0;
}
draw();
}
// ---------- 流程控制 ----------
function resetGame() {
grid = Array.from({length: ROWS}, () => Array(COLS).fill(0));
bag = [];
nextQueue = [nextType(), nextType(), nextType()];
piece = spawn(nextQueue.shift());
nextQueue.push(nextType());
holdType = null;
canHold = true;
score = 0; lines = 0; level = 1;
dropCounter = 0; dropInterval = 1000; lastTime = 0;
gameOver = false; paused = false;
updateStats();
drawNext();
drawHold();
}
function startGame() {
resetGame();
running = true;
hideOverlays();
if (!rafId) loop();
}
function endGame() {
gameOver = true;
running = false;
let record = false;
if (score > highScore) {
highScore = score;
localStorage.setItem('tetris_best', String(highScore));
document.getElementById('highScore').textContent = highScore;
record = true;
}
document.getElementById('finalScore').textContent = '本局得分:' + score;
document.getElementById('newRecord').style.display = record ? 'block' : 'none';
show('overOverlay');
draw();
}
function togglePause() {
if (!running || gameOver) return;
paused = !paused;
lastTime = performance.now();
document.getElementById('pauseBtn').textContent = paused ? '继续 (P)' : '暂停 (P)';
paused ? show('pauseOverlay') : hide('pauseOverlay');
}
function show(id) { document.getElementById(id).classList.remove('hidden'); }
function hide(id) { document.getElementById(id).classList.add('hidden'); }
function hideOverlays() { ['startOverlay', 'pauseOverlay', 'overOverlay'].forEach(hide); }
// ---------- 输入 ----------
document.addEventListener('keydown', e => {
if (e.repeat && e.code === 'Space') return;
switch (e.code) {
case 'ArrowLeft': e.preventDefault(); move(-1); break;
case 'ArrowRight': e.preventDefault(); move(1); break;
case 'ArrowDown': e.preventDefault(); softDrop(); break;
case 'ArrowUp':
case 'KeyX': e.preventDefault(); rotatePiece(); break;
case 'Space': e.preventDefault(); hardDrop(); break;
case 'KeyC': holdPiece(); break;
case 'KeyP': togglePause(); break;
case 'Enter':
if (!running || gameOver) startGame();
break;
}
});
// 触屏按钮(支持按住连发)
document.querySelectorAll('.touch-controls button').forEach(btn => {
const act = btn.dataset.act;
let timer = null;
const fire = () => {
if (act === 'left') move(-1);
else if (act === 'right') move(1);
else if (act === 'down') softDrop();
else if (act === 'rotate') rotatePiece();
else if (act === 'drop') hardDrop();
else if (act === 'hold') holdPiece();
else if (act === 'pause') togglePause();
else if (act === 'reset') startGame();
};
btn.addEventListener('pointerdown', e => {
e.preventDefault();
fire();
if (['left', 'right', 'down'].includes(act)) {
timer = setInterval(fire, act === 'down' ? 60 : 110);
}
});
['pointerup', 'pointerleave', 'pointercancel'].forEach(ev =>
btn.addEventListener(ev, () => { if (timer) { clearInterval(timer); timer = null; } })
);
});
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('restartBtn').addEventListener('click', startGame);
document.getElementById('resetBtn').addEventListener('click', startGame);
document.getElementById('resumeBtn').addEventListener('click', togglePause);
document.getElementById('pauseBtn').addEventListener('click', togglePause);
// 初始画面
resetGame();
running = false;
loop();
})();
</script>
</body>
</html>
-556
View File
@@ -1,556 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
<title>天空冲刺 · 手机躲避游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
background: linear-gradient(180deg, #0b1a3a 0%, #1b3b5c 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
padding: 10px;
margin: 0;
}
.game-container {
width: 100%;
max-width: 400px;
background: radial-gradient(circle at 50% 30%, #35637c, #0e2a3f);
border-radius: 48px;
box-shadow: 0 25px 50px rgba(0,0,0,0.6), inset 0 0 20px rgba(255,255,255,0.2);
padding: 16px 12px 20px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
canvas {
display: block;
width: 100%;
height: auto;
border-radius: 32px;
background: radial-gradient(ellipse at 50% 20%, #7fc9ff 0%, #3a6d9c 70%);
box-shadow: inset 0 -8px 20px rgba(0,20,40,0.7), 0 12px 25px rgba(0,0,0,0.5);
touch-action: none; /* 禁止默认手势,优化触摸响应 */
}
.info-panel {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
padding: 0 10px;
color: white;
font-weight: bold;
text-shadow: 0 2px 8px rgba(0,0,0,0.6);
font-size: 1.3rem;
}
.score-board {
background: rgba(10, 25, 45, 0.7);
backdrop-filter: blur(12px);
border-radius: 50px;
padding: 8px 22px;
letter-spacing: 1px;
box-shadow: 0 8px 18px rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.25);
}
.restart-btn {
background: #ffb84d;
border: none;
color: #0a1f32;
font-weight: bold;
font-size: 1.2rem;
padding: 10px 25px;
border-radius: 40px;
background: linear-gradient(180deg, #ffd966 0%, #f0a500 100%);
box-shadow: 0 8px 0 #b76e00, 0 12px 22px rgba(0,0,0,0.4);
transition: all 0.08s ease;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
border: 1px solid rgba(255,255,200,0.7);
touch-action: manipulation;
}
.restart-btn:active {
transform: translateY(4px);
box-shadow: 0 4px 0 #b76e00, 0 8px 14px rgba(0,0,0,0.5);
background: linear-gradient(180deg, #f5c45e 0%, #d18d00 100%);
}
.hint {
color: rgba(255,255,255,0.8);
font-size: 0.8rem;
margin-top: 6px;
text-align: center;
font-weight: 500;
text-shadow: 0 2px 5px black;
}
@media (max-width: 400px) {
.game-container {
padding: 12px 8px 16px;
}
.info-panel {
font-size: 1.1rem;
}
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="360" height="600" aria-label="天空冲刺游戏画面"></canvas>
<div class="info-panel">
<div class="score-board" id="scoreDisplay">0</div>
<button class="restart-btn" id="restartButton">↺ 起飞</button>
</div>
<div class="hint">👆 手指滑动或拖动飞机 | 躲避石块</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
// 固定尺寸,适配高清显示
const CW = 360;
const CH = 600;
canvas.width = CW;
canvas.height = CH;
// ---------- 游戏状态 ----------
let gameActive = true;
let score = 0;
// 玩家飞机属性
const PLAYER_WIDTH = 38;
const PLAYER_HEIGHT = 44;
let playerX = CW / 2 - PLAYER_WIDTH/2;
const playerY = CH - 90; // 垂直位置固定(玩家只水平移动)
// 障碍物数组
let obstacles = [];
// 障碍物设置
const OBSTACLE_WIDTH = 36;
const OBSTACLE_HEIGHT = 48;
const OBSTACLE_SPEED = 4.2; // 每帧下落速度
const SPAWN_FRAME_INTERVAL = 38; // 大约每38帧生成一个
let frameCounter = 0;
// 移动控制 (触摸/鼠标)
let targetPlayerX = playerX; // 用于平滑跟随或直接设置
// 触摸状态
let activeTouchId = null; // 追踪当前触摸点
// ---------- 辅助函数:限制玩家位置 ----------
function clampPlayerX(value) {
return Math.max(5, Math.min(CW - PLAYER_WIDTH - 5, value));
}
// ---------- 生成新障碍物 ----------
function spawnObstacle() {
// 随机x位置,确保不超出边界
const maxX = CW - OBSTACLE_WIDTH;
const x = Math.random() * maxX;
obstacles.push({
x: x,
y: -OBSTACLE_HEIGHT - Math.random() * 20, // 从顶外出现
width: OBSTACLE_WIDTH,
height: OBSTACLE_HEIGHT,
});
}
// ---------- 重置游戏 ----------
function restartGame() {
gameActive = true;
score = 0;
obstacles = [];
frameCounter = 0;
playerX = CW / 2 - PLAYER_WIDTH/2;
targetPlayerX = playerX;
activeTouchId = null;
updateScoreDisplay();
}
// ---------- 游戏结束 ----------
function endGame() {
if (!gameActive) return; // 防止重复触发
gameActive = false;
// 可以添加震动反馈(如果支持)
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(30);
}
}
// ---------- 更新分数显示 ----------
function updateScoreDisplay() {
scoreSpan.textContent = score;
}
// ---------- 碰撞检测 (矩形重叠) ----------
function checkCollision(playerRect, obs) {
return (playerRect.x < obs.x + obs.width &&
playerRect.x + playerRect.width > obs.x &&
playerRect.y < obs.y + obs.height &&
playerRect.y + playerRect.height > obs.y);
}
// ---------- 更新逻辑 ----------
function update() {
if (!gameActive) return;
// 1. 玩家位置平滑移动到目标位置 (使操作更跟手)
const DX = targetPlayerX - playerX;
// 快速跟随,但保留手感
if (Math.abs(DX) > 0.3) {
playerX += DX * 0.35;
// 避免微小抖动
if (Math.abs(DX) < 0.5) {
playerX = targetPlayerX;
}
} else {
playerX = targetPlayerX;
}
playerX = clampPlayerX(playerX);
// 同步目标位置以防超出边界后不一致
targetPlayerX = clampPlayerX(targetPlayerX);
// 2. 移动障碍物 & 碰撞检测 & 移除屏幕外
const playerRect = {
x: playerX,
y: playerY,
width: PLAYER_WIDTH,
height: PLAYER_HEIGHT,
};
for (let i = obstacles.length - 1; i >= 0; i--) {
const obs = obstacles[i];
obs.y += OBSTACLE_SPEED;
// 碰撞检测 (仅当游戏活跃时)
if (gameActive && checkCollision(playerRect, obs)) {
endGame();
return; // 立即停止更新,避免继续处理
}
// 移除超出底部的障碍物,并增加分数
if (obs.y > CH + 50) {
obstacles.splice(i, 1);
if (gameActive) {
score += 1;
updateScoreDisplay();
}
}
}
// 3. 生成新障碍物 (游戏活跃时才生成)
if (gameActive) {
frameCounter++;
if (frameCounter >= SPAWN_FRAME_INTERVAL) {
spawnObstacle();
frameCounter = 0;
// 动态略微调整生成间隔,增加随机性
// 保持在30~45之间变化
// 但为了稳定,使用固定基准加小波动
const variation = Math.floor(Math.random() * 12) - 4; // -4~7
frameCounter = -variation; // 技巧:让下次间隔变化
}
}
}
// ---------- 绘制图形 ----------
function draw() {
ctx.clearRect(0, 0, CW, CH);
// ------- 绘制装饰云朵 (背景) -------
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
drawCloud(40, 120, 30);
drawCloud(280, 200, 35);
drawCloud(70, 320, 28);
drawCloud(300, 430, 32);
drawCloud(150, 70, 38);
// ------- 绘制障碍物 (石块/陨石) -------
for (let obs of obstacles) {
// 石块渐变
const gradient = ctx.createLinearGradient(obs.x, obs.y, obs.x + 10, obs.y + obs.height);
gradient.addColorStop(0, '#5d4a3a');
gradient.addColorStop(0.5, '#4a3728');
gradient.addColorStop(1, '#2f231a');
ctx.fillStyle = gradient;
ctx.shadowColor = 'rgba(0,0,0,0.6)';
ctx.shadowBlur = 8;
ctx.beginPath();
// 不规则石块效果
const w = obs.width;
const h = obs.height;
const x = obs.x;
const y = obs.y;
ctx.moveTo(x + 5, y);
ctx.lineTo(x + w - 6, y + 4);
ctx.lineTo(x + w, y + h/2);
ctx.lineTo(x + w - 5, y + h - 5);
ctx.lineTo(x + w/2, y + h);
ctx.lineTo(x + 3, y + h - 7);
ctx.lineTo(x, y + h/3);
ctx.closePath();
ctx.fill();
// 高光线
ctx.shadowBlur = 4;
ctx.fillStyle = '#7d6b5c';
ctx.beginPath();
ctx.moveTo(x + 8, y + 5);
ctx.lineTo(x + w - 12, y + 8);
ctx.lineTo(x + w - 9, y + h/2 - 2);
ctx.lineTo(x + 6, y + h/2 - 3);
ctx.closePath();
ctx.fill();
// 重置阴影
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
// 裂纹
ctx.strokeStyle = '#1a120a';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(x + 10, y + 12);
ctx.lineTo(x + 18, y + 25);
ctx.lineTo(x + 13, y + 34);
ctx.stroke();
}
// ------- 绘制玩家飞机 -------
ctx.shadowColor = '#00000080';
ctx.shadowBlur = 12;
const px = playerX;
const py = playerY;
// 机身主色
const planeGrad = ctx.createLinearGradient(px, py, px + PLAYER_WIDTH*0.8, py + PLAYER_HEIGHT);
planeGrad.addColorStop(0, '#ffd966');
planeGrad.addColorStop(0.6, '#f0a500');
planeGrad.addColorStop(1, '#c47a00');
ctx.fillStyle = planeGrad;
// 绘制飞机主体 (喷气式风格)
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2, py - 4); // 机头
ctx.lineTo(px + PLAYER_WIDTH-8, py + PLAYER_HEIGHT-12); // 右尾翼
ctx.lineTo(px + PLAYER_WIDTH-4, py + PLAYER_HEIGHT-6);
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT-10); // 尾部中心
ctx.lineTo(px + 4, py + PLAYER_HEIGHT-6);
ctx.lineTo(px + 8, py + PLAYER_HEIGHT-12); // 左尾翼
ctx.closePath();
ctx.fill();
// 机翼
ctx.fillStyle = '#e59400';
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2, py + 12);
ctx.lineTo(px - 3, py + 28);
ctx.lineTo(px + 10, py + 32);
ctx.lineTo(px + PLAYER_WIDTH/2, py + 24);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2, py + 12);
ctx.lineTo(px + PLAYER_WIDTH+3, py + 28);
ctx.lineTo(px + PLAYER_WIDTH-10, py + 32);
ctx.lineTo(px + PLAYER_WIDTH/2, py + 24);
ctx.closePath();
ctx.fill();
// 驾驶舱
ctx.fillStyle = '#aee2ff';
ctx.shadowColor = '#ffffff80';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.ellipse(px + PLAYER_WIDTH/2, py + 10, 7, 9, 0, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.ellipse(px + PLAYER_WIDTH/2-2, py + 8, 2.5, 3.5, 0, 0, Math.PI*2);
ctx.fill();
// 引擎火焰
ctx.shadowBlur = 14;
ctx.fillStyle = '#ff7b24';
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2-5, py + PLAYER_HEIGHT-8);
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT+6);
ctx.lineTo(px + PLAYER_WIDTH/2+5, py + PLAYER_HEIGHT-8);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ffdd55';
ctx.beginPath();
ctx.moveTo(px + PLAYER_WIDTH/2-3, py + PLAYER_HEIGHT-6);
ctx.lineTo(px + PLAYER_WIDTH/2, py + PLAYER_HEIGHT);
ctx.lineTo(px + PLAYER_WIDTH/2+3, py + PLAYER_HEIGHT-6);
ctx.closePath();
ctx.fill();
// 重置阴影
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
// ------- 游戏结束画面 -------
if (!gameActive) {
ctx.globalAlpha = 0.8;
ctx.fillStyle = '#0a1424';
ctx.fillRect(0, 0, CW, CH);
ctx.globalAlpha = 1;
ctx.fillStyle = 'white';
ctx.font = 'bold 28px "Segoe UI", system-ui';
ctx.textAlign = 'center';
ctx.shadowColor = 'black';
ctx.shadowBlur = 16;
ctx.fillText('💥 坠毁', CW/2, CH/2-20);
ctx.font = '18px system-ui';
ctx.fillText('点击"起飞"继续', CW/2, CH/2+45);
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
}
}
function drawCloud(x, y, size) {
ctx.fillStyle = 'rgba(255, 255, 250, 0.25)';
ctx.beginPath();
ctx.arc(x, y, size*0.6, 0, Math.PI*2);
ctx.arc(x+size*0.5, y-size*0.2, size*0.7, 0, Math.PI*2);
ctx.arc(x-size*0.4, y-size*0.1, size*0.55, 0, Math.PI*2);
ctx.arc(x+size*0.2, y+size*0.2, size*0.5, 0, Math.PI*2);
ctx.fill();
}
// ---------- 游戏循环 ----------
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ---------- 事件处理 (触摸/鼠标适配) ----------
function handleStart(e) {
e.preventDefault();
if (!gameActive) return; // 游戏结束时不处理移动
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const canvasRect = canvas.getBoundingClientRect();
// 计算相对于canvas内部的x坐标 (按比例缩放)
const scaleX = CW / canvasRect.width; // canvas物理像素与CSS宽度比例
const relativeX = (clientX - canvasRect.left) * scaleX;
// 设定目标位置:飞机中心对准触摸点
let newTarget = relativeX - PLAYER_WIDTH/2;
targetPlayerX = clampPlayerX(newTarget);
playerX = targetPlayerX; // 立即吸附,提升响应
// 记录触摸点id用于移动跟踪
if (e.touches) {
activeTouchId = e.touches[0].identifier;
}
}
function handleMove(e) {
e.preventDefault();
if (!gameActive) return;
// 对于触摸事件,需要找到匹配的触摸点
if (e.touches) {
if (activeTouchId === null) return;
let currentTouch = null;
for (let i = 0; i < e.touches.length; i++) {
if (e.touches[i].identifier === activeTouchId) {
currentTouch = e.touches[i];
break;
}
}
if (!currentTouch) return;
const canvasRect = canvas.getBoundingClientRect();
const scaleX = CW / canvasRect.width;
const relativeX = (currentTouch.clientX - canvasRect.left) * scaleX;
let newTarget = relativeX - PLAYER_WIDTH/2;
targetPlayerX = clampPlayerX(newTarget);
} else {
// 鼠标移动 (用于桌面调试)
const canvasRect = canvas.getBoundingClientRect();
const scaleX = CW / canvasRect.width;
const relativeX = (e.clientX - canvasRect.left) * scaleX;
let newTarget = relativeX - PLAYER_WIDTH/2;
targetPlayerX = clampPlayerX(newTarget);
}
}
function handleEnd(e) {
e.preventDefault();
// 触摸结束,清除活动触摸点
if (e.touches) {
// 如果剩余触摸点为空,则重置activeTouchId
if (e.touches.length === 0) {
activeTouchId = null;
} else {
// 仍有其他手指,尝试切换到第一个
activeTouchId = e.touches[0].identifier;
}
} else {
// 鼠标松开不做特殊处理
activeTouchId = null;
}
}
// 绑定事件
function bindEvents() {
// 触摸事件
canvas.addEventListener('touchstart', handleStart, { passive: false });
canvas.addEventListener('touchmove', handleMove, { passive: false });
canvas.addEventListener('touchend', handleEnd);
canvas.addEventListener('touchcancel', handleEnd);
// 鼠标事件 (便于开发调试和部分设备)
canvas.addEventListener('mousedown', handleStart);
window.addEventListener('mousemove', (e) => {
// 只有按下鼠标左键时才处理移动
if (e.buttons === 1 && gameActive) {
e.preventDefault();
const canvasRect = canvas.getBoundingClientRect();
const scaleX = CW / canvasRect.width;
const relativeX = (e.clientX - canvasRect.left) * scaleX;
let newTarget = relativeX - PLAYER_WIDTH/2;
targetPlayerX = clampPlayerX(newTarget);
}
});
window.addEventListener('mouseup', (e) => {
// 不做特殊操作
});
// 重启按钮
document.getElementById('restartButton').addEventListener('click', (e) => {
restartGame();
});
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
e.preventDefault();
restartGame();
});
}
// 启动一切
bindEvents();
restartGame(); // 初始状态
gameLoop();
})();
</script>
</body>
</html>
-479
View File
@@ -1,479 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
<title>深渊矿工 · 收集宝石游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
background: linear-gradient(180deg, #1a0e08 0%, #3b2a1a 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, sans-serif;
padding: 10px;
margin: 0;
}
.game-container {
width: 100%;
max-width: 400px;
background: radial-gradient(circle at 50% 40%, #5e3e1a, #2d1c0b);
border-radius: 48px;
box-shadow: 0 25px 45px rgba(0,0,0,0.7), inset 0 0 20px rgba(255,215,0,0.2);
padding: 16px 12px 20px;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #7a5c3a;
}
canvas {
display: block;
width: 100%;
height: auto;
border-radius: 32px;
background: radial-gradient(ellipse at 50% 10%, #4a3620 0%, #1e1308 85%);
box-shadow: inset 0 -12px 25px rgba(0,0,0,0.8), 0 10px 20px black;
touch-action: none;
}
.info-panel {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
padding: 0 10px;
color: #ffd78b;
font-weight: bold;
text-shadow: 0 2px 6px black;
font-size: 1.3rem;
}
.score-board {
background: rgba(0,0,0,0.6);
backdrop-filter: blur(10px);
border-radius: 50px;
padding: 8px 22px;
letter-spacing: 1px;
box-shadow: 0 6px 14px rgba(0,0,0,0.7);
border: 1px solid #c9a23b;
color: #ffd966;
}
.restart-btn {
background: #e6b422;
border: none;
color: #2b1a04;
font-weight: bold;
font-size: 1.2rem;
padding: 10px 24px;
border-radius: 40px;
background: linear-gradient(180deg, #f9e076 0%, #ca9620 100%);
box-shadow: 0 7px 0 #7a5200, 0 10px 20px black;
transition: all 0.08s ease;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
border: 1px solid #ffe28c;
touch-action: manipulation;
}
.restart-btn:active {
transform: translateY(4px);
box-shadow: 0 3px 0 #7a5200, 0 7px 12px black;
background: linear-gradient(180deg, #ebc85c 0%, #ad7d18 100%);
}
.hint {
color: #dbbc6c;
font-size: 0.8rem;
margin-top: 6px;
text-align: center;
font-weight: 500;
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="360" height="600" aria-label="深渊矿工游戏画面"></canvas>
<div class="info-panel">
<div class="score-board" id="scoreDisplay">💎 0</div>
<button class="restart-btn" id="restartButton">⛏️ 新矿洞</button>
</div>
<div class="hint">👆 左右滑动矿车 | 接宝石 ✦ 躲岩石</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
const CW = 360;
const CH = 600;
canvas.width = CW;
canvas.height = CH;
// ----- 游戏状态 -----
let gameActive = true;
let score = 0;
// 矿车属性
const CART_WIDTH = 56;
const CART_HEIGHT = 40;
let cartX = CW / 2 - CART_WIDTH/2;
const cartY = CH - 85; // 固定垂直位置
// 下落物品数组
let items = [];
// 物品类型
const TYPE_GEM = 'gem'; // 宝石 (+1分)
const TYPE_ROCK = 'rock'; // 岩石 (碰撞结束)
const ITEM_SIZE = 34; // 统一尺寸
const FALL_SPEED = 3.8;
const SPAWN_RATE = 32; // 帧间隔
let frameCount = 0;
// 触摸控制
let targetCartX = cartX;
let activeTouchId = null;
// ----- 辅助函数 -----
function clampCart(value) {
return Math.max(4, Math.min(CW - CART_WIDTH - 4, value));
}
// 生成物品 (宝石或岩石)
function spawnItem() {
const maxX = CW - ITEM_SIZE;
const x = Math.random() * maxX;
// 70%概率宝石,30%岩石
const type = Math.random() < 0.7 ? TYPE_GEM : TYPE_ROCK;
items.push({
x: x,
y: -ITEM_SIZE - Math.random() * 15,
size: ITEM_SIZE,
type: type,
});
}
// 重置游戏
function restartGame() {
gameActive = true;
score = 0;
items = [];
frameCount = 0;
cartX = CW / 2 - CART_WIDTH/2;
targetCartX = cartX;
activeTouchId = null;
updateScoreDisplay();
}
function endGame() {
if (!gameActive) return;
gameActive = false;
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(40);
}
}
function updateScoreDisplay() {
scoreSpan.textContent = `💎 ${score}`;
}
// 碰撞检测
function rectCollide(r1, r2) {
return !(r2.x > r1.x + r1.w || r2.x + r2.w < r1.x ||
r2.y > r1.y + r1.h || r2.y + r2.h < r1.y);
}
// ----- 更新逻辑 -----
function update() {
if (!gameActive) return;
// 矿车平滑移动
const diff = targetCartX - cartX;
if (Math.abs(diff) > 0.3) {
cartX += diff * 0.3;
if (Math.abs(diff) < 0.6) cartX = targetCartX;
} else {
cartX = targetCartX;
}
cartX = clampCart(cartX);
targetCartX = clampCart(targetCartX);
// 矿车碰撞矩形
const cartRect = {
x: cartX,
y: cartY,
w: CART_WIDTH,
h: CART_HEIGHT,
};
// 处理物品下落
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
item.y += FALL_SPEED;
const itemRect = {
x: item.x,
y: item.y,
w: item.size,
h: item.size,
};
// 碰撞检测
if (gameActive && rectCollide(cartRect, itemRect)) {
if (item.type === TYPE_GEM) {
// 收集宝石
score++;
updateScoreDisplay();
items.splice(i, 1);
continue;
} else if (item.type === TYPE_ROCK) {
// 撞到岩石,游戏结束
endGame();
return;
}
}
// 移除超出屏幕的物品
if (item.y > CH + 50) {
items.splice(i, 1);
}
}
// 生成新物品
if (gameActive) {
frameCount++;
if (frameCount >= SPAWN_RATE) {
spawnItem();
frameCount = 0;
// 动态微调
const variation = Math.floor(Math.random() * 10) - 3;
frameCount = -variation;
}
}
}
// ----- 绘制图形 -----
function draw() {
ctx.clearRect(0, 0, CW, CH);
// 绘制岩壁纹理
ctx.fillStyle = '#2f2416';
for (let i = 0; i < 8; i++) {
ctx.fillRect(0, i * 85 + (Date.now()*0.01 % 85), 8, 25);
ctx.fillRect(CW-8, i * 85 + 30 + (Date.now()*0.01 % 70), 8, 25);
}
// 绘制物品
for (let item of items) {
const { x, y, size, type } = item;
ctx.save();
ctx.shadowColor = '#00000080';
ctx.shadowBlur = 8;
if (type === TYPE_GEM) {
// 绘制宝石 (多面体)
const grad = ctx.createLinearGradient(x, y, x+size, y+size);
grad.addColorStop(0, '#b5f2ff');
grad.addColorStop(0.4, '#3cc7e0');
grad.addColorStop(1, '#0f5b6e');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(x + size/2, y);
ctx.lineTo(x + size, y + size*0.6);
ctx.lineTo(x + size*0.7, y + size);
ctx.lineTo(x + size*0.3, y + size);
ctx.lineTo(x, y + size*0.6);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#ffffffcc';
ctx.beginPath();
ctx.moveTo(x + size/2, y+2);
ctx.lineTo(x + size*0.8, y + size*0.55);
ctx.lineTo(x + size/2, y + size*0.7);
ctx.closePath();
ctx.fill();
} else {
// 绘制岩石
const grad = ctx.createLinearGradient(x, y, x+6, y+size);
grad.addColorStop(0, '#7d6b5a');
grad.addColorStop(0.7, '#3e3325');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(x+5, y);
ctx.lineTo(x+size-6, y+3);
ctx.lineTo(x+size, y+size/2);
ctx.lineTo(x+size-4, y+size-5);
ctx.lineTo(x+size/2, y+size);
ctx.lineTo(x+3, y+size-6);
ctx.lineTo(x, y+size/3);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#5c5040';
ctx.beginPath();
ctx.arc(x+size/2-2, y+size/2-2, 4, 0, Math.PI*2);
ctx.fill();
}
ctx.restore();
}
// 绘制矿车
ctx.save();
ctx.shadowColor = '#000000';
ctx.shadowBlur = 14;
const cx = cartX;
const cy = cartY;
// 矿车车身
const cartGrad = ctx.createLinearGradient(cx, cy, cx, cy+CART_HEIGHT);
cartGrad.addColorStop(0, '#b3823e');
cartGrad.addColorStop(0.7, '#613d1c');
ctx.fillStyle = cartGrad;
ctx.beginPath();
ctx.moveTo(cx+8, cy);
ctx.lineTo(cx+CART_WIDTH-8, cy);
ctx.lineTo(cx+CART_WIDTH-2, cy+CART_HEIGHT-8);
ctx.lineTo(cx+CART_WIDTH-2, cy+CART_HEIGHT-2);
ctx.lineTo(cx+2, cy+CART_HEIGHT-2);
ctx.lineTo(cx+2, cy+CART_HEIGHT-8);
ctx.closePath();
ctx.fill();
// 车轮
ctx.fillStyle = '#2b1a0a';
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(cx+10, cy+CART_HEIGHT-6, 8, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(cx+CART_WIDTH-10, cy+CART_HEIGHT-6, 8, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#5a5a5a';
ctx.beginPath();
ctx.arc(cx+10, cy+CART_HEIGHT-6, 4, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(cx+CART_WIDTH-10, cy+CART_HEIGHT-6, 4, 0, Math.PI*2);
ctx.fill();
// 矿石点缀
ctx.fillStyle = '#f9e076';
ctx.shadowBlur = 6;
ctx.beginPath();
ctx.arc(cx+15, cy+12, 5, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ffb703';
ctx.beginPath();
ctx.arc(cx+35, cy+18, 5, 0, Math.PI*2);
ctx.fill();
ctx.restore();
// 游戏结束画面
if (!gameActive) {
ctx.globalAlpha = 0.8;
ctx.fillStyle = '#0f0803';
ctx.fillRect(0, 0, CW, CH);
ctx.globalAlpha = 1;
ctx.fillStyle = '#f5d78c';
ctx.font = 'bold 28px "Segoe UI", system-ui';
ctx.textAlign = 'center';
ctx.shadowColor = 'black';
ctx.shadowBlur = 18;
ctx.fillText('⛔ 矿洞塌方', CW/2, CH/2-20);
ctx.font = '18px system-ui';
ctx.fillText('点击"新矿洞"继续', CW/2, CH/2+45);
ctx.shadowBlur = 0;
ctx.shadowColor = 'transparent';
}
}
// ----- 游戏循环 -----
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ----- 事件处理 -----
function handleStart(e) {
e.preventDefault();
if (!gameActive) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const relX = (clientX - rect.left) * scaleX;
targetCartX = clampCart(relX - CART_WIDTH/2);
cartX = targetCartX;
if (e.touches) activeTouchId = e.touches[0].identifier;
}
function handleMove(e) {
e.preventDefault();
if (!gameActive) return;
if (e.touches) {
if (activeTouchId === null) return;
let touch = null;
for (let i=0; i<e.touches.length; i++) {
if (e.touches[i].identifier === activeTouchId) { touch = e.touches[i]; break; }
}
if (!touch) return;
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const relX = (touch.clientX - rect.left) * scaleX;
targetCartX = clampCart(relX - CART_WIDTH/2);
} else {
if (e.buttons === 1) {
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const relX = (e.clientX - rect.left) * scaleX;
targetCartX = clampCart(relX - CART_WIDTH/2);
}
}
}
function handleEnd(e) {
e.preventDefault();
if (e.touches && e.touches.length === 0) {
activeTouchId = null;
} else if (e.touches) {
activeTouchId = e.touches[0].identifier;
}
}
function bindEvents() {
canvas.addEventListener('touchstart', handleStart, { passive: false });
canvas.addEventListener('touchmove', handleMove, { passive: false });
canvas.addEventListener('touchend', handleEnd);
canvas.addEventListener('touchcancel', handleEnd);
canvas.addEventListener('mousedown', handleStart);
window.addEventListener('mousemove', (e) => {
if (e.buttons === 1 && gameActive) {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const relX = (e.clientX - rect.left) * scaleX;
targetCartX = clampCart(relX - CART_WIDTH/2);
}
});
document.getElementById('restartButton').addEventListener('click', restartGame);
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
e.preventDefault();
restartGame();
});
}
bindEvents();
restartGame();
gameLoop();
})();
</script>
</body>
</html>
-562
View File
@@ -1,562 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
<title>泡泡射手 · 消除游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
background: linear-gradient(180deg, #1a2f4a 0%, #3c5f7c 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, sans-serif;
padding: 10px;
margin: 0;
}
.game-container {
width: 100%;
max-width: 400px;
background: radial-gradient(circle at 50% 30%, #5b8cbf, #1e3b4f);
border-radius: 48px;
box-shadow: 0 25px 50px rgba(0,0,0,0.6), inset 0 0 25px rgba(255,255,255,0.2);
padding: 16px 12px 20px;
display: flex;
flex-direction: column;
align-items: center;
}
canvas {
display: block;
width: 100%;
height: auto;
border-radius: 32px;
background: radial-gradient(ellipse at 50% 15%, #a3d0ff 0%, #32638a 80%);
box-shadow: inset 0 -10px 25px rgba(0,20,40,0.7), 0 12px 25px rgba(0,0,0,0.5);
touch-action: none;
}
.info-panel {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
padding: 0 10px;
color: white;
font-weight: bold;
text-shadow: 0 2px 8px rgba(0,0,0,0.6);
font-size: 1.3rem;
}
.score-board {
background: rgba(20, 40, 60, 0.7);
backdrop-filter: blur(12px);
border-radius: 50px;
padding: 8px 22px;
letter-spacing: 1px;
box-shadow: 0 8px 18px rgba(0,0,0,0.5);
border: 1px solid rgba(255,255,255,0.3);
}
.restart-btn {
background: #ff7b9c;
border: none;
color: white;
font-weight: bold;
font-size: 1.2rem;
padding: 10px 25px;
border-radius: 40px;
background: linear-gradient(180deg, #ffb3c6 0%, #e85d7a 100%);
box-shadow: 0 8px 0 #a8324e, 0 12px 22px rgba(0,0,0,0.4);
transition: all 0.08s ease;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
border: 1px solid #ffd1dc;
touch-action: manipulation;
}
.restart-btn:active {
transform: translateY(4px);
box-shadow: 0 4px 0 #a8324e, 0 8px 14px rgba(0,0,0,0.5);
background: linear-gradient(180deg, #f595aa 0%, #c94a62 100%);
}
.hint {
color: rgba(255,255,255,0.85);
font-size: 0.8rem;
margin-top: 6px;
text-align: center;
font-weight: 500;
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="360" height="600" aria-label="泡泡射手游戏画面"></canvas>
<div class="info-panel">
<div class="score-board" id="scoreDisplay">🫧 0</div>
<button class="restart-btn" id="restartButton">🔁 新一局</button>
</div>
<div class="hint">👆 点击屏幕发射泡泡 | 消除三个同色</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
const CW = 360;
const CH = 600;
canvas.width = CW;
canvas.height = CH;
// ----- 游戏配置 -----
const ROWS = 7; // 泡泡网格行数
const COLS = 8; // 列数
const BUBBLE_RADIUS = 18; // 泡泡半径
const CELL_SIZE = BUBBLE_RADIUS * 2 + 2; // 间距
const GRID_OFFSET_X = (CW - COLS * CELL_SIZE) / 2 + BUBBLE_RADIUS;
const GRID_OFFSET_Y = 65;
// 发射器位置
const SHOOTER_Y = CH - 50;
let shooterX = CW / 2;
// 颜色池
const COLORS = ['#FF6B6B', '#4ECDC4', '#FFE66D', '#A37EF0', '#FF8A5C'];
// 游戏状态
let gameActive = true;
let score = 0;
let grid = []; // 二维数组存储泡泡颜色(null为空)
let currentBubbleColor = null;
let nextBubbleColor = null;
let projectile = null; // 飞行中的泡泡 { x, y, vx, vy, color }
// 触摸追踪
let aiming = false;
let aimStartX = 0;
let aimStartY = 0;
// ----- 初始化网格 -----
function createGrid() {
grid = [];
for (let r = 0; r < ROWS; r++) {
grid[r] = [];
for (let c = 0; c < COLS; c++) {
// 偏移行 (交错效果)
const colOffset = (r % 2 === 1) ? 0 : 0;
// 只填充前几行
if (r < 4) {
const colorIndex = Math.floor(Math.random() * COLORS.length);
grid[r][c] = COLORS[colorIndex];
} else {
grid[r][c] = null;
}
}
}
}
// 随机颜色
function randomColor() {
return COLORS[Math.floor(Math.random() * COLORS.length)];
}
// 重置游戏
function restartGame() {
gameActive = true;
score = 0;
createGrid();
currentBubbleColor = randomColor();
nextBubbleColor = randomColor();
projectile = null;
shooterX = CW / 2;
updateScoreDisplay();
}
function endGame() {
if (!gameActive) return;
gameActive = false;
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(25);
}
}
function updateScoreDisplay() {
scoreSpan.textContent = `🫧 ${score}`;
}
// 根据行列计算泡泡中心坐标
function getBubblePos(row, col) {
const x = GRID_OFFSET_X + col * CELL_SIZE + (row % 2 === 1 ? CELL_SIZE/2 : 0);
const y = GRID_OFFSET_Y + row * CELL_SIZE * 0.88;
return { x, y };
}
// 查找相邻同色泡泡 (用于消除)
function findConnected(row, col, color, visited = new Set()) {
const key = `${row},${col}`;
if (row < 0 || row >= ROWS || col < 0 || col >= COLS || visited.has(key)) return [];
if (grid[row][col] !== color) return [];
visited.add(key);
const cluster = [{ row, col }];
// 六个方向 (考虑交错行)
const directionsEven = [
[-1, -1], [-1, 0], [0, -1], [0, 1], [1, -1], [1, 0]
];
const directionsOdd = [
[-1, 0], [-1, 1], [0, -1], [0, 1], [1, 0], [1, 1]
];
const dirs = row % 2 === 0 ? directionsEven : directionsOdd;
for (let [dr, dc] of dirs) {
const nr = row + dr;
const nc = col + dc;
cluster.push(...findConnected(nr, nc, color, visited));
}
return cluster;
}
// 移除泡泡并更新分数
function removeCluster(cluster) {
if (cluster.length < 3) return false;
for (let { row, col } of cluster) {
grid[row][col] = null;
}
score += cluster.length * 10;
updateScoreDisplay();
return true;
}
// 处理悬浮泡泡掉落 (没有连接到顶部的泡泡全部掉落)
function dropFloatingBubbles() {
const visited = new Set();
// 从顶行开始标记所有连接的泡泡
function markConnected(row, col) {
const key = `${row},${col}`;
if (row < 0 || row >= ROWS || col < 0 || col >= COLS || visited.has(key)) return;
if (grid[row][col] === null) return;
visited.add(key);
const dirsEven = [[-1,-1],[-1,0],[0,-1],[0,1],[1,-1],[1,0]];
const dirsOdd = [[-1,0],[-1,1],[0,-1],[0,1],[1,0],[1,1]];
const dirs = row % 2 === 0 ? dirsEven : dirsOdd;
for (let [dr, dc] of dirs) {
markConnected(row + dr, col + dc);
}
}
for (let c = 0; c < COLS; c++) {
if (grid[0][c] !== null) markConnected(0, c);
}
let dropped = 0;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c] !== null && !visited.has(`${r},${c}`)) {
grid[r][c] = null;
dropped++;
}
}
}
if (dropped > 0) {
score += dropped * 15;
updateScoreDisplay();
}
}
// 检查游戏是否结束 (泡泡到达底部警戒线)
function checkGameOver() {
for (let r = ROWS - 1; r >= 0; r--) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c] !== null) {
const pos = getBubblePos(r, c);
if (pos.y + BUBBLE_RADIUS > SHOOTER_Y - 15) {
endGame();
return true;
}
}
}
}
return false;
}
// 发射泡泡
function shootBubble(targetX, targetY) {
if (!gameActive || projectile) return;
if (!currentBubbleColor) return;
const dx = targetX - shooterX;
const dy = targetY - SHOOTER_Y;
const length = Math.sqrt(dx * dx + dy * dy);
if (length < 5) return; // 太近不发射
const speed = 9.5;
const vx = (dx / length) * speed;
const vy = (dy / length) * speed;
projectile = {
x: shooterX,
y: SHOOTER_Y,
vx: vx,
vy: vy,
color: currentBubbleColor,
radius: BUBBLE_RADIUS
};
currentBubbleColor = nextBubbleColor;
nextBubbleColor = randomColor();
}
// 更新弹丸位置与碰撞
function updateProjectile() {
if (!projectile || !gameActive) return;
projectile.x += projectile.vx;
projectile.y += projectile.vy;
// 边界反弹
if (projectile.x - BUBBLE_RADIUS < 0) {
projectile.x = BUBBLE_RADIUS;
projectile.vx *= -0.9;
}
if (projectile.x + BUBBLE_RADIUS > CW) {
projectile.x = CW - BUBBLE_RADIUS;
projectile.vx *= -0.9;
}
// 顶部碰撞
if (projectile.y - BUBBLE_RADIUS < GRID_OFFSET_Y - BUBBLE_RADIUS) {
placeProjectile();
return;
}
// 与网格泡泡碰撞检测
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c] === null) continue;
const pos = getBubblePos(r, c);
const dist = Math.hypot(projectile.x - pos.x, projectile.y - pos.y);
if (dist < BUBBLE_RADIUS * 1.9) {
placeProjectile();
return;
}
}
}
// 掉出底部 (失败情况,但不直接结束,只是移除弹丸)
if (projectile.y > CH + 50) {
projectile = null;
}
}
function placeProjectile() {
if (!projectile) return;
// 找到最近的网格位置
let bestRow = -1, bestCol = -1, bestDist = Infinity;
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c] !== null) continue;
const pos = getBubblePos(r, c);
const dist = Math.hypot(projectile.x - pos.x, projectile.y - pos.y);
if (dist < bestDist) {
bestDist = dist;
bestRow = r;
bestCol = c;
}
}
}
if (bestRow !== -1 && bestCol !== -1 && bestDist < CELL_SIZE * 1.2) {
grid[bestRow][bestCol] = projectile.color;
// 检查消除
const cluster = findConnected(bestRow, bestCol, projectile.color);
if (cluster.length >= 3) {
removeCluster(cluster);
dropFloatingBubbles();
}
checkGameOver();
}
projectile = null;
}
// ----- 绘制 -----
function draw() {
ctx.clearRect(0, 0, CW, CH);
// 绘制网格泡泡
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (grid[r][c] === null) continue;
const { x, y } = getBubblePos(r, c);
drawBubble(x, y, grid[r][c]);
}
}
// 绘制飞行弹丸
if (projectile) {
drawBubble(projectile.x, projectile.y, projectile.color);
}
// 绘制发射器
ctx.save();
ctx.shadowColor = '#00000060';
ctx.shadowBlur = 12;
// 发射台
ctx.fillStyle = '#445566';
ctx.beginPath();
ctx.arc(shooterX, SHOOTER_Y, 14, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#2c3e50';
ctx.beginPath();
ctx.arc(shooterX, SHOOTER_Y, 10, 0, Math.PI*2);
ctx.fill();
// 当前泡泡预览
if (currentBubbleColor) {
drawBubble(shooterX, SHOOTER_Y - 2, currentBubbleColor);
}
// 下一个泡泡预览
if (nextBubbleColor) {
ctx.fillStyle = '#ffffff30';
ctx.beginPath();
ctx.arc(shooterX + 38, SHOOTER_Y - 8, 14, 0, Math.PI*2);
ctx.fill();
drawBubble(shooterX + 38, SHOOTER_Y - 8, nextBubbleColor, 12);
}
ctx.restore();
// 引导线
if (aiming && gameActive && !projectile) {
ctx.beginPath();
ctx.strokeStyle = '#ffffff80';
ctx.lineWidth = 2;
ctx.setLineDash([6, 8]);
ctx.moveTo(shooterX, SHOOTER_Y);
ctx.lineTo(aimStartX, aimStartY);
ctx.stroke();
ctx.setLineDash([]);
}
// 游戏结束
if (!gameActive) {
ctx.globalAlpha = 0.75;
ctx.fillStyle = '#0a1a2a';
ctx.fillRect(0, 0, CW, CH);
ctx.globalAlpha = 1;
ctx.fillStyle = 'white';
ctx.font = 'bold 28px "Segoe UI"';
ctx.textAlign = 'center';
ctx.fillText('🫧 结束', CW/2, CH/2-20);
}
}
function drawBubble(x, y, color, radius = BUBBLE_RADIUS) {
ctx.save();
ctx.shadowColor = '#00000040';
ctx.shadowBlur = 8;
const grad = ctx.createRadialGradient(x-4, y-4, radius*0.1, x, y, radius);
grad.addColorStop(0, '#ffffff');
grad.addColorStop(0.4, color);
grad.addColorStop(1, '#00000040');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ffffff90';
ctx.beginPath();
ctx.arc(x-3, y-4, radius*0.3, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
// ----- 游戏循环 -----
function gameLoop() {
if (gameActive) {
updateProjectile();
}
draw();
requestAnimationFrame(gameLoop);
}
// ----- 事件处理 -----
function handleStart(e) {
e.preventDefault();
if (!gameActive) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const scaleY = CH / rect.height;
const canvasX = (clientX - rect.left) * scaleX;
const canvasY = (clientY - rect.top) * scaleY;
// 如果点击靠近发射器区域,开始瞄准
if (Math.abs(canvasY - SHOOTER_Y) < 60 && !projectile) {
aiming = true;
aimStartX = canvasX;
aimStartY = canvasY;
}
}
function handleMove(e) {
e.preventDefault();
if (!aiming || !gameActive) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const scaleY = CH / rect.height;
aimStartX = (clientX - rect.left) * scaleX;
aimStartY = (clientY - rect.top) * scaleY;
}
function handleEnd(e) {
e.preventDefault();
if (aiming && gameActive && !projectile) {
shootBubble(aimStartX, aimStartY);
}
aiming = false;
}
function bindEvents() {
canvas.addEventListener('touchstart', handleStart, { passive: false });
canvas.addEventListener('touchmove', handleMove, { passive: false });
canvas.addEventListener('touchend', handleEnd);
canvas.addEventListener('touchcancel', handleEnd);
canvas.addEventListener('mousedown', handleStart);
window.addEventListener('mousemove', (e) => {
if (aiming && gameActive) {
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const scaleY = CH / rect.height;
aimStartX = (e.clientX - rect.left) * scaleX;
aimStartY = (e.clientY - rect.top) * scaleY;
}
});
window.addEventListener('mouseup', handleEnd);
document.getElementById('restartButton').addEventListener('click', restartGame);
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
e.preventDefault();
restartGame();
});
}
bindEvents();
restartGame();
gameLoop();
})();
</script>
</body>
</html>
-448
View File
@@ -1,448 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
<title>节奏光剑 · 反应游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
background: linear-gradient(180deg, #0a0a1a 0%, #1a1a3a 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, sans-serif;
padding: 10px;
margin: 0;
}
.game-container {
width: 100%;
max-width: 400px;
background: radial-gradient(circle at 50% 40%, #2a2a5a, #0d0d2b);
border-radius: 48px;
box-shadow: 0 25px 50px rgba(0,0,0,0.8), inset 0 0 30px rgba(100,100,255,0.2);
padding: 16px 12px 20px;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #4a4a8a;
}
canvas {
display: block;
width: 100%;
height: auto;
border-radius: 32px;
background: radial-gradient(ellipse at 50% 0%, #1e1e4a 0%, #08081a 80%);
box-shadow: inset 0 -10px 30px rgba(0,0,0,0.9), 0 12px 30px black;
touch-action: none;
}
.info-panel {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
padding: 0 10px;
color: #d0d0ff;
font-weight: bold;
text-shadow: 0 2px 10px #000066;
font-size: 1.3rem;
}
.score-board {
background: rgba(10, 10, 40, 0.8);
backdrop-filter: blur(15px);
border-radius: 50px;
padding: 8px 22px;
letter-spacing: 1px;
box-shadow: 0 8px 20px rgba(0,0,0,0.7);
border: 1px solid #6a6aff;
color: #ccccff;
}
.restart-btn {
background: #7b4aff;
border: none;
color: white;
font-weight: bold;
font-size: 1.2rem;
padding: 10px 25px;
border-radius: 40px;
background: linear-gradient(180deg, #9f7aff 0%, #5a2ae0 100%);
box-shadow: 0 8px 0 #361a8c, 0 12px 24px rgba(0,0,0,0.6);
transition: all 0.08s ease;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
border: 1px solid #b89fff;
touch-action: manipulation;
}
.restart-btn:active {
transform: translateY(4px);
box-shadow: 0 4px 0 #361a8c, 0 8px 16px black;
background: linear-gradient(180deg, #8a65f0 0%, #421db0 100%);
}
.hint {
color: #aaaadd;
font-size: 0.8rem;
margin-top: 6px;
text-align: center;
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="360" height="600" aria-label="节奏光剑游戏画面"></canvas>
<div class="info-panel">
<div class="score-board" id="scoreDisplay">⚡ 0</div>
<button class="restart-btn" id="restartButton">🔮 重启</button>
</div>
<div class="hint">👇 点击下方轨道区域 | 击中光点</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
const CW = 360;
const CH = 600;
canvas.width = CW;
canvas.height = CH;
// ----- 轨道设置 -----
const NUM_TRACKS = 4;
const TRACK_WIDTH = CW / NUM_TRACKS;
const HIT_ZONE_Y = CH - 75; // 判定线Y坐标
const HIT_TOLERANCE = 42; // 判定范围
// 游戏状态
let gameActive = true;
let score = 0;
let combo = 0;
let maxCombo = 0;
// 音符数组
let notes = [];
// 粒子效果
let particles = [];
// 节拍控制
const NOTE_SPEED = 5.2;
const SPAWN_INTERVAL = 28; // 帧间隔
let frameCounter = 0;
// 轨道闪烁效果
let trackFlash = Array(NUM_TRACKS).fill(0);
// ----- 辅助函数 -----
function randomTrack() {
return Math.floor(Math.random() * NUM_TRACKS);
}
function spawnNote() {
const track = randomTrack();
const x = track * TRACK_WIDTH + TRACK_WIDTH/2;
notes.push({
x: x,
y: -30,
track: track,
radius: 14,
active: true,
});
}
function restartGame() {
gameActive = true;
score = 0;
combo = 0;
maxCombo = 0;
notes = [];
particles = [];
frameCounter = 0;
trackFlash = Array(NUM_TRACKS).fill(0);
updateScoreDisplay();
}
function endGame() {
if (!gameActive) return;
gameActive = false;
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(50);
}
}
function updateScoreDisplay() {
scoreSpan.textContent = `⚡ ${score}`;
}
// 添加粒子
function spawnParticles(x, y, color) {
for (let i = 0; i < 10; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 5;
particles.push({
x: x,
y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1.0,
decay: 0.02 + Math.random() * 0.04,
color: color,
size: 2 + Math.random() * 4,
});
}
}
// 处理点击轨道
function hitTrack(trackIndex) {
if (!gameActive) return;
let hitSuccess = false;
let bestNoteIndex = -1;
let bestDistance = HIT_TOLERANCE;
// 寻找最近的音符
for (let i = notes.length - 1; i >= 0; i--) {
const note = notes[i];
if (!note.active || note.track !== trackIndex) continue;
const distToZone = Math.abs(note.y - HIT_ZONE_Y);
if (distToZone < bestDistance) {
bestDistance = distToZone;
bestNoteIndex = i;
}
}
if (bestNoteIndex !== -1) {
const note = notes[bestNoteIndex];
notes.splice(bestNoteIndex, 1);
combo++;
if (combo > maxCombo) maxCombo = combo;
// 根据准确度加分
const accuracy = 1 - (bestDistance / HIT_TOLERANCE);
const points = Math.floor(10 + accuracy * 30);
score += points;
spawnParticles(note.x, note.y, '#ffff88');
trackFlash[trackIndex] = 1.0;
hitSuccess = true;
}
if (!hitSuccess) {
// 失误:重置连击
combo = 0;
trackFlash[trackIndex] = 0.4;
spawnParticles(
trackIndex * TRACK_WIDTH + TRACK_WIDTH/2,
HIT_ZONE_Y,
'#ff5555'
);
}
updateScoreDisplay();
}
// ----- 更新逻辑 -----
function update() {
if (!gameActive) return;
// 更新音符位置
for (let i = notes.length - 1; i >= 0; i--) {
const note = notes[i];
note.y += NOTE_SPEED;
// 超出屏幕底部移除,并断连击
if (note.y > CH + 50) {
notes.splice(i, 1);
combo = 0;
updateScoreDisplay();
}
}
// 更新粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= p.decay;
if (p.life <= 0) particles.splice(i, 1);
}
// 更新轨道闪烁
for (let t = 0; t < NUM_TRACKS; t++) {
if (trackFlash[t] > 0) trackFlash[t] -= 0.05;
}
// 生成音符
if (gameActive) {
frameCounter++;
if (frameCounter >= SPAWN_INTERVAL) {
spawnNote();
frameCounter = 0;
const variation = Math.floor(Math.random() * 8) - 2;
frameCounter = -variation;
}
}
// 检查游戏结束条件 (可选:漏掉太多音符?这里不使用,纯无限)
}
// ----- 绘制图形 -----
function draw() {
ctx.clearRect(0, 0, CW, CH);
// 绘制轨道线
for (let t = 0; t < NUM_TRACKS; t++) {
const trackX = t * TRACK_WIDTH;
ctx.fillStyle = trackFlash[t] > 0.1
? `rgba(180, 180, 255, ${0.15 + trackFlash[t]*0.4})`
: 'rgba(40, 40, 80, 0.4)';
ctx.fillRect(trackX + 4, 0, TRACK_WIDTH - 8, CH);
ctx.strokeStyle = '#3a3a7a';
ctx.lineWidth = 1;
ctx.setLineDash([12, 20]);
ctx.beginPath();
ctx.moveTo(trackX + TRACK_WIDTH/2, 0);
ctx.lineTo(trackX + TRACK_WIDTH/2, CH);
ctx.stroke();
ctx.setLineDash([]);
}
// 判定线
ctx.strokeStyle = '#ffdd44';
ctx.lineWidth = 3;
ctx.shadowColor = '#ffaa00';
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.moveTo(0, HIT_ZONE_Y);
ctx.lineTo(CW, HIT_ZONE_Y);
ctx.stroke();
ctx.shadowBlur = 0;
// 绘制音符
for (let note of notes) {
if (!note.active) continue;
ctx.save();
ctx.shadowColor = '#ffffff';
ctx.shadowBlur = 16;
const gradient = ctx.createRadialGradient(note.x-2, note.y-2, 2, note.x, note.y, note.radius);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(0.5, '#aaccff');
gradient.addColorStop(1, '#3355aa');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(note.x, note.y, note.radius, 0, Math.PI*2);
ctx.fill();
// 内核高光
ctx.fillStyle = '#ffffffdd';
ctx.beginPath();
ctx.arc(note.x-2, note.y-3, note.radius*0.4, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
// 绘制粒子
for (let p of particles) {
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
}
ctx.globalAlpha = 1;
// 连击显示
if (combo > 2) {
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 22px "Segoe UI"';
ctx.textAlign = 'center';
ctx.shadowColor = '#ffaa00';
ctx.shadowBlur = 20;
ctx.fillText(`${combo} COMBO`, CW/2, 65);
ctx.shadowBlur = 0;
}
// 游戏结束
if (!gameActive) {
ctx.globalAlpha = 0.8;
ctx.fillStyle = '#05051a';
ctx.fillRect(0, 0, CW, CH);
ctx.globalAlpha = 1;
ctx.fillStyle = '#d0d0ff';
ctx.font = 'bold 28px "Segoe UI"';
ctx.textAlign = 'center';
ctx.fillText('🌙 结束', CW/2, CH/2-25);
ctx.font = '16px system-ui';
ctx.fillText(`最高连击: ${maxCombo}`, CW/2, CH/2+30);
}
}
// ----- 游戏循环 -----
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ----- 事件处理 -----
function handleCanvasTouch(e) {
e.preventDefault();
if (!gameActive) return;
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
// 支持多点触摸处理多个轨道
if (e.touches) {
for (let i = 0; i < e.touches.length; i++) {
const touch = e.touches[i];
const canvasX = (touch.clientX - rect.left) * scaleX;
const trackIndex = Math.floor(canvasX / TRACK_WIDTH);
if (trackIndex >= 0 && trackIndex < NUM_TRACKS) {
hitTrack(trackIndex);
}
}
}
}
function handleMouseDown(e) {
e.preventDefault();
if (!gameActive) return;
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const canvasX = (e.clientX - rect.left) * scaleX;
const trackIndex = Math.floor(canvasX / TRACK_WIDTH);
if (trackIndex >= 0 && trackIndex < NUM_TRACKS) {
hitTrack(trackIndex);
}
}
function bindEvents() {
canvas.addEventListener('touchstart', handleCanvasTouch, { passive: false });
canvas.addEventListener('mousedown', handleMouseDown);
document.getElementById('restartButton').addEventListener('click', restartGame);
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
e.preventDefault();
restartGame();
});
}
bindEvents();
restartGame();
gameLoop();
})();
</script>
</body>
</html>
-459
View File
@@ -1,459 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover, shrink-to-fit=yes">
<title>像素守卫 · 射击游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
background: linear-gradient(180deg, #1a3a1a 0%, #0d260d 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', system-ui, sans-serif;
padding: 10px;
margin: 0;
}
.game-container {
width: 100%;
max-width: 400px;
background: radial-gradient(circle at 50% 30%, #3a6b3a, #1a331a);
border-radius: 48px;
box-shadow: 0 25px 50px rgba(0,0,0,0.7), inset 0 0 25px rgba(150,255,150,0.15);
padding: 16px 12px 20px;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #5a8a5a;
}
canvas {
display: block;
width: 100%;
height: auto;
border-radius: 32px;
background: radial-gradient(ellipse at 50% 20%, #4a7a4a 0%, #1a2e1a 85%);
box-shadow: inset 0 -8px 25px rgba(0,0,0,0.8), 0 12px 28px black;
touch-action: none;
}
.info-panel {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 14px;
padding: 0 10px;
color: #c0ffc0;
font-weight: bold;
text-shadow: 0 2px 8px black;
font-size: 1.3rem;
}
.score-board {
background: rgba(0,20,0,0.8);
backdrop-filter: blur(12px);
border-radius: 50px;
padding: 8px 22px;
letter-spacing: 1px;
box-shadow: 0 8px 18px rgba(0,0,0,0.6);
border: 1px solid #6aaa6a;
}
.restart-btn {
background: #4caf50;
border: none;
color: white;
font-weight: bold;
font-size: 1.2rem;
padding: 10px 25px;
border-radius: 40px;
background: linear-gradient(180deg, #6fbf6f 0%, #2e7d32 100%);
box-shadow: 0 8px 0 #1b5e20, 0 12px 22px rgba(0,0,0,0.5);
transition: all 0.08s ease;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
border: 1px solid #a5d6a7;
touch-action: manipulation;
}
.restart-btn:active {
transform: translateY(4px);
box-shadow: 0 4px 0 #1b5e20, 0 8px 14px rgba(0,0,0,0.6);
background: linear-gradient(180deg, #5aa55a 0%, #1f5a22 100%);
}
.hint {
color: #a0d0a0;
font-size: 0.8rem;
margin-top: 6px;
text-align: center;
}
</style>
</head>
<body>
<div class="game-container">
<canvas id="gameCanvas" width="360" height="600" aria-label="像素守卫射击游戏"></canvas>
<div class="info-panel">
<div class="score-board" id="scoreDisplay">🗡️ 0</div>
<button class="restart-btn" id="restartButton">🏰 守城</button>
</div>
<div class="hint">👈 滑动瞄准 | 点击射击敌人</div>
</div>
<script>
(function() {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('scoreDisplay');
const CW = 360;
const CH = 600;
canvas.width = CW;
canvas.height = CH;
// ----- 炮台设置 -----
const TOWER_X = CW / 2;
const TOWER_Y = CH - 55;
let aimAngle = -Math.PI / 2; // 默认向上
// 游戏状态
let gameActive = true;
let score = 0;
let health = 100;
const MAX_HEALTH = 100;
// 敌人数组
let enemies = [];
// 子弹数组
let bullets = [];
// 粒子效果
let particles = [];
// 生成控制
const ENEMY_SPAWN_INTERVAL = 40;
let frameCounter = 0;
let difficultyTimer = 0;
// ----- 辅助函数 -----
function spawnEnemy() {
const side = Math.random() < 0.5 ? 'left' : 'right';
const x = side === 'left' ? -20 : CW + 20;
const y = 40 + Math.random() * 250;
const speedX = side === 'left' ? 1.2 + Math.random() * 1.5 : -(1.2 + Math.random() * 1.5);
const speedY = (Math.random() - 0.5) * 1.2;
enemies.push({
x: x,
y: y,
vx: speedX,
vy: speedY,
radius: 14,
health: 1,
color: '#ff5555',
});
}
function restartGame() {
gameActive = true;
score = 0;
health = MAX_HEALTH;
enemies = [];
bullets = [];
particles = [];
frameCounter = 0;
difficultyTimer = 0;
aimAngle = -Math.PI / 2;
updateScoreDisplay();
}
function endGame() {
if (!gameActive) return;
gameActive = false;
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(60);
}
}
function updateScoreDisplay() {
scoreSpan.textContent = `🗡️ ${score} ❤️ ${health}`;
}
function damagePlayer(amount) {
health -= amount;
if (health <= 0) {
health = 0;
endGame();
}
updateScoreDisplay();
}
// 发射子弹
function shootBullet() {
if (!gameActive) return;
const bulletSpeed = 8;
const vx = Math.cos(aimAngle) * bulletSpeed;
const vy = Math.sin(aimAngle) * bulletSpeed;
bullets.push({
x: TOWER_X,
y: TOWER_Y,
vx: vx,
vy: vy,
radius: 5,
life: 80,
});
}
// ----- 更新逻辑 -----
function update() {
if (!gameActive) return;
// 更新子弹
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx;
b.y += b.vy;
b.life--;
if (b.life <= 0 || b.x < -30 || b.x > CW + 30 || b.y < -30 || b.y > CH + 30) {
bullets.splice(i, 1);
continue;
}
// 子弹与敌人碰撞
let bulletRemoved = false;
for (let j = enemies.length - 1; j >= 0; j--) {
const enemy = enemies[j];
const dist = Math.hypot(b.x - enemy.x, b.y - enemy.y);
if (dist < enemy.radius + b.radius) {
// 消灭敌人
enemies.splice(j, 1);
bulletRemoved = true;
score += 20;
updateScoreDisplay();
spawnParticles(enemy.x, enemy.y, '#ffaa00', 8);
break;
}
}
if (bulletRemoved) {
bullets.splice(i, 1);
}
}
// 更新敌人
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
enemy.x += enemy.vx;
enemy.y += enemy.vy;
// 敌人与炮台碰撞
const distToTower = Math.hypot(enemy.x - TOWER_X, enemy.y - TOWER_Y);
if (distToTower < 35) {
enemies.splice(i, 1);
damagePlayer(12);
spawnParticles(TOWER_X, TOWER_Y, '#ff3333', 12);
continue;
}
// 移除离开屏幕的敌人
if (enemy.x < -60 || enemy.x > CW + 60 || enemy.y < -60 || enemy.y > CH + 60) {
enemies.splice(i, 1);
}
}
// 更新粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 0.03;
if (p.life <= 0) particles.splice(i, 1);
}
// 生成敌人 (随难度增加)
if (gameActive) {
frameCounter++;
difficultyTimer++;
const adjustedInterval = Math.max(18, ENEMY_SPAWN_INTERVAL - Math.floor(difficultyTimer / 300));
if (frameCounter >= adjustedInterval) {
spawnEnemy();
frameCounter = 0;
if (Math.random() < 0.3) spawnEnemy(); // 偶尔双倍
}
}
}
function spawnParticles(x, y, color, count) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 2 + Math.random() * 5;
particles.push({
x: x, y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1.0,
color: color,
size: 3 + Math.random() * 4,
});
}
}
// ----- 绘制图形 -----
function draw() {
ctx.clearRect(0, 0, CW, CH);
// 绘制地面网格
ctx.strokeStyle = '#2a4a2a';
ctx.lineWidth = 0.8;
for (let i = 0; i < CW; i += 30) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, CH);
ctx.stroke();
}
// 绘制敌人
for (let enemy of enemies) {
ctx.save();
ctx.shadowColor = '#ff0000aa';
ctx.shadowBlur = 14;
ctx.fillStyle = enemy.color;
ctx.beginPath();
ctx.arc(enemy.x, enemy.y, enemy.radius, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(enemy.x-3, enemy.y-4, 4, 0, Math.PI*2);
ctx.fill();
ctx.restore();
}
// 绘制子弹
for (let b of bullets) {
ctx.fillStyle = '#ffff66';
ctx.shadowColor = '#ffaa00';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(b.x, b.y, b.radius, 0, Math.PI*2);
ctx.fill();
}
// 绘制粒子
for (let p of particles) {
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.shadowBlur = 0;
// 绘制炮台
ctx.save();
ctx.translate(TOWER_X, TOWER_Y);
// 基座
ctx.fillStyle = '#5a5a7a';
ctx.shadowColor = '#000000';
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.arc(0, 0, 18, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#7a7aaa';
ctx.beginPath();
ctx.arc(0, 0, 12, 0, Math.PI*2);
ctx.fill();
// 炮管
ctx.rotate(aimAngle);
ctx.fillStyle = '#ccccdd';
ctx.fillRect(12, -5, 24, 10);
ctx.fillStyle = '#aaaacc';
ctx.fillRect(30, -3, 10, 6);
ctx.restore();
// 生命条
ctx.fillStyle = '#331111';
ctx.fillRect(15, CH-18, CW-30, 14);
const healthPercent = health / MAX_HEALTH;
const healthColor = healthPercent > 0.5 ? '#44cc44' : '#ff6644';
ctx.fillStyle = healthColor;
ctx.fillRect(15, CH-18, (CW-30) * healthPercent, 14);
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
ctx.strokeRect(15, CH-18, CW-30, 14);
// 游戏结束
if (!gameActive) {
ctx.globalAlpha = 0.8;
ctx.fillStyle = '#0a0f0a';
ctx.fillRect(0, 0, CW, CH);
ctx.globalAlpha = 1;
ctx.fillStyle = '#ffcccc';
ctx.font = 'bold 28px "Segoe UI"';
ctx.textAlign = 'center';
ctx.fillText('💀 失守', CW/2, CH/2-20);
}
}
// ----- 游戏循环 -----
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ----- 事件处理 -----
function handleAim(e) {
e.preventDefault();
if (!gameActive) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const rect = canvas.getBoundingClientRect();
const scaleX = CW / rect.width;
const scaleY = CH / rect.height;
const canvasX = (clientX - rect.left) * scaleX;
const canvasY = (clientY - rect.top) * scaleY;
aimAngle = Math.atan2(canvasY - TOWER_Y, canvasX - TOWER_X);
}
function handleShoot(e) {
e.preventDefault();
if (!gameActive) return;
shootBullet();
}
function bindEvents() {
canvas.addEventListener('touchmove', handleAim, { passive: false });
canvas.addEventListener('touchstart', (e) => {
handleAim(e);
handleShoot(e);
});
canvas.addEventListener('mousemove', (e) => {
if (e.buttons === 1) handleAim(e);
});
canvas.addEventListener('mousedown', (e) => {
handleAim(e);
handleShoot(e);
});
document.getElementById('restartButton').addEventListener('click', restartGame);
document.getElementById('restartButton').addEventListener('touchstart', (e) => {
e.preventDefault();
restartGame();
});
}
bindEvents();
restartGame();
gameLoop();
})();
</script>
</body>
</html>
-227
View File
@@ -1,227 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="DNFS 树状笔记:以树状结构组织知识的笔记社区,支持 Markdown、链接卡片、图片与文件分享。">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fdf6ee">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1e1712">
<link rel="icon" href="favicon.ico">
<link rel="stylesheet" href="css/theme.css">
<link rel="stylesheet" href="css/index.css">
<title>DNFS 树状笔记 — 让知识自然成体系</title>
</head>
<body>
<header class="header">
<div class="wrap">
<div class="header-inner">
<h1 id="logo">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="10.51" x2="15.42" y2="6.51"/><line x1="8.59" y1="13.49" x2="15.42" y2="17.49"/></svg>
<span>DNFS 树状笔记</span>
</h1>
<nav class="right-group" aria-label="用户操作">
<img src="favicon.ico" alt="用户头像" id="avatar" style="display:none">
<b id="name" style="display:none"></b>
<button id="enterBtn" class="btn btn-primary" style="display:none">进入笔记</button>
<button id="logoutBtn" class="btn btn-ghost" style="display:none">退出</button>
<button id="loginBtn" class="btn btn-primary">登录</button>
<button id="registerBtn" class="btn btn-ghost">注册</button>
</nav>
</div>
</div>
</header>
<main id="main-page">
<!-- ===== Hero ===== -->
<section class="hero">
<div class="hero-bg" aria-hidden="true">
<svg class="hero-tree" viewBox="0 0 800 440" fill="none">
<defs>
<radialGradient id="glow" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="currentColor" stop-opacity="0.18"/>
<stop offset="100%" stop-color="currentColor" stop-opacity="0"/>
</radialGradient>
</defs>
<circle cx="400" cy="210" r="180" fill="url(#glow)"/>
<g class="tree-branches" stroke="currentColor" stroke-linecap="round">
<path pathLength="100" d="M400 400 L400 295" stroke-width="2.5"/>
<path pathLength="100" d="M400 295 C 365 265, 310 240, 260 205" stroke-width="2"/>
<path pathLength="100" d="M400 295 C 435 265, 490 240, 540 205" stroke-width="2"/>
<path pathLength="100" d="M260 205 C 238 178, 210 152, 185 120" stroke-width="1.5"/>
<path pathLength="100" d="M260 205 C 278 178, 300 152, 318 120" stroke-width="1.5"/>
<path pathLength="100" d="M540 205 C 518 178, 496 152, 478 120" stroke-width="1.5"/>
<path pathLength="100" d="M540 205 C 562 178, 590 152, 615 120" stroke-width="1.5"/>
<path pathLength="100" d="M185 120 C 172 100, 160 82, 150 60" stroke-width="1"/>
<path pathLength="100" d="M318 120 C 328 100, 338 82, 345 60" stroke-width="1"/>
<path pathLength="100" d="M478 120 C 468 100, 458 82, 452 60" stroke-width="1"/>
<path pathLength="100" d="M615 120 C 628 100, 640 82, 650 60" stroke-width="1"/>
</g>
<g class="tree-nodes" fill="currentColor">
<circle cx="400" cy="400" r="7" opacity=".9"/>
<circle cx="400" cy="295" r="5.5"/>
<circle cx="260" cy="205" r="5"/>
<circle cx="540" cy="205" r="5"/>
<circle cx="185" cy="120" r="4"/>
<circle cx="318" cy="120" r="4"/>
<circle cx="478" cy="120" r="4"/>
<circle cx="615" cy="120" r="4"/>
<circle cx="150" cy="60" r="3" opacity=".65"/>
<circle cx="345" cy="60" r="3" opacity=".65"/>
<circle cx="452" cy="60" r="3" opacity=".65"/>
<circle cx="650" cy="60" r="3" opacity=".65"/>
</g>
</svg>
</div>
<div class="wrap hero-inner">
<p class="hero-badge"><span class="badge-dot"></span>树状结构 · Markdown · 多媒体分享</p>
<h2 class="hero-title">让知识像树一样<br><span class="accent-text">自然生长</span></h2>
<p class="hero-sub">每个节点都可以生长出子节点,像树枝一样层层延展<br>让零散的想法汇聚成体系</p>
<div class="hero-actions">
<button class="btn btn-primary btn-lg" data-cta>开始记录</button>
<a class="btn btn-ghost btn-lg" href="#features">了解更多</a>
</div>
</div>
<a class="scroll-hint" href="#features" aria-label="下滑了解特性">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</a>
</section>
<!-- ===== 核心特性 ===== -->
<section id="features" class="section features-section">
<div class="wrap">
<p class="section-eyebrow">核心特性</p>
<h2 class="section-title">为什么选择树状笔记</h2>
<p class="section-sub">从灵感到体系,让知识在树上自然生长</p>
<div class="features">
<div class="feature-card">
<div class="feature-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="10.51" x2="15.42" y2="6.51"/><line x1="8.59" y1="13.49" x2="15.42" y2="17.49"/></svg>
</div>
<div class="feature-body">
<h3>树状结构组织</h3>
<p>每个节点都能生长出子节点,像树枝般层层延展,让零散的想法自然汇聚成体系。</p>
</div>
<span class="feature-tag">无限层级</span>
</div>
<div class="feature-card">
<div class="feature-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
</div>
<div class="feature-body">
<h3>Markdown 写作</h3>
<p>原生支持 Markdown 渲染,代码块、引用、列表一应俱全,专注内容本身。</p>
</div>
<span class="feature-tag">专注写作</span>
</div>
<div class="feature-card">
<div class="feature-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</div>
<div class="feature-body">
<h3>多媒体分享</h3>
<p>链接卡片、图片直显、文件下载,多种内容形态一站承载,分享更自由。</p>
</div>
<span class="feature-tag">自由分享</span>
</div>
<div class="feature-card">
<div class="feature-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</div>
<div class="feature-body">
<h3>深色模式</h3>
<p>跟随系统偏好自动切换明暗主题,深夜记录也不刺眼,日夜皆宜。</p>
</div>
<span class="feature-tag">日夜皆宜</span>
</div>
</div>
</div>
</section>
<!-- ===== 关于本站 ===== -->
<section class="section about-section">
<div class="wrap">
<p class="section-eyebrow">我们的理念</p>
<h2 class="section-title">关于本站</h2>
<div class="about-panel">
<svg class="about-deco" viewBox="0 0 200 200" fill="none" aria-hidden="true">
<circle cx="100" cy="100" r="90" stroke="currentColor" stroke-width=".6" opacity=".15"/>
<circle cx="100" cy="100" r="60" stroke="currentColor" stroke-width=".5" opacity=".10"/>
<circle cx="100" cy="100" r="30" stroke="currentColor" stroke-width=".4" opacity=".08"/>
</svg>
<p class="site-intro">
DNFS 树状笔记是一个以树状结构组织内容的笔记社区:每个节点都可以生长出子节点,
像树枝一样层层延展,让知识自然成体系。支持 Markdown 文本、链接卡片、图片与文件分享,
注册登录后即可创建属于你自己的知识树。
</p>
<ul class="about-stats">
<li><b>∞</b><span>节点层级无上限</span></li>
<li><b>MD</b><span>原生 Markdown 渲染</span></li>
<li><b>4+</b><span>内容形态一站承载</span></li>
</ul>
</div>
<nav class="quick-links" aria-label="快捷链接">
<a href="allcolor.html">调色板</a>
<a href="games/">小游戏</a>
<a href="LICENSE.html">用户使用条例</a>
</nav>
</div>
</section>
<!-- ===== 结尾 CTA ===== -->
<section class="section cta-section" aria-label="立即开始">
<div class="wrap">
<div class="cta-band">
<svg class="cta-tree" viewBox="0 0 400 200" fill="none" aria-hidden="true">
<g stroke="currentColor" stroke-width="1.2" stroke-linecap="round" opacity=".6">
<path d="M200 190 L200 140"/>
<path d="M200 140 C 175 120, 145 105, 120 85"/>
<path d="M200 140 C 225 120, 255 105, 280 85"/>
<path d="M120 85 C 105 65, 90 50, 80 30"/>
<path d="M120 85 C 135 65, 148 50, 155 30"/>
<path d="M280 85 C 265 65, 252 50, 245 30"/>
<path d="M280 85 C 295 65, 310 50, 320 30"/>
</g>
<g fill="currentColor" opacity=".5">
<circle cx="200" cy="190" r="4"/>
<circle cx="200" cy="140" r="3"/>
<circle cx="120" cy="85" r="2.5"/>
<circle cx="280" cy="85" r="2.5"/>
<circle cx="80" cy="30" r="2"/>
<circle cx="155" cy="30" r="2"/>
<circle cx="245" cy="30" r="2"/>
<circle cx="320" cy="30" r="2"/>
</g>
</svg>
<h2 class="cta-title">准备好让知识生长了吗?</h2>
<p class="cta-sub">注册一个账号,种下你的第一棵知识树</p>
<button class="btn btn-cta btn-lg" data-cta>立即开始</button>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="wrap">
<div class="footer-inner">
<div class="footer-brand">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="10.51" x2="15.42" y2="6.51"/><line x1="8.59" y1="13.49" x2="15.42" y2="17.49"/></svg>
<span>DNFS 树状笔记</span>
</div>
<nav class="footer-nav" aria-label="页脚导航">
<a href="dnfs.html">树状笔记</a>
<a href="allcolor.html">调色板</a>
<a href="games/">小游戏</a>
<a href="LICENSE.html">用户使用条例</a>
</nav>
<p class="footer-icp">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
</p>
</div>
</div>
</footer>
<script src="js/token_manager.js"></script>
<script src="js/index.js"></script>
</body>
</html>
-435
View File
@@ -1,435 +0,0 @@
'use strict';
/* ============================================================
* aibox Web 客户端 · 移动端版(经 gateway WS 反向代理,仅管理员可用)
* 协议与 BotOps/aibox client.py 完全兼容:
* 发送:{"type":"message","content":...} / {"type":"command","command":"/..."} / {"type":"interrupt",...}
* 接收:chunk(增量片段) / reasoning_start / reasoning / reasoning_end / ai_break /
* complete / line(过程行) / task_process / task_status / task_error / task_added /
* dialogue_ended / dialogue_reset / interrupt_ack / error / info /
* command_result / connection_ack / notification / pong
* ============================================================ */
/* gateway [WsProxy] routes 中配置的路径(HttpOnly q4_token Cookie 随握手自动携带,JS 不经手 Token)。
* 需与 gateway.ini 的 [WsProxy] routes 条目一致,如:routes = /ws/aibox=ws://127.0.0.1:8765; */
const WS_URL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws/aibox';
const RECONNECT_MIN = 2000; /* 指数退避起点(2s→30s 封顶) */
const RECONNECT_MAX = 30000;
const REASONING_TAIL = 3000; /* 思考区域超长时仅显示末尾字符数 */
/* ---------- Markdown 渲染(链接一律新标签打开且不带 referrer) ---------- */
const md = window.markdownit({ html: false, linkify: true, breaks: true });
const defaultLinkOpen = md.renderer.rules.link_open || ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
tokens[idx].attrSet('target', '_blank');
tokens[idx].attrSet('rel', 'noopener noreferrer');
return defaultLinkOpen(tokens, idx, options, env, self);
};
/* ---------- DOM ---------- */
const chatEl = document.getElementById('chat');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send-btn');
const interruptBtn = document.getElementById('interrupt-btn');
const connBtn = document.getElementById('conn-btn');
const statusEl = document.getElementById('conn-status');
const statusText = document.getElementById('status-text');
const taskPanel = document.getElementById('task-panel');
const taskList = document.getElementById('task-list');
const taskToggle = document.getElementById('task-toggle');
const taskBadge = document.getElementById('task-badge');
const backdrop = document.getElementById('backdrop');
/* ---------- 状态 ---------- */
let ws = null;
let manualClose = false; /* 手动断开则抑制自动重连 */
let reconnectDelay = RECONNECT_MIN;
let reconnectTimer = null;
let heartbeatTimer = null; /* 心跳定时器,10s 发送 ping 保持 WS 连接 */
const HEARTBEAT_INTERVAL = 10000; /* 心跳间隔:需低于前置代理(Nginx/CDN)的 proxy_read_timeout */
function startHeartbeat() {
stopHeartbeat();
heartbeatTimer = setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, HEARTBEAT_INTERVAL);
}
function stopHeartbeat() {
if (heartbeatTimer) { clearTimeout(heartbeatTimer); heartbeatTimer = null; }
}
/* 流式状态:不预创建空气泡,第一个 chunk 到来才挂载(保证思考区必然在正式回答之前) */
let aiBubble = null, aiText = '', aiActive = false;
let reasoningBox = null, reasoningBody = null, reasoningHead = null, reasoningText = '';
let unreadTasks = 0;
/* ---------- 滚动(距底 80px 内才自动跟随,用户上翻阅读不打断) ---------- */
function nearBottom() {
return chatEl.scrollHeight - chatEl.scrollTop - chatEl.clientHeight < 80;
}
function scrollToBottom(force) {
if (force || nearBottom()) chatEl.scrollTop = chatEl.scrollHeight;
}
/* ---------- 系统消息 / 任务面板行 ---------- */
function addSystem(text, isError) {
const div = document.createElement('div');
div.className = isError ? 'msg msg-error' : 'msg msg-system';
div.textContent = text;
chatEl.appendChild(div);
scrollToBottom();
}
function addTaskLine(text) {
const div = document.createElement('div');
div.textContent = text;
taskList.appendChild(div);
taskList.scrollTop = taskList.scrollHeight;
if (!taskPanel.classList.contains('open')) {
unreadTasks += 1;
taskBadge.style.display = '';
taskBadge.textContent = unreadTasks > 99 ? '99+' : String(unreadTasks);
}
}
/* ---------- 消息气泡 ---------- */
function addUserBubble(text) {
const div = document.createElement('div');
div.className = 'msg msg-user';
div.textContent = text;
chatEl.appendChild(div);
scrollToBottom(true);
}
function ensureAiBubble() {
if (aiBubble) return;
aiBubble = document.createElement('div');
aiBubble.className = 'msg msg-ai streaming';
chatEl.appendChild(aiBubble);
scrollToBottom(true);
}
/* 服务端 chunk 为增量片段(发送后即清零),直接拼接 */
function appendAi(delta) {
if (!delta || aiActive === false) return;
ensureAiBubble();
aiText += delta;
aiBubble.innerHTML = md.render(aiText);
scrollToBottom();
}
/* complete 为收尾信号:chunk 流式是内容权威来源,仅异常丢帧时用 full_text 补建 */
function finishAi(fullText) {
if (aiActive === false && !aiBubble && !reasoningBox) return;
if (!aiBubble && fullText) {
ensureAiBubble();
aiText = fullText;
aiBubble.innerHTML = md.render(aiText);
}
if (aiBubble) aiBubble.classList.remove('streaming');
aiBubble = null; aiText = ''; aiActive = false;
reasoningBox = null; reasoningBody = null; reasoningHead = null; reasoningText = '';
setStreaming(false);
scrollToBottom();
}
/* ---------- 思考区域(可折叠) ---------- */
function startReasoning(hint) {
if (reasoningBox) return;
reasoningText = '';
reasoningBox = document.createElement('div');
reasoningBox.className = 'msg msg-reasoning';
reasoningHead = document.createElement('div');
reasoningHead.className = 'reasoning-head';
reasoningHead.textContent = '💭 ' + (hint || '思考中...');
reasoningBody = document.createElement('div');
reasoningBody.className = 'reasoning-body';
reasoningHead.addEventListener('click', () => reasoningBody.classList.toggle('folded'));
reasoningBox.appendChild(reasoningHead);
reasoningBox.appendChild(reasoningBody);
chatEl.appendChild(reasoningBox);
scrollToBottom(true);
}
function appendReasoning(delta) {
if (!delta) return;
if (aiActive === false) return; /* 已收尾(中断/完成),丢弃残留片段 */
if (!reasoningBox) startReasoning();
reasoningText += delta;
reasoningBody.textContent = reasoningText.length > REASONING_TAIL
? '...(前略)\n' + reasoningText.slice(-REASONING_TAIL)
: reasoningText;
scrollToBottom();
}
function endReasoning() {
if (reasoningHead && reasoningText) {
reasoningHead.textContent = '💭 思考过程(' + reasoningText.length + ' 字符,点击折叠/展开)';
reasoningBody.classList.add('folded'); /* 结束后默认折叠,避免占屏 */
}
/* 清除引用(DOM元素保留显示),下一轮 reasoning_start 创建新的思考区域 */
reasoningBox = null; reasoningHead = null; reasoningBody = null; reasoningText = '';
}
function abortReasoning() {
/* 用户中断:更新思考区域标签为已中断,保留已展示内容 */
if (reasoningHead) {
if (reasoningText) {
reasoningHead.textContent = '🛑 思考已中断(' + reasoningText.length + ' 字符)';
} else {
reasoningHead.textContent = '🛑 思考已中断';
}
}
/* 清除引用,DOM元素保留显示 */
reasoningBox = null; reasoningHead = null; reasoningBody = null; reasoningText = '';
}
/* ---------- AI 文本分段(工具调用时 breakAi,后续 chunk 创建新气泡) ---------- */
function breakAi() {
if (aiBubble) {
aiBubble.classList.remove('streaming');
aiBubble = null; aiText = '';
}
}
/* ---------- 连接状态 ---------- */
function setStatus(cls, text) {
statusEl.className = cls;
statusText.textContent = text;
}
function setStreaming(on) {
interruptBtn.style.display = on ? '' : 'none';
sendBtn.disabled = false; /* 流式期间仍可发送(服务端排队),仅中断按钮显隐 */
}
/* ---------- WebSocket 生命周期(指数退避自动重连,手动断开抑制) ---------- */
function connect() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
clearTimeout(reconnectTimer);
manualClose = false;
setStatus('connecting', '连接中');
try {
ws = new WebSocket(WS_URL);
} catch (e) {
addSystem('连接失败:' + e.message, true);
scheduleReconnect();
return;
}
ws.onopen = () => {
reconnectDelay = RECONNECT_MIN;
setStatus('connected', '已连接');
connBtn.textContent = '断开';
connBtn.classList.add('on');
startHeartbeat();
};
ws.onmessage = (ev) => {
let data;
try { data = JSON.parse(ev.data); }
catch (e) { data = { type: 'chunk', content: ev.data }; }
routeMessage(data);
};
ws.onclose = () => {
stopHeartbeat();
setStatus('disconnected', '已断开');
connBtn.textContent = '连接';
connBtn.classList.remove('on');
finishAi('');
ws = null;
if (!manualClose) scheduleReconnect();
};
ws.onerror = () => { /* onclose 随后触发,统一在 onclose 处理 */ };
}
function scheduleReconnect() {
if (manualClose) return;
setStatus('connecting', Math.round(reconnectDelay / 1000) + 's 后重连');
clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX);
connect();
}, reconnectDelay);
}
function disconnect(manual) {
manualClose = manual !== false;
clearTimeout(reconnectTimer);
stopHeartbeat();
if (ws) { ws.close(); ws = null; }
setStatus('disconnected', manualClose ? '已手动断开' : '已断开');
connBtn.textContent = '连接';
connBtn.classList.remove('on');
}
/* ---------- aibox 协议消息路由 ---------- */
function routeMessage(data) {
const t = data.type || '';
const content = typeof data.content === 'string' ? data.content : (data.content == null ? '' : String(data.content));
if (t === 'chunk') {
appendAi(content);
} else if (t === 'reasoning_start') {
startReasoning(content || '深度思考中...');
} else if (t === 'reasoning') {
appendReasoning(content);
} else if (t === 'reasoning_end') {
endReasoning();
} else if (t === 'ai_break') {
/* 工具调用前分段:结束当前AI文本段(保留已显示内容),后续chunk创建新气泡 */
breakAi();
} else if (t === 'line') {
/* 过程行:工具调用提示 / 工具输出 → 任务面板;助手前缀 / 思考提示等聊天区已有承载,跳过 */
const stripped = content.trim();
if (!stripped) return;
if (/^(💬 助手|💬 Assistant|🤔 思考中|🤔 Thinking|🤔 深度思考中|🤔 分析工具结果|🤔 Analyzing|🔧 需要调用工具|🔧 Need to call)/.test(stripped)) return;
addTaskLine(stripped);
} else if (t === 'complete') {
finishAi(content);
} else if (t === 'dialogue_ended') {
finishAi(aiText);
if (content) addSystem('✅ ' + content);
} else if (t === 'task_added') {
addSystem('➕ ' + content);
} else if (t === 'dialogue_reset') {
if (content) addSystem('🔄 ' + content);
} else if (t === 'pong') {
/* 心跳响应,忽略 */
} else if (t === 'task_process') {
const sub = data.msg_type || '';
const title = data.task_title || '未知任务';
if (sub === 'chunk') {
const tail = content.slice(-80);
if (tail.trim()) addTaskLine('⚡ ' + title + ': ' + tail);
} else if (sub === 'complete') {
addTaskLine('✅ 任务完成: ' + title);
}
} else if (t === 'task_status') {
const icon = { executing: '⚙️', complete: '✅', failed: '❌', added: '➕', batch_start: '🚀', batch_complete: '🏁' }[data.status] || '📋';
addTaskLine(icon + ' [' + (data.task_id || '?') + '] ' + (data.title || '') + ': ' + (data.message || content));
} else if (t === 'task_error') {
addTaskLine('❌ [' + (data.task_id || '?') + '] ' + (data.task_title || '?') + ': ' + (data.error || '未知错误'));
} else if (t === 'interrupt_ack') {
/* 服务端确认中断:立即收尾前端状态 */
abortReasoning();
finishAi('');
addSystem('🛑 ' + (content || '已中断'));
} else if (t === 'error') {
addSystem('❌ ' + content, true);
} else if (t === 'info') {
/* 过滤隐式通知(对话开始等),避免污染聊天流 */
if (!/(Conversation started|对话开始|🎭)/.test(content)) addSystem('ℹ️ ' + content);
} else if (t === 'command_result') {
addSystem(content);
} else if (t === 'connection_ack') {
addSystem('✅ ' + content);
} else if (t === 'notification') {
addSystem('🔔 ' + content);
} else if (content) {
/* 未知消息类型不追加到AI聊天区(避免非AI内容污染回复),记入任务面板 */
addTaskLine('[未知消息 type=' + t + '] ' + content.slice(0, 200));
}
}
/* ---------- 发送 ---------- */
function wsSend(obj) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
addSystem('未连接到服务器', true);
return false;
}
ws.send(JSON.stringify(obj));
return true;
}
/* 本地命令表:命中则本地处理,其余 / 命令透传服务器 */
const LOCAL_COMMANDS = {
'/clear'() {
chatEl.replaceChildren();
finishAi('');
},
'/help'() {
addSystem('本地命令:/clear 清屏 · /connect 连接 · /disconnect 断开 · /interrupt 中断 · 其余 / 命令透传服务器');
},
'/connect'() { connect(); },
'/disconnect'() { disconnect(true); },
'/interrupt'() { sendInterrupt(); },
};
function sendCurrentInput() {
const text = inputEl.value.trim();
if (!text) return;
if (text.startsWith('/')) {
const local = LOCAL_COMMANDS[text.split(/\s/)[0]];
if (local) {
local();
inputEl.value = '';
autoResize();
return;
}
}
/* 与 BotOps 一致:/ 开头按 command 发送,否则 message */
const data = text.startsWith('/')
? { type: 'command', command: text }
: { type: 'message', content: text };
if (!wsSend(data)) return;
addUserBubble(text);
aiActive = true; /* 开始接收本轮 AI 流 */
setStreaming(true);
inputEl.value = '';
autoResize();
inputEl.focus();
}
function sendInterrupt() {
wsSend({ type: 'interrupt', content: '用户按下了 Ctrl+C 中断当前操作' });
}
/* ---------- 输入区自适应高度 ---------- */
function autoResize() {
inputEl.style.height = 'auto';
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
}
inputEl.addEventListener('input', autoResize);
inputEl.addEventListener('keydown', (e) => {
/* Enter 发送,Shift+Enter 换行;isComposing 防中文输入法选词误发 */
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.preventDefault();
sendCurrentInput();
}
});
/* 软键盘弹出动画结束后保持聊天区停留底部 */
inputEl.addEventListener('focus', () => setTimeout(() => scrollToBottom(true), 300));
sendBtn.addEventListener('click', sendCurrentInput);
interruptBtn.addEventListener('click', sendInterrupt);
/* ---------- 连接按钮与任务面板(抽屉开关) ---------- */
connBtn.addEventListener('click', () => {
if (ws && ws.readyState === WebSocket.OPEN) disconnect(true);
else connect();
});
function openTaskPanel(open) {
taskPanel.classList.toggle('open', open);
backdrop.classList.toggle('show', open);
if (open) {
unreadTasks = 0;
taskBadge.style.display = 'none';
}
}
taskToggle.addEventListener('click', () => openTaskPanel(!taskPanel.classList.contains('open')));
document.getElementById('task-close').addEventListener('click', () => openTaskPanel(false));
backdrop.addEventListener('click', () => openTaskPanel(false));
/* ---------- 启动:管理员预检(WS 握手由 gateway 强制 is_admin,此处仅提示) ---------- */
(async () => {
try {
const res = await fetch('/whoami', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
const data = res.ok ? await res.json() : null;
const tip = document.getElementById('admin-tip');
if (!data || !data.login) {
tip.textContent = '未登录:aibox 仅管理员可用,请先登录管理员账号';
tip.style.display = '';
} else if (!data.is_admin) {
tip.textContent = '当前账号不是管理员,连接将被网关拒绝';
tip.style.display = '';
}
} catch (e) { /* 预检失败不阻塞,由 WS 握手兜底 */ }
connect();
})();
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-103
View File
@@ -1,103 +0,0 @@
/* index.js — 社区首页逻辑
*
* 登录态检测:POST /whoami(HttpOnly q4_token Cookie 自动携带,JS 不经手 Token)
* 已登录:显示头像/用户名与"进入笔记/退出"按钮,首屏 CTA 指向笔记页
* 未登录:显示"登录/注册",首屏 CTA 指向登录页
*/
(function () {
'use strict';
const nameEl = document.getElementById('name');
const avatarEl = document.getElementById('avatar');
const enterBtn = document.getElementById('enterBtn');
const logoutBtn = document.getElementById('logoutBtn');
const loginBtn = document.getElementById('loginBtn');
const registerBtn = document.getElementById('registerBtn');
const ctaBtns = document.querySelectorAll('[data-cta]'); // 首屏与页尾 CTA 按钮
const logoEl = document.getElementById('logo');
let loggedIn = false;
function go(url) { window.location.href = url; }
logoEl.addEventListener('click', () => go('index.html'));
enterBtn.addEventListener('click', () => go('dnfs.html'));
loginBtn.addEventListener('click', () => go('login.html'));
registerBtn.addEventListener('click', () => go('register.html'));
ctaBtns.forEach(function (btn) {
btn.addEventListener('click', () => go(loggedIn ? 'dnfs.html' : 'login.html'));
});
async function checkLogin() {
try {
const res = await fetch('/whoami', { method: 'POST' });
if (!res.ok) return;
const data = await res.json();
if (data && data.login && data.user) {
loggedIn = true;
nameEl.textContent = String(data.user);
nameEl.style.display = '';
avatarEl.style.display = '';
enterBtn.style.display = '';
logoutBtn.style.display = '';
loginBtn.style.display = 'none';
registerBtn.style.display = 'none';
ctaBtns.forEach(function (btn) { btn.textContent = '进入我的知识树'; });
}
} catch (e) {
console.warn('index: 登录态检测失败', e.message);
}
}
logoutBtn.addEventListener('click', async function () {
try {
await fetch('/logout', { method: 'POST' });
} catch (e) {
console.warn('index: 退出登录请求异常', e.message);
}
location.reload();
});
checkLogin();
})();
/* ===== 视觉增强:滚动渐显 + 卡片光标光斑(装饰性,与登录逻辑解耦) ===== */
(function () {
'use strict';
const reduceMotion = !window.matchMedia('(prefers-reduced-motion: no-preference)').matches;
/* 滚动进入视口渐显:由 JS 添加 .reveal 隐藏初始态,无 JS / 减弱动效时内容天然可见 */
if (!reduceMotion && 'IntersectionObserver' in window) {
const targets = document.querySelectorAll('.section-eyebrow, .section-title, .section-sub, .feature-card, .site-intro, .about-stats, .quick-links, .cta-band');
targets.forEach(function (el, i) {
el.classList.add('reveal');
el.style.transitionDelay = (i % 4) * 70 + 'ms';
});
const io = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
const el = entry.target;
el.classList.add('reveal-in');
io.unobserve(el);
/* 动画结束后移除类与延迟,把 transition 控制权交还 hover 等交互样式 */
setTimeout(function () {
el.classList.remove('reveal', 'reveal-in');
el.style.transitionDelay = '';
}, 900);
});
}, { threshold: 0.15, rootMargin: '0px 0px -8% 0px' });
targets.forEach(function (el) { io.observe(el); });
}
/* 特性卡片光标光斑:鼠标位置写入 CSS 变量,由 index.css 径向渐变消费(仅精确指针设备) */
if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
document.querySelectorAll('.feature-card').forEach(function (card) {
card.addEventListener('mousemove', function (e) {
const rect = card.getBoundingClientRect();
card.style.setProperty('--mx', (e.clientX - rect.left) + 'px');
card.style.setProperty('--my', (e.clientY - rect.top) + 'px');
});
});
}
})();
-153
View File
@@ -1,153 +0,0 @@
const msgTip = document.getElementById('msgTip');
const pwdTip = document.getElementById('pwdTip');
const agreeTip = document.getElementById('agreeTip');
const captchaTip = document.getElementById('captchaTip');
const form = document.getElementById('form');
const usernameInput = document.getElementById('loginUsername');
const passwordInput = document.getElementById('loginPassword');
const captchaInput = document.getElementById('captchaAnswer');
const captchaImg = document.getElementById('captchaImg');
const agreeBox = document.getElementById('agreeBox');
const resetUser = document.getElementById('resetUser');
const registerBtn = document.getElementById('registerBtn');
const loginButton = document.getElementById('loginBtn');
/* ── 图形验证码(gateway 代理 captcha 服务:GET /captcha/image) ── */
let captchaToken = '';
let captchaLoading = false; // 防重复点击:加载中忽略再次触发
async function refreshCaptcha() {
if (captchaLoading) return;
captchaLoading = true;
captchaToken = '';
try {
const res = await fetch('/captcha/image', { cache: 'no-store' });
if (res.status === 403) { // WAF 拦截:body 非图片,提示后待用户点击重试
captchaImg.removeAttribute('src');
captchaImg.alt = '验证码加载被安全规则拦截,点击重试';
return;
}
if (!res.ok) throw new Error('captcha load failed');
captchaToken = res.headers.get('X-Captcha-Token') || '';
const blob = await res.blob();
if (captchaImg.src && captchaImg.src.startsWith('blob:')) URL.revokeObjectURL(captchaImg.src);
captchaImg.src = URL.createObjectURL(blob);
captchaImg.alt = '点击刷新';
} catch (e) {
captchaImg.removeAttribute('src');
captchaImg.alt = '加载失败,点击重试';
} finally {
captchaLoading = false;
}
}
captchaImg.addEventListener('click', refreshCaptcha);
refreshCaptcha();
resetUser.addEventListener('click', function() { clearTipStyle(); });
function clearTipStyle() {
msgTip.innerHTML = '';
pwdTip.innerHTML = '';
agreeTip.innerHTML = '';
captchaTip.innerHTML = '';
msgTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
agreeTip.classList.remove('tip-red');
captchaTip.classList.remove('tip-red');
}
clearTipStyle();
function checkUsername() {
const result = FormValidators.username(usernameInput.value);
msgTip.textContent = result.message;
msgTip.className = result.className;
return result.valid ? result.value : false;
}
function checkPwd() {
const result = FormValidators.password(passwordInput.value);
pwdTip.textContent = result.message;
pwdTip.className = result.className;
return result.valid ? result.value : false;
}
function checkCaptcha() {
captchaTip.innerHTML = '';
captchaTip.classList.remove('tip-red');
const v = captchaInput.value.trim();
if (!v) {
captchaTip.innerHTML = '请输入验证码!';
captchaTip.classList.add('tip-red');
return false;
}
if (!captchaToken) {
captchaTip.innerHTML = '验证码未加载,请点击图片刷新';
captchaTip.classList.add('tip-red');
return false;
}
return v;
}
function checkAgree() {
agreeTip.innerHTML = '';
agreeTip.classList.remove('tip-red');
if (!agreeBox.checked) {
agreeTip.innerHTML = '请同意用户协议!';
agreeTip.classList.add('tip-red');
return false;
}
return true;
}
usernameInput.addEventListener('focus', checkUsername);
passwordInput.addEventListener('focus', checkPwd);
usernameInput.addEventListener('input', checkUsername);
passwordInput.addEventListener('input', checkPwd);
usernameInput.addEventListener('blur', checkUsername);
passwordInput.addEventListener('blur', checkPwd);
agreeBox.addEventListener('change', checkAgree);
registerBtn.addEventListener('click', function() { location.href = 'register.html'; });
form.addEventListener('submit', async function(e) {
e.preventDefault();
clearTipStyle();
const userOk = checkUsername();
const pwdOk = checkPwd();
const captchaOk = checkCaptcha();
const agreeOk = checkAgree();
if (!userOk || !pwdOk || !captchaOk || !agreeOk) return;
const jsonBody = JSON.stringify({
username: userOk,
password: pwdOk,
captcha_token: captchaToken,
captcha_answer: captchaOk
});
try {
loginButton.disabled = true;
const res = await fetch('/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: jsonBody
});
const responseText = await res.text();
if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE);
let result;
try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); }
if (!res.ok) throw new Error(result.message || '登录失败');
if (result.success) {
// Token 已由网关通过 HttpOnly Cookie 下发,JS 不再经手
alert('登录成功');
location.href = '/dnfs.html';
} else {
alert(result.message || '登录失败');
}
} catch (err) {
alert(err.message || '接口请求失败');
} finally {
// 无论成败都刷新验证码:Token 已被一次性消费,或需防重试
captchaInput.value = '';
refreshCaptcha();
loginButton.disabled = false;
}
});
File diff suppressed because it is too large. Load diff
-289
View File
@@ -1,289 +0,0 @@
const form = document.getElementById('form');
const emailInput = document.getElementById('regemail');
const codeInput = document.getElementById('code');
const getCodeButton = document.getElementById('getCode');
const passwordInput = document.getElementById('regpsw');
const repeatPasswordInput = document.getElementById('repwd');
const emlTip = document.getElementById('emlTip');
const pwdTip = document.getElementById('pwdTip');
const codeTip = document.getElementById('codeTip');
const captchaImg = document.getElementById('captchaImg');
const captchaInput = document.getElementById('captchaAnswer');
const captchaTip = document.getElementById('captchaTip');
const submitButton = document.getElementById('formsub');
let codeTarget = ''; // 已发送验证码的目标邮箱(验证码本体由服务端校验,前端不经手)
let cooldownTimer = null; // 重发倒计时(与服务端同邮箱 60s 发送冷却对齐)
/* ── 图形验证码(gateway 代理 captcha 服务:GET /captcha/image) ── */
let captchaToken = '';
let captchaLoading = false; // 防重复点击:加载中忽略再次触发
async function refreshCaptcha(preserveInput) {
if (captchaLoading) return;
captchaLoading = true;
captchaToken = '';
if (!preserveInput) captchaInput.value = ''; // 默认清空输入;preserveInput=true 时保留(获取验证码后)
captchaImg.style.opacity = '0.5'; // 视觉提示:正在刷新
try {
const res = await fetch('/captcha/image', { cache: 'no-store' });
if (res.status === 403) { // WAF 拦截:body 非图片,提示后待用户点击重试
captchaImg.removeAttribute('src');
captchaImg.alt = '验证码加载被安全规则拦截,点击重试';
return;
}
if (!res.ok) throw new Error('captcha load failed');
captchaToken = res.headers.get('X-Captcha-Token') || '';
const blob = await res.blob();
if (captchaImg.src && captchaImg.src.startsWith('blob:')) URL.revokeObjectURL(captchaImg.src);
captchaImg.src = URL.createObjectURL(blob);
captchaImg.alt = '点击刷新';
captchaImg.style.opacity = ''; // 恢复透明度
} catch (e) {
captchaImg.removeAttribute('src');
captchaImg.alt = '加载失败,点击重试';
} finally {
captchaLoading = false;
}
}
captchaImg.addEventListener('click', refreshCaptcha);
refreshCaptcha();
// 发送成功后进入倒计时,避免用户触发服务端同邮箱 60s 发送冷却
function startResendCooldown(seconds) {
clearInterval(cooldownTimer);
let remain = seconds;
getCodeButton.disabled = true;
getCodeButton.value = `${remain}s 后重发`;
cooldownTimer = setInterval(function () {
remain -= 1;
if (remain <= 0) {
clearInterval(cooldownTimer);
cooldownTimer = null;
getCodeButton.disabled = false;
getCodeButton.value = '获取验证码';
} else {
getCodeButton.value = `${remain}s 后重发`;
}
}, 1000);
}
form.addEventListener('reset', function () {
clearTipStyle();
codeTarget = '';
captchaInput.value = '';
refreshCaptcha();
});
function clearTipStyle() {
emlTip.innerHTML = '';
pwdTip.innerHTML = '';
codeTip.innerHTML = '';
captchaTip.innerHTML = '';
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
captchaTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
}
clearTipStyle();
function checkEmail() {
const result = FormValidators.email(emailInput.value);
emlTip.innerHTML = '';
emlTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
emlTip.textContent = result.message;
emlTip.classList.add(result.className);
return result.valid ? result.value : false;
}
function checkPassword() {
const result = FormValidators.password(passwordInput.value, 6);
pwdTip.innerHTML = '';
pwdTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
if (!result.valid) {
pwdTip.textContent = result.message;
pwdTip.classList.add(result.className);
return false;
}
if (result.value !== repeatPasswordInput.value) {
pwdTip.textContent = '两次输入的密码不一致!';
pwdTip.classList.add('tip-yellow');
return false;
}
pwdTip.textContent = result.message;
pwdTip.classList.add(result.className);
return result.value;
}
function checkCode() {
const code = codeInput.value.trim();
codeTip.innerHTML = '';
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
if (!codeTarget) {
codeTip.textContent = '请先获取验证码!';
codeTip.classList.add('tip-red');
return false;
}
if (emailInput.value.trim() !== codeTarget) {
codeTip.textContent = '邮箱已修改,请重新获取验证码!';
codeTip.classList.add('tip-red');
return false;
}
if (!/^\d{6}$/.test(code)) {
codeTip.textContent = '验证码必须是6位数字!';
codeTip.classList.add('tip-yellow');
return false;
}
// 验证码正确性由服务端在注册提交时校验(一次性消费)
return code;
}
function checkCaptcha() {
captchaTip.innerHTML = '';
captchaTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
const v = captchaInput.value.trim();
if (!v) {
captchaTip.textContent = '请输入图形验证码!';
captchaTip.classList.add('tip-red');
return false;
}
if (!captchaToken) {
captchaTip.textContent = '图形验证码未加载,请点击图片刷新';
captchaTip.classList.add('tip-red');
return false;
}
return v;
}
passwordInput.addEventListener('focus', checkPassword);
repeatPasswordInput.addEventListener('focus', checkPassword);
passwordInput.addEventListener('input', checkPassword);
repeatPasswordInput.addEventListener('input', checkPassword);
passwordInput.addEventListener('blur', checkPassword);
repeatPasswordInput.addEventListener('blur', checkPassword);
emailInput.addEventListener('focus', checkEmail);
emailInput.addEventListener('input', function () {
checkEmail();
if (codeTarget) {
checkCode();
}
});
emailInput.addEventListener('blur', checkEmail);
codeInput.addEventListener('focus', checkCode);
codeInput.addEventListener('input', checkCode);
codeInput.addEventListener('blur', checkCode);
getCodeButton.addEventListener('click', async function () {
if (getCodeButton.disabled) return; // 防重复点击
const email = checkEmail();
if (!email) return;
const captchaAnswer = checkCaptcha();
if (!captchaAnswer) return;
getCodeButton.disabled = true;
codeTip.textContent = '验证码发送中…';
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
codeTip.classList.add('tip-yellow');
let httpStatus = 0;
try {
const res = await fetch('/register/send_code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
captcha_token: captchaToken,
captcha_answer: captchaAnswer
})
});
httpStatus = res.status;
const responseText = await res.text();
if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE);
let result;
try { result = JSON.parse(responseText); } catch { throw new Error('服务器响应格式异常'); }
if (!res.ok || !result.success) throw new Error(result.message || '验证码发送失败');
codeTarget = email;
codeInput.value = '';
codeTip.textContent = '验证码已发送至邮箱,请查收(10 分钟内有效)';
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
codeTip.classList.add('tip-green');
startResendCooldown(60);
} catch (err) {
codeTip.textContent = err.message || '验证码发送失败,请稍后重试';
codeTip.classList.remove('tip-red', 'tip-yellow', 'tip-green');
codeTip.classList.add('tip-red');
if (httpStatus === 429) startResendCooldown(60); // 命中冷却/限流,倒计时对齐
} finally {
// CAPTCHA Token 一次性消费,无论成败都刷新图形验证码;
// 刷新时保留已输入的图形验证码答案,避免误清空用户输入
refreshCaptcha(true);
if (!cooldownTimer) getCodeButton.disabled = false;
}
});
form.addEventListener('submit', async function (e) {
e.preventDefault();
const email = checkEmail();
const password = checkPassword();
const code = checkCode();
if (!email || !password || !code) {
return;
}
const jsonBody = JSON.stringify({
username: email,
password,
email_code: code
});
try {
submitButton.disabled = true;
const res = await fetch('/register', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: jsonBody
});
const responseText = await res.text();
if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE);
let result;
try {
result = JSON.parse(responseText);
} catch {
console.error(`注册接口返回非 JSON 内容(HTTP ${res.status}):`, responseText);
throw new Error('服务器响应格式异常,请稍后重试');
}
if (!res.ok) {
throw new Error(result.message || `注册失败(HTTP ${res.status})`);
}
if (result.success) {
alert('注册成功');
form.reset();
codeTarget = '';
location.href = 'login.html';
return;
}
alert(result.message || '注册失败');
} catch (err) {
console.error('注册请求异常:', err);
alert(err.message || '接口请求失败,请检查后端服务是否启动');
} finally {
submitButton.disabled = false;
}
});
-119
View File
@@ -1,119 +0,0 @@
/**
* Token 生命周期管理器
* - 定时静默刷新:在 Token 80% 生命周期时提前调 /refresh_token
* - 全局 fetch 拦截器:收到 401 先尝试刷新一次,成功则重试原请求,失败则原样返回 401
* - 依赖:gateway POST /refresh_token(Cookie q4_token 自动携带)
* - 无侵入:与 dnfs.js 完全解耦
*/
let _tokenExpireAt = null;
let _refreshTimer = null;
const REFRESH_ADVANCE = 0.8; // 在 80% 生命周期时提前刷新
const REFRESH_MIN_GAP = 60; // 两次刷新最少间隔(秒)
const RETRY_COOLDOWN = 15; // 刷新失败后冷却期(秒)
/* ---------- 过期时间估算:通过 /whoami 获取 iat + keep_time ---------- */
async function estimateExpiry() {
try {
const res = await _rawFetch('/whoami', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
});
if (!res.ok) return false;
const data = await res.json();
if (!data.login) return false;
// 优先 created_at(刷新时更新),回退 iat(兼容旧 Token)
// 网关以 created_at + keep_time 判定令牌有效性,iat 刷新后不变,不能用于估算过期
const issueStr = data.created_at || data.iat;
const keepTime = data.keep_time;
if (!issueStr || !keepTime) return false;
const issueDate = new Date(issueStr.replace(' ', 'T') + 'Z');
if (isNaN(issueDate.getTime())) return false;
_tokenExpireAt = issueDate.getTime() + keepTime * 1000;
scheduleRefresh();
return true;
} catch (e) {
console.warn('TokenManager: 过期时间估算失败', e.message);
return false;
}
}
/* ---------- 定时刷新 ---------- */
function scheduleRefresh() {
clearTimeout(_refreshTimer);
if (!_tokenExpireAt) return;
const remainingSec = (_tokenExpireAt - Date.now()) / 1000;
if (remainingSec <= 0) {
// Token 已过期,立即尝试刷新
tryRefresh();
return;
}
// 80% 位置触发,但至少等 REFRESH_MIN_GAP 秒
const delaySec = Math.max(remainingSec * (1 - REFRESH_ADVANCE), REFRESH_MIN_GAP);
_refreshTimer = setTimeout(tryRefresh, delaySec * 1000);
}
async function tryRefresh() {
try {
const res = await _rawFetch('/refresh_token', { method: 'POST' });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
if (data.message) console.warn('TokenManager: 刷新被拒 -', data.message);
// 冷却后重新估算
_refreshTimer = setTimeout(() => estimateExpiry(), RETRY_COOLDOWN * 1000);
return;
}
const data = await res.json();
if (data.success && data.data) {
console.log(`TokenManager: 已刷新 (第${data.data.refresh_count}次, 剩余${data.data.refreshes_remaining}次)`);
}
await estimateExpiry();
} catch (e) {
console.warn('TokenManager: 刷新网络异常', e.message);
_refreshTimer = setTimeout(() => estimateExpiry(), RETRY_COOLDOWN * 1000);
}
}
/* ---------- 全局 fetch 401 拦截器 ---------- */
const _rawFetch = window.fetch;
window.fetch = async function (url, options) {
let resp = await _rawFetch(url, options);
// 仅处理 401;/refresh_token 自身不重试防循环
if (resp.status !== 401 || (typeof url === 'string' && url.includes('/refresh_token'))) {
return resp;
}
try {
const refreshRes = await _rawFetch('/refresh_token', { method: 'POST' });
if (!refreshRes.ok) return resp;
const data = await refreshRes.json();
if (!data.success) return resp;
console.log('TokenManager: 401 拦截 → 刷新成功,重试原请求');
await estimateExpiry();
// Cookie 已由 gateway Set-Cookie 自动更新,直接重试
return await _rawFetch(url, options);
} catch (e) {
console.warn('TokenManager: 401 刷新异常', e.message);
}
return resp;
};
/* ---------- 启动 ---------- */
estimateExpiry();
-82
View File
@@ -1,82 +0,0 @@
(function () {
function username(value) {
const normalizedValue = value.trim();
if (!normalizedValue) {
return { valid: false, message: '用户名不可为空', className: 'tip-red' };
}
if (normalizedValue.length > 20) {
return { valid: false, message: '用户名不得长于20字符!', className: 'tip-yellow' };
}
return {
valid: true,
value: normalizedValue,
message: '用户名格式正确',
className: 'tip-green'
};
}
function password(value, minLength) {
const requiredLength = minLength || 1;
if (!value) {
return { valid: false, message: '密码不可为空!', className: 'tip-red' };
}
if (value.length < requiredLength) {
return {
valid: false,
message: `密码至少${requiredLength}位字符!`,
className: 'tip-red'
};
}
return {
valid: true,
value,
message: '密码格式正确',
className: 'tip-green'
};
}
function email(value) {
const normalizedValue = value.trim();
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!normalizedValue) {
return { valid: false, message: '邮箱不可为空!', className: 'tip-red' };
}
if (!emailPattern.test(normalizedValue)) {
return { valid: false, message: '邮箱格式不正确!', className: 'tip-yellow' };
}
return {
valid: true,
value: normalizedValue,
message: '邮箱格式正确',
className: 'tip-green'
};
}
function note_title(value) {
return true;
}
function note_content(value) {
return true;
}
window.FormValidators = { username, password, email };
}());
/* WAF 拦截识别:waf 命中规则时返回 403 + 纯文本 "blocked by WAF"
* (见 waf/internal/proxy/proxy.go block());gateway 的 403(Target-Url
* 白名单拒绝)body 为 JSON 错误消息,不含此特征,不会误判。 */
window.WafTip = {
MESSAGE: '请求被 WAF 安全规则拦截,请检查输入内容',
isBlocked(res, responseText) {
return res.status === 403
&& typeof responseText === 'string'
&& responseText.indexOf('blocked by WAF') !== -1;
}
};
-190
View File
@@ -1,190 +0,0 @@
// ===== 路径解析:从 URL 取当前目录(去除首尾斜杠) =====
const currentPath = decodeURIComponent(window.location.pathname).replace(/^\/+|\/+$/g, '');
// ===== DOM 引用(id 与原版本完全一致,保持后端/前端契约不变) =====
const fileList = document.getElementById("fileList");
const loading = document.getElementById("loading");
const empty = document.getElementById("empty");
const searchInput = document.getElementById("searchInput");
const totalItems = document.getElementById("totalItems");
const totalDirs = document.getElementById("totalDirs");
const totalFiles = document.getElementById("totalFiles");
const totalVideos = document.getElementById("totalVideos");
const totalAudios = document.getElementById("totalAudios");
const btnRefresh = document.getElementById("btnRefresh");
const breadcrumb = document.getElementById("breadcrumb");
// ===== 状态 =====
let items = [];
let searchQuery = "";
// ===== 工具函数 =====
const SIZE_UNITS = ["B", "KB", "MB", "GB"];
function formatSize(bytes) {
if (bytes === 0) return "0B";
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(1) + SIZE_UNITS[i];
}
const FILE_ICONS = {
pdf: "📄", doc: "📝", docx: "📝", xls: "📊", xlsx: "📊",
zip: "📦", rar: "📦", "7z": "📦",
jpg: "🖼️", jpeg: "🖼️", png: "🖼️", gif: "🖼️",
txt: "📃", json: "📋", html: "🌐", css: "🎨", js: "⚡"
};
function getIcon(item) {
if (item.type === "dir") return "📁";
if (item.type === "video") return "🎬";
if (item.type === "audio") return "🎵";
const ext = item.name.split(".").pop().toLowerCase();
return FILE_ICONS[ext] || "📄";
}
// 转义文本,防止文件名含特殊字符导致 XSS 或模板断裂
function esc(str) {
return String(str)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
// ===== 导航行为(与原版本一致) =====
function navigate(path, type) {
if (type === "dir") window.location.href = "/" + path;
else if (type === "video" || type === "audio") window.location.href = "/video/" + path;
else if (/\.html?$/i.test(path)) window.open("/" + path, "_blank");
else window.location.href = "/" + path;
}
function playMedia(path) { window.location.href = "/video/" + path; }
function downloadFile(path) { window.location.href = "/" + path + "?download=true"; }
// ===== 面包屑 =====
function renderBreadcrumb() {
const parts = currentPath ? currentPath.split('/') : [];
const frags = ['<a href="/">根目录</a>'];
let acc = '';
parts.forEach((part, i) => {
if (i === parts.length - 1) {
frags.push(`<span class="sep">›</span><span class="current">${esc(part)}</span>`);
} else {
acc += (i === 0 ? '' : '/') + part;
frags.push(`<span class="sep">›</span><a href="/${encodeURIComponent(acc)}/">${esc(part)}</a>`);
}
});
breadcrumb.innerHTML = frags.join('');
}
// ===== 列表渲染(使用转义,避免文件名注入) =====
function render() {
const q = searchQuery.toLowerCase();
const filtered = searchQuery
? items.filter(i => i.name.toLowerCase().includes(q))
: items;
totalItems.textContent = filtered.length;
totalDirs.textContent = filtered.filter(i => i.type === "dir").length;
totalFiles.textContent = filtered.filter(i => i.type === "file").length;
totalVideos.textContent = filtered.filter(i => i.type === "video").length;
totalAudios.textContent = filtered.filter(i => i.type === "audio").length;
if (filtered.length === 0) {
fileList.innerHTML = "";
empty.style.display = "block";
loading.style.display = "none";
return;
}
empty.style.display = "none";
loading.style.display = "none";
fileList.innerHTML = filtered.map(item => {
const path = esc(item.path);
const type = esc(item.type);
const name = esc(item.name);
const sizeOrDir = item.type !== "dir"
? `<span>${formatSize(item.size)}</span>`
: '<span>📁 文件夹</span>';
const playBtn = (item.type === "video" || item.type === "audio")
? `<button class="btn-play" onclick="event.stopPropagation(); playMedia('${path}')">▶</button>`
: '';
return `
<div class="item" onclick="navigate('${path}', '${type}')">
<span class="icon">${getIcon(item)}</span>
<div class="info">
<div class="name">${name}</div>
<div class="meta">
${sizeOrDir}
<span>${esc(item.modified)}</span>
</div>
</div>
<div class="actions">
${playBtn}
<button class="btn-download" onclick="event.stopPropagation(); downloadFile('${path}')">⬇</button>
</div>
</div>`;
}).join("");
}
// ===== 数据拉取(接口与参数与原版本一致) =====
let isLoading = false; // 防重复点击:请求进行中忽略再次触发
async function loadData() {
if (isLoading) return;
isLoading = true;
loading.style.display = "block";
fileList.innerHTML = "";
empty.style.display = "none";
try {
const url = `/api/filelist?path=${encodeURIComponent(currentPath)}`;
const res = await fetch(url);
const responseText = await res.text();
if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE);
const data = JSON.parse(responseText);
if (data.success) {
items = data.items || [];
render();
} else {
throw new Error(data.error || "加载失败");
}
} catch (e) {
fileList.innerHTML = `<div class="error">❌ 加载失败: ${esc(e.message)}</div>`;
loading.style.display = "none";
} finally {
isLoading = false;
}
}
// ===== 事件绑定(快捷键 / 搜索 / 刷新 与原版本一致) =====
let searchTimer;
searchInput.addEventListener("input", function () {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
searchQuery = this.value.trim();
render();
}, 200);
});
document.addEventListener("keydown", function (e) {
if ((e.ctrlKey && e.key === "f") ||
(e.key === "/" && !e.ctrlKey && !e.metaKey)) {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
if (e.key === "Escape") {
searchInput.value = "";
searchQuery = "";
render();
searchInput.blur();
}
});
btnRefresh.addEventListener("click", function (e) {
e.preventDefault(); // <a href="#"> 默认会跳页顶,拦截
loadData();
});
// ===== 启动 =====
renderBreadcrumb();
loadData();
-86
View File
@@ -1,86 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>登录</title>
<link rel="stylesheet" href="css/theme.css">
</head>
<body>
<div class="card">
<h2>用户登录</h2>
<form id="form" novalidate>
<div>
<label>邮箱:</label>
<input type="text" id="loginUsername" placeholder="请输入邮箱">
<span id="msgTip"></span>
<label>密码:</label>
<input type="password" id="loginPassword" placeholder="请输入密码">
<span id="pwdTip"></span>
<label>验证码:</label>
<div style="display:flex;align-items:center;gap:8px">
<input type="text" id="captchaAnswer" placeholder="请输入验证码" maxlength="4" autocomplete="off" style="flex:1;min-width:0;width:auto">
<img id="captchaImg" alt="加载中…" title="点击刷新验证码" style="width:120px;height:38px;flex:none;cursor:pointer;border:1.5px solid var(--border);border-radius:10px;box-sizing:border-box">
</div>
<span id="captchaTip"></span>
</div>
<div class="form-actions">
<button type="button" id="registerBtn">注册</button>
<input type="submit" id="loginBtn" value="登录">
<input type="reset" id="resetUser" value="重置">
</div>
<div class="form-check">
<input type="checkbox" id="agreeBox" name="agree" value="access">
<label id="agreement" for="agreeBox">我已阅读并同意</label>
<a id="agreementLink" href="LICENSE.html" rel="noopener" style="color:var(--accent);font-size:14px;text-decoration:none;cursor:pointer">《用户使用条例》</a>
<span id="agreeTip"></span>
</div>
</form>
<a href="forgot.html" style="display:block;text-align:center;color:#999;font-size:13px;margin:8px 0">忘记密码?</a>
<div style="text-align:center;margin:12px 0;position:relative"><div style="position:absolute;top:50%;left:0;right:0;height:1px;background:#e0d6cc"></div><span style="background:#fff;padding:0 12px;position:relative;color:#999;font-size:13px">或</span></div>
<a href="https://yhdet.top/yhdet_auth?callback=https://qsont.me/yhdet_callback" class="yhdet-login-btn">开源社区 快捷登录</a>
<style>.yhdet-login-btn{display:block;width:100%;padding:12px;border:1.5px solid #1a73e8;border-radius:8px;background:white;color:#1a73e8;font-size:15px;font-weight:600;text-decoration:none;text-align:center;box-sizing:border-box;margin-bottom:12px}.yhdet-login-btn:hover{background:#e8f0fe}</style>
<!-- 用户使用条例弹窗 -->
<div id="licenseModal" class="license-modal-overlay" style="display:none">
<div class="license-modal-box">
<button id="licenseClose" class="license-modal-close" aria-label="关闭">&times;</button>
<iframe id="licenseFrame" src="about:blank" title="用户使用条例"></iframe>
</div>
</div>
<style>
.license-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;padding:16px;z-index:1000}
.license-modal-box{position:relative;width:100%;max-width:680px;max-height:86vh;background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,.25)}
.license-modal-box iframe{width:100%;height:86vh;border:0;display:block}
.license-modal-close{position:absolute;top:8px;right:10px;width:32px;height:32px;border:0;background:rgba(255,255,255,.9);border-radius:50%;font-size:22px;line-height:1;cursor:pointer;color:#555;z-index:2}
.license-modal-close:hover{background:#fff;color:#000}
</style>
</div>
<footer class="footer">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
</footer>
<script src="js/validation.js"></script>
<script src="js/login.js"></script>
<script>
(function(){
var link = document.getElementById('agreementLink');
var modal = document.getElementById('licenseModal');
var frame = document.getElementById('licenseFrame');
var close = document.getElementById('licenseClose');
function show(){
frame.src = 'LICENSE.html';
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
function hide(){
modal.style.display = 'none';
frame.src = 'about:blank';
document.body.style.overflow = '';
}
link.addEventListener('click', function(e){ e.preventDefault(); show(); });
close.addEventListener('click', hide);
modal.addEventListener('click', function(e){ if(e.target === modal) hide(); });
})();
</script>
</body>
</html>
View File
Whitespace-only changes.
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="data:;base64,=">
</head>
<body>
log
</body>
</html>
-35
View File
@@ -1,35 +0,0 @@
.top-bar-con {
width: 230px;
display: flex;
justify-content: space-between;
}
.newBtn,
.flushBtn,
.removeBtn {
width: 84px;
height: 42px;
font-size: 16px;
font-weight: bold;
letter-spacing: 1px;
cursor: pointer;
}
.newBtn {
background-color: #5cff007d;
}
.flushBtn {
background-color: #5757577d;
}
.removeBtn {
background-color: #ff00007d;
color: #000000b8;
}
td {
width: 180px;
text-align: center;
font-size: 20px;
}
-21
View File
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="module.css">
</head>
<body>
<div class="top-bar-con">
<button class="newBtn">新建</button>
<button class="flushBtn">刷新</button>
</div>
<p>模块列表需 mgmt Flask 后端 API 提供,静态页面不展示。</p>
</body>
</html>
-74
View File
@@ -1,74 +0,0 @@
.newBtn,
.flushBtn,
.stopBtn {
width: 84px;
height: 42px;
font-size: 16px;
font-weight: bold;
letter-spacing: 1px;
cursor: pointer;
}
.newBtn {
background-color: #5cff007d;
}
.flushBtn {
background-color: #5757577d;
margin-left: 60px;
}
.stopBtn {
background-color: #ff00007d;
}
.text,
.title {
font-size: 20px;
}
td,
th {
width: 180px;
text-align: center;
}
#inProgressText {
font-size: 20px;
margin-top: 5px;
letter-spacing: 2px;
}
/* modal window style */
.modal-con {
width: 100%;
height: 100%;
background-color: rgba(68, 94, 65, 0.77);
position: absolute;
left: 0px;
top: 0px;
display: none;
justify-content: center;
z-index: 1;
}
.modal-con .window {
width: 200px;
height: 120px;
background-color: #5cff007d;
padding: 0px 10px;
}
.modal-con .window .top-bar {
display: flex;
justify-content: end;
}
.modal-con .window .top-bar .close-btn {
cursor: pointer;
}
.modal-con .window .create-btn,
.modal-con .window .input .input-area {
margin-top: 5px;
}
-179
View File
@@ -1,179 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="process.css">
</head>
<body>
<!-- stop, new flush -->
<button class="newBtn" onclick="newBtn()">新建</button>
<button class="flushBtn" onclick="flushBtn()">刷新</button>
<table border="1">
<thead>
<tr>
<th class="title">PID</th>
<th class="title">状态</th>
<th class="title">命令</th>
<th class="title">操作</th>
</tr>
</thead>
<tbody id="tBody"></tbody>
</table>
<div id="inProgressText"></div>
<div class="modal-con" id="modal-con">
<div class="window">
<div class="top-bar">
<div class="close-btn" onclick="closeModal()">X</div>
</div>
<div class="input">
<div class="prompt">输入命令:</div>
<input id="input-area" class="input-area" type="text">
</div>
<button class="create-btn" onclick="createProcess()">创建</button>
</div>
</div>
<script>
// core 进程管理接口经 gateway 代理(Target-Url 指向 core:5200)
const CORE_TARGET = "http://127.0.0.1:5200";
function handleUnauthorized(res) {
if (res.status === 401) {
alert("未授权或管理员 Token 已失效,请重新登录");
setTimeout(() => { window.location.href = "../index.html"; }, 800);
return true;
}
return false;
}
async function api(path, body) {
const res = await fetch(path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Target-Url': CORE_TARGET
},
body: body ? JSON.stringify(body) : '{}'
});
if (handleUnauthorized(res)) return null;
const text = await res.text();
let result;
try {
result = JSON.parse(text);
} catch (e) {
console.error('接口返回非 JSON:', text);
throw new Error('服务器响应格式异常 (HTTP ' + res.status + ')');
}
if (!res.ok || !result.success) {
throw new Error(result.message || '请求失败 (HTTP ' + res.status + ')');
}
return result;
}
// 列表接口返回的 data,可能是数组,也可能是 ptree 序列化后的
// {"": {...}, "": {...}} 对象(boost property_tree 对数组的表示)
function toProcessArray(data) {
if (Array.isArray(data)) return data;
if (data && typeof data === "object") {
return Object.values(data);
}
return [];
}
async function loadProcesses() {
const tbody = document.getElementById("tBody");
const progress = document.getElementById("inProgressText");
tbody.innerHTML = "";
progress.innerText = "加载中...";
try {
const result = await api('/list');
const list = toProcessArray(result.data);
tbody.innerHTML = "";
for (let i = 0; i < list.length; i++) {
const p = list[i];
const tr = document.createElement("tr");
const pid = p.pid;
const status = p.running === true ? "运行中" : (p.running === false ? "已停止" : "未知");
tr.innerHTML = `
<td>${pid}</td>
<td>${status}</td>
<td>${p.command || "-"}</td>
<td><button class="stopBtn" onclick="stopBtn(${pid})">停止</button></td>
`;
tbody.appendChild(tr);
}
progress.innerText = "";
} catch (err) {
progress.innerText = "";
tbody.innerHTML = "";
alert(err.message || '加载进程列表失败');
}
}
function newBtn() {
const modal = document.getElementById("modal-con")
modal.style.display = "flex"
}
async function createProcess() {
const moduleName = document.getElementById("input-area").value.trim()
if (!moduleName) {
alert("请输入要执行的命令");
return;
}
const progress = document.getElementById("inProgressText");
progress.innerText = "启动中...";
try {
await api('/exec', { command: moduleName });
alert("启动成功");
closeModal();
await loadProcesses();
} catch (err) {
progress.innerText = "";
alert(err.message || '启动失败');
}
}
function flushBtn() {
loadProcesses();
}
async function stopBtn(pid) {
if (!confirm("确认停止该服务?")) {
return
}
const progress = document.getElementById("inProgressText");
progress.innerText = "停止中...";
try {
await api('/kill', { pid: pid });
alert("已停止");
await loadProcesses();
} catch (err) {
progress.innerText = "";
alert(err.message || '停止失败');
}
}
function closeModal() {
const modal = document.getElementById("modal-con")
const input = document.getElementById("input-area")
input.value = ""
modal.style.display = "none"
}
// 页面加载时自动拉取进程列表
window.onload = () => {
loadProcesses();
};
</script>
</body>
</html>
-41
View File
@@ -1,41 +0,0 @@
.top-bar-con {
width: 230px;
display: flex;
justify-content: space-between;
}
td {
width: 180px;
text-align: center;
font-size: 20px;
}
.addUserBtn,
.flushBtn,
.changePasswdBtn,
.deleteUserBtn {
width: 84px;
height: 42px;
font-size: 16px;
font-weight: bold;
letter-spacing: 1px;
cursor: pointer;
}
.addUserBtn {
background-color: #5cff007d;
}
.flushBtn {
background-color: #5757577d;
}
.changePasswdBtn {
background-color: #e6db407d;
color: #fb0202b0;
}
.deleteUserBtn {
background-color: #ff00007d;
color: #000000b8;
}
-39
View File
@@ -1,39 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="user.css">
</head>
<body>
<div class="top-bar-con">
<button class="addUserBtn" onclick="addUser()">添加用户</button>
<button class="flushBtn" onclick="flush()">刷新</button>
</div>
<p>用户列表需 mgmt Flask 后端 API 提供,静态页面不展示。</p>
<script>
function addUser() {
alert("注意:这是静态页面 - 添加用户需要后端 API 支持,请通过 mgmt Flask 后端访问");
}
function changePasswd(id) {
alert("注意:这是静态页面 - 修改密码需要后端 API 支持,请通过 mgmt Flask 后端访问");
}
function deleteUser(id) {
alert("注意:这是静态页面 - 删除用户需要后端 API 支持,请通过 mgmt Flask 后端访问");
}
function flush() {
window.location.reload()
}
</script>
</body>
</html>
-39
View File
@@ -1,39 +0,0 @@
body {
display: flex;
justify-content: center;
}
.main {
background-color: rgb(233, 233, 233);
padding: 20px 10px;
}
.main .title {
font-weight: bold;
}
.main .name-con,
.main .passwd-con {
display: flex;
justify-content: center;
}
.main .name-con {
margin-top: 20px;
}
.main .passwd-con {
margin-top: 10px;
}
.main .name-con .user-name-text,
.main .passwd-con .user-passwd-text {
width: 100px;
}
.main .login-con {
width: 100%;
display: flex;
justify-content: center;
margin-top: 10px;
}
-88
View File
@@ -1,88 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>pmc management</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="index.css">
</head>
<body>
<div class="main">
<div class="title">Welcome to pmc management system</div>
<div class="name-con">
<div class="user-name-text">Name: </div>
<input class="user-name-input" id="user-name" type="text">
</div>
<div class="passwd-con">
<div class="user-passwd-text">Password: </div>
<input class="user-passwd-input" id="user-passwd" type="password">
</div>
<div class="login-con">
<button onclick="login()">login</button>
</div>
</div>
<script>
function login() {
const name = document.getElementById("user-name").value
const passwd = document.getElementById("user-passwd").value
const request = new XMLHttpRequest()
const REQUEST_URL = "/api/login"
// if user not input any information
if (name === "" || passwd === "") {
alert("please input name or passwd")
return
}
// request api
let requestData = new URLSearchParams()
requestData.append("user-name", name)
requestData.append("user-passwd", passwd)
request.open("post", REQUEST_URL, true)
request.send(requestData)
request.onload = () => {
if (request.status === 401) {
alert(request.responseText)
return
}
if (request.status === 404) {
alert(request.responseText)
return
}
if (request.status === 500) {
alert("服务端错误")
return
}
if (request.status === 200 && request.readyState === 4) {
const token = JSON.parse(request.responseText).token
let date = new Date()
date.setTime(date.getTime() + (3 * 24 * 60 * 60 * 1000))
const UTCTime = date.toUTCString()
document.cookie = `token=${token}; path=/; expires=${UTCTime}`
window.location = JSON.parse(request.responseText).redirect;
return
} else {
alert(`意外错误 (${request.status})`)
return
}
}
}
document.addEventListener("keydown", (e) => {
if (e.key != "Enter") {
return
}
login()
})
</script>
</body>
</html>
-54
View File
@@ -1,54 +0,0 @@
:root {
--listMargin: 15%;
}
* {
margin: 0px;
padding: 0px;
}
.top-con {
display: flex;
}
.top-con >*{
margin-left: 25px;
font-size: 20px;
}
.top-con div button {
font-size: 16px;
}
.main {
width: 100%;
height: 600px;
display: flex;
}
.feature-list {
width: 150px;
height: 100%;
background-color: #FFFFCC;
padding: 0px 10px 0px 10px;
overflow-y: scroll;
}
.feature-list .item {
width: 100%;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
background-color: #0066cc;
color: #fff;
font-size: 18px;
font-weight: bold;
margin-top: var(--listMargin);
cursor: pointer;
}
.pages {
width: 100%;
height: 100%;
}
-119
View File
@@ -1,119 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>management page</title>
<link rel="icon" href="data:;base64,=">
<link rel="stylesheet" href="mgmt.css">
<script>
async function loadUserInfo() {
try {
const res = await fetch('/whoami', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: '{}'
});
const data = await res.json();
if (data.login && data.user) {
document.getElementById("current-user").innerText = "当前用户: " + data.user;
document.getElementById("current-role").innerText = data.is_admin ? "当前角色: 管理员" : "当前角色: 普通用户";
if (!data.is_admin) {
alert("需要管理员权限才能访问此页面");
}
} else {
document.getElementById("current-user").innerText = "当前用户: 未登录";
document.getElementById("current-role").innerText = "当前角色: 未知";
setTimeout(() => {
window.location = "index.html";
}, 1000);
}
} catch (err) {
console.error('获取用户信息失败:', err);
document.getElementById("current-user").innerText = "当前用户: 未知";
document.getElementById("current-role").innerText = "当前角色: 未知";
}
}
window.onload = () => {
loadUserInfo();
const featureList = [
{
name: "进程",
url: "features/process.html"
},
{
name: "模块",
url: "features/module.html"
},
// {
// name: "日志",
// url: "features/log.html"
// },
{
name: "用户",
url: "features/user.html"
}
]
const featureListEle = document.getElementById("feature-list")
for (let i = 0; i < featureList.length; i++) {
// get name and url
const name = featureList[i].name
const url = featureList[i].url
// Create a item
let listItemEle = document.createElement("div")
listItemEle.setAttribute("class", "item")
listItemEle.innerText = name
listItemEle.setAttribute("onclick", `switchPage('${url}')`)
// append element into feature list
featureListEle.append(listItemEle)
}
// set default page in iframe
const DEFAULT_PAGE = "<!DOCTYPE html><h1>请在左侧选择功能</h1>"
document.getElementById("page").contentDocument.write(DEFAULT_PAGE)
}
</script>
</head>
<body>
<div class="top-con">
<div id="current-user">当前用户: 未登录</div>
<div id="current-role">当前角色: 未知</div>
<div class="button-con">
<button onclick="logout()">退出登陆</button>
</div>
</div>
<div class="main">
<div class="feature-list" id="feature-list"></div>
<div class="pages">
<iframe id="page" src="" frameborder="1" width="100%" height="100%"></iframe>
</div>
</div>
<script>
// change the iframe's url
function switchPage(url) {
const page = document.getElementById("page")
page.setAttribute("src", url)
}
function logout() {
if (!confirm("确认退出?")) {
return
}
document.cookie = "token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
window.location = "index.html"
}
</script>
</body>
</html>
-48
View File
@@ -1,48 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>注册</title>
<link rel="stylesheet" href="css/theme.css">
<link rel="stylesheet" href="css/auth.css">
</head>
<body>
<div class="card">
<p class="auth-eyebrow">DNFS 树状笔记</p>
<h2>注册页面</h2>
<form id="form">
<div>
<label>邮箱号:</label>
<input type="email" id="regemail" placeholder="请输入邮箱号">
<span id="emlTip"></span>
<div class="code-box">图形验证码:
<input type="text" id="captchaAnswer" placeholder="请输入图形验证码" maxlength="4" autocomplete="off">
<img id="captchaImg" class="captcha-img" alt="加载中…" title="点击刷新验证码">
<span id="captchaTip"></span>
</div>
<div class="code-box">邮箱验证码:
<input type="text" id="code" placeholder="请输入6位验证码" inputmode="numeric" maxlength="6">
<input type="button" id="getCode" value="获取验证码">
<span id="codeTip"></span>
</div>
<label>密码:</label>
<input type="password" id="regpsw" placeholder="密码至少6位">
<label>验证密码:</label>
<input type="password" id="repwd" placeholder="请再次输入密码">
<span id="pwdTip"></span>
</div>
<div class="form-actions">
<input id="formsub" type="submit" value="提交">
<input id="resetUser" type="reset" value="重置">
<input type="button" value="返回登录" onclick="location.href='login.html'">
</div>
</form>
</div>
<footer class="footer">
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">蜀ICP备2024107064号-2</a>
</footer>
<script src="js/validation.js"></script>
<script src="js/register.js?v=7"></script>
</body>
</html>