update: 2026-03-28 20:59

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
<div id="life-countdown" style="border: 1px solid #ddd; padding: 10px; border-radius: 5px;">
<h3 class="colorful-text" style="text-align: center;">人生倒计时</h3>
<div class="countdown-section" style="margin: 10px 0;">
<div class="countdown-text" style="color: red;">今天已过去 <span id="hours-today"></span> 小时</div>
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
<div id="bar-today" class="progress" style="background: #f39c12;"></div>
</div>
</div>
<div class="countdown-section" style="margin: 10px 0;">
<div class="countdown-text" style="color: orange;">本周已过去 <span id="days-week"></span></div>
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
<div id="bar-week" class="progress" style="background: #3498db;"></div>
</div>
</div>
<div class="countdown-section" style="margin: 10px 0;">
<div class="countdown-text" style="color: yellow;">本月已过去 <span id="days-month"></span></div>
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
<div id="bar-month" class="progress" style="background: #2ecc71;"></div>
</div>
</div>
<div class="countdown-section" style="margin: 10px 0;">
<div class="countdown-text" style="color: green;">今年已过去 <span id="days-year"></span></div>
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
<div id="bar-year" class="progress" style="background: #9b59b6;"></div>
</div>
</div>
<div class="countdown-section" style="margin: 10px 0;">
<div class="countdown-text" style="color: blue;">距离春节还有 <span id="days-chunjie"></span></div>
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
<div id="bar-chunjie" class="progress" style="background: #2980b9;"></div>
</div>
</div>
<div class="countdown-section" style="margin: 10px 0;">
<div class="countdown-text" style="color: purple;">距离我的生日还有 <span id="days-birthday"></span></div>
<div class="progress-bar" style="background: #ddd; border-radius: 10px; overflow: hidden;">
<div id="bar-birthday" class="progress" style="background: #e74c3c;"></div>
</div>
</div>
</div>
<style>
.colorful-text {
font-size: 28px;
font-weight: bold;
color: #ff6347; /* 可以根据需要调整颜色 */
}
.countdown-text {
font-size: 18px;
font-weight: bold;
margin-bottom: 5px;
}
.progress {
height: 20px;
transition: width 1s ease-in-out;
}
.progress-bar {
border-radius: 10px;
overflow: hidden;
}
</style>
<script>
function updateCountdown() {
const now = new Date();
// 今天已过去的小时数
const hoursToday = now.getHours();
document.getElementById('hours-today').innerText = hoursToday;
document.getElementById('bar-today').style.width = (hoursToday / 24 * 100) + '%';
// 本周已过去的天数
const daysWeek = now.getDay();
document.getElementById('days-week').innerText = daysWeek;
document.getElementById('bar-week').style.width = (daysWeek / 7 * 100) + '%';
// 本月已过去的天数
const daysMonth = now.getDate();
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
document.getElementById('days-month').innerText = daysMonth;
document.getElementById('bar-month').style.width = (daysMonth / daysInMonth * 100) + '%';
// 今年已过去的天数
const start = new Date(now.getFullYear(), 0, 0);
const diff = now - start;
const oneDay = 1000 * 60 * 60 * 24;
const daysYear = Math.floor(diff / oneDay);
document.getElementById('days-year').innerText = daysYear;
document.getElementById('bar-year').style.width = (daysYear / 365 * 100) + '%';
// 离春节还有多少天2025年1月29日
const chunjieDate = new Date(2025, 0, 29); // 春节日期假设为2025年1月29日
const diffChunjie = chunjieDate - now;
const daysChunjie = Math.floor(diffChunjie / oneDay);
document.getElementById('days-chunjie').innerText = daysChunjie;
document.getElementById('bar-chunjie').style.width = ((365 - daysChunjie) / 365 * 100) + '%';
// 离生日还有多少天每年的10月25日
let birthdayDate = new Date(now.getFullYear(), 9, 25);
if (now > birthdayDate) {
birthdayDate = new Date(now.getFullYear() + 1, 9, 25);
}
const diffBirthday = birthdayDate - now;
const daysBirthday = Math.floor(diffBirthday / (1000 * 60 * 60 * 24));
document.getElementById('days-birthday').innerText = daysBirthday;
document.getElementById('bar-birthday').style.width = ((365 - daysBirthday) / 365 * 100) + '%';
}
updateCountdown();
setInterval(updateCountdown, 3600000); // 每小时更新一次
</script>

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>飞雪前端艺术</title>
<style>
* {
padding: 0;
margin: 0;
}
body {
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// 获取 canvas 元素的宽度和高度,并将宽度和高度设置为屏幕的可用宽度和高度。
const width = document.getElementById("canvas").width = screen.availWidth;
const height = document.getElementById("canvas").height = screen.availHeight;
// 获取 canvas 的绘图上下文
const ctx = document.getElementById("canvas").getContext("2d");
// 创建一个大小为 width/10 的数组并填充为 0
const arr = Array(Math.ceil(width / 10)).fill(0);
// 创建一个字符串数组,用于存储字符。
const str = "✧︎︎︎︎︎︎♾♲✰︎✦︎☭︎︎︎︎✵︎︎⚘︎✞︎♘♞☆︎★☼︎☾◎☽︎Ω℞№︎❂❁︎✣✶✺✷◦◉⦿☒✗☐☞◇☛︎︎⌘✘︎".split("");
ctx.font = "10px 优设标题黑";
function rain() {
// 设置颜色,并绘制一个全屏的矩形
ctx.fillStyle = "rgba(0,0,20,0.05)";
ctx.fillRect(0, 0, width, height);
// 设置文字的颜色
ctx.fillStyle = '#00c8aa';
arr.forEach(function (value, index) {
// 根据数组的索引值来绘制文字x 坐标为索引值 * 10y 坐标为 value + 10。
ctx.fillText(str[Math.floor(Math.random() * str.length)], index * 10, value + 10);
// 从上一次绘制的位置开始,将数组值设置为下一次绘制位置。
arr[index] = value >= height || value > 8888 * Math.random() ? 0 : value + 10;
});
}
// 每 30 毫秒执行一次 rain() 函数。
setInterval(rain, 30);
</script>
</body>
</html>

View File

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

View File

@@ -0,0 +1,509 @@
<!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" />
<title>圣诞快乐</title>
<style>
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; overflow: hidden; background: #000; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, PingFangSC, "Microsoft YaHei", sans-serif; }
#app { position: relative; width: 100vw; height: 100vh; }
#canvas-container { position: absolute; inset: 0; }
.barrage { position: fixed; inset: 0; pointer-events: none; z-index: 9; }
.barrage-item { position: fixed; left: 100vw; white-space: nowrap; font-weight: 700; text-shadow: 0 0 8px rgba(255,255,255,0.35), 0 0 16px rgba(255,255,255,0.2); filter: drop-shadow(0 2px 4px rgba(0,0,0,0.35)); animation-name: fly; animation-timing-function: linear; animation-fill-mode: forwards; }
@keyframes fly { 0% { transform: translateX(0); } 100% { transform: translateX(-120vw); } }
.fullscreen-btn { position: fixed; right: 14px; bottom: 14px; z-index: 11; padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.12); color: #fff; font-weight: 600; letter-spacing: 0.5px; backdrop-filter: blur(4px); cursor: pointer; }
.fullscreen-btn:hover { background: rgba(255,255,255,0.18); }
@media (max-width: 768px) { .fullscreen-btn { padding: 9px 12px; right: 10px; bottom: 10px; font-size: 13px; } }
@media (max-width: 768px) {
.barrage-item { font-size: clamp(14px, 3.5vw, 20px); }
}
@media (min-width: 769px) {
.barrage-item { font-size: clamp(16px, 2.1vw, 28px); }
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module">
import { createApp, ref, onMounted, h } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
import * as THREE from 'https://unpkg.com/three@0.164.1/build/three.module.js';
const App = {
setup() {
const isFs = ref(false);
const barrageItems = ref([]);
const containerEl = ref(null);
let renderer, scene, camera, group;
let geoLeaves, matLeaves, pointsLeaves, posLeaves, colLeaves, velLeaves, tgtLeaves, startMorphLeaves;
let geoOrnaments, matOrnaments, pointsOrnaments, posOrnaments, colOrnaments, velOrnaments, tgtOrnaments, startMorphOrnaments;
let geoTrunk, matTrunk, pointsTrunk, posTrunk, colTrunk, velTrunk, tgtTrunk, startMorphTrunk;
let geoStar, matStar, pointsStar, posStar, colStar, velStar, tgtStar, startMorphStar;
let geoAccents, matAccents, pointsAccents, posAccents, colAccents, velAccents, tgtAccents, startMorphAccents;
let snowGroup, snowSprites, snowVel, snowRot, snowScale, snowOpacityPhase;
let startTime = 0;
const disperseDuration = 3500;
const morphDuration = 3000;
const particleCountLeaves = 5400;
const particleCountOrnaments = 400;
const particleCountTrunk = 1200;
const particleCountStar = 320;
const particleCountAccents = 160;
const snowCount = 200;
const treeHeight = 14;
const baseRadius = 6.2;
let running = true;
const topY = treeHeight + 1.2;
const bottomY = -1.4;
const messages = [
'圣诞快乐', '平安喜乐', '万事胜意', '心想事成', '前程似锦', '阖家幸福',
'福星高照', '岁岁常欢愉', '诸事顺遂', '新年好运常在', '健康平安', '幸福常伴'
];
function randomHsl() {
const h = Math.floor(Math.random() * 360);
const s = 68 + Math.random() * 22;
const l = 50 + Math.random() * 10;
return `hsl(${h}deg, ${s}%, ${l}%)`;
}
function createSnowflakeTexture() {
const c = document.createElement('canvas');
c.width = 128; c.height = 128;
const ctx = c.getContext('2d');
ctx.clearRect(0, 0, 128, 128);
ctx.translate(64, 64);
ctx.strokeStyle = 'rgba(255,255,255,0.95)';
ctx.lineWidth = 2;
for (let i = 0; i < 6; i++) {
const ang = (Math.PI * 2 / 6) * i;
const x = Math.cos(ang) * 48;
const y = Math.sin(ang) * 48;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(x, y);
ctx.stroke();
for (let b = 1; b <= 3; b++) {
const t = b / 4;
const bx = Math.cos(ang) * 48 * t;
const by = Math.sin(ang) * 48 * t;
const sideAng = ang + Math.PI / 2;
const len = 10 * (1 - t);
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo(bx + Math.cos(sideAng) * len, by + Math.sin(sideAng) * len);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo(bx - Math.cos(sideAng) * len, by - Math.sin(sideAng) * len);
ctx.stroke();
}
}
const tex = new THREE.CanvasTexture(c);
tex.minFilter = THREE.LinearMipmapLinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.needsUpdate = true;
return tex;
}
function createSnowGlowTexture() {
const c = document.createElement('canvas');
c.width = 128; c.height = 128;
const ctx = c.getContext('2d');
const grd = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
grd.addColorStop(0, 'rgba(255,255,255,0.57)');
grd.addColorStop(0.4, 'rgba(255,255,255,0.23)');
grd.addColorStop(1, 'rgba(255,255,255,0.0)');
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 128, 128);
const tex = new THREE.CanvasTexture(c);
tex.minFilter = THREE.LinearMipmapLinearFilter;
tex.magFilter = THREE.LinearFilter;
tex.needsUpdate = true;
return tex;
}
function spawnBarrage() {
const count = 2 + Math.floor(Math.random() * 3);
const now = Date.now();
for (let i = 0; i < count; i++) {
const text = messages[Math.floor(Math.random() * messages.length)];
const top = Math.random() * 90 + 5;
const duration = 10 + Math.random() * 8;
const color = randomHsl();
const id = `${now}-${i}-${Math.random().toString(36).slice(2,7)}`;
barrageItems.value.push({ id, text, top: `${top}%`, duration: `${duration}s`, color, shadow: color });
setTimeout(() => {
const idx = barrageItems.value.findIndex(x => x.id === id);
if (idx >= 0) barrageItems.value.splice(idx, 1);
}, duration * 1000 + 400);
}
}
function enterFs() {
const el = document.getElementById('app') || containerEl.value || document.documentElement;
const rfs = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;
if (rfs) rfs.call(el);
}
function exitFs() {
const efs = document.exitFullscreen || document.webkitExitFullscreen || document.msExitFullscreen;
if (efs) efs.call(document);
}
function toggleFs() { if (document.fullscreenElement) exitFs(); else enterFs(); }
function computeLeafTargets(count) {
const arr = new Float32Array(count * 3);
const layers = 12;
for (let i = 0; i < count; i++) {
const layer = Math.floor(Math.random() * layers);
const yBase = (layer / (layers - 1)) * treeHeight;
const y = yBase + (Math.random() - 0.5) * (treeHeight * 0.035 + (1 - layer / layers) * 0.12);
const t = y / treeHeight;
const shelf = 0.45 * Math.sin(3.5 * (1 - t));
const wobble = 0.25 * Math.sin(8 * t + Math.random() * 0.8);
const r = Math.max(0.04, (baseRadius * (1 - t)) + shelf + wobble);
const a = Math.random() * Math.PI * 2;
const x = r * Math.cos(a);
const z = r * Math.sin(a);
arr[i * 3] = x;
arr[i * 3 + 1] = y;
arr[i * 3 + 2] = z;
}
return arr;
}
function computeTrunkTargets(count) {
const arr = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const ty = -1 + Math.random() * (treeHeight * 0.22);
const tr = 0.35 + Math.random() * 0.22;
const ta = Math.random() * Math.PI * 2;
arr[i * 3] = tr * Math.cos(ta);
arr[i * 3 + 1] = ty;
arr[i * 3 + 2] = tr * Math.sin(ta);
}
return arr;
}
function computeOrnamentTargets(count) {
const arr = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const y = Math.random() * treeHeight * 0.95;
const t = y / treeHeight;
const r = Math.max(0.05, (baseRadius * (1 - t)) + 0.3 * Math.sin(7 * t + Math.random()));
const a = Math.random() * Math.PI * 2;
const x = r * Math.cos(a);
const z = r * Math.sin(a);
arr[i * 3] = x;
arr[i * 3 + 1] = y;
arr[i * 3 + 2] = z;
}
return arr;
}
function computeStarTargets(count) {
const arr = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const a = Math.random() * Math.PI * 2;
const rBase = 0.75;
const r = rBase + 0.25 * Math.cos(5 * a) + (Math.random() - 0.5) * 0.12;
const x = r * Math.cos(a);
const z = r * Math.sin(a);
const y = treeHeight + 0.9 + (Math.random() - 0.5) * 0.35;
arr[i * 3] = x;
arr[i * 3 + 1] = y;
arr[i * 3 + 2] = z;
}
return arr;
}
function computeAccentsTargets(count) {
const arr = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const y = Math.random() * treeHeight;
const t = y / treeHeight;
const r = Math.max(0.05, (baseRadius * (1 - t)) + 0.35 * Math.sin(6 * t + Math.random()));
const a = Math.random() * Math.PI * 2;
const x = r * Math.cos(a);
const z = r * Math.sin(a);
arr[i * 3] = x;
arr[i * 3 + 1] = y;
arr[i * 3 + 2] = z;
}
return arr;
}
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function ornamentColors(index) {
const palette = [
new THREE.Color(0xff4d6d), new THREE.Color(0xffb703), new THREE.Color(0x32c3ff),
new THREE.Color(0x8ce99a), new THREE.Color(0xb197fc), new THREE.Color(0xffaad4)
];
return palette[index % palette.length];
}
function initSet(count, size, colorFn, computeTargets) {
const geo = new THREE.BufferGeometry();
const pos = new Float32Array(count * 3);
const col = new Float32Array(count * 3);
const vel = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
const ix = i * 3;
pos[ix] = (Math.random() - 0.5) * 0.4;
pos[ix + 1] = (Math.random() - 0.5) * 0.4;
pos[ix + 2] = (Math.random() - 0.5) * 0.4;
let vx = (Math.random() - 0.5);
let vy = (Math.random() - 0.5) * 0.6;
let vz = (Math.random() - 0.5);
const s = 0.012 + Math.random() * 0.018;
vel[ix] = vx * s;
vel[ix + 1] = vy * s;
vel[ix + 2] = vz * s;
const c = colorFn(i);
col[ix] = c.r;
col[ix + 1] = c.g;
col[ix + 2] = c.b;
}
geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
geo.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
const mat = new THREE.PointsMaterial({ size, sizeAttenuation: true, transparent: true, opacity: 0.95, vertexColors: true, blending: THREE.AdditiveBlending, depthWrite: false });
const points = new THREE.Points(geo, mat);
const targets = computeTargets(count);
group.add(points);
return { geo, pos, col, vel, mat, points, targets };
}
function initThree() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x06101a, 0.035);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 6.5, 20);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
containerEl.value.appendChild(renderer.domElement);
group = new THREE.Group();
scene.add(group);
const ambient = new THREE.AmbientLight(0xffffff, 0.35);
scene.add(ambient);
const point = new THREE.PointLight(0xfff3e0, 3.2, 100);
point.position.set(4, treeHeight + 2, 6);
scene.add(point);
({ geo: geoLeaves, pos: posLeaves, col: colLeaves, vel: velLeaves, mat: matLeaves, points: pointsLeaves, targets: tgtLeaves } = initSet(
particleCountLeaves,
0.12,
() => {
const g = 0.35 + Math.random() * 0.65;
const r = 0.2 + 0.8 * Math.random();
const green = 0.5 + 0.5 * Math.random();
return new THREE.Color(r * g, green * g, 0.2 * g);
},
computeLeafTargets
));
({ geo: geoOrnaments, pos: posOrnaments, col: colOrnaments, vel: velOrnaments, mat: matOrnaments, points: pointsOrnaments, targets: tgtOrnaments } = initSet(
particleCountOrnaments,
0.14,
(i) => ornamentColors(i),
computeOrnamentTargets
));
({ geo: geoTrunk, pos: posTrunk, col: colTrunk, vel: velTrunk, mat: matTrunk, points: pointsTrunk, targets: tgtTrunk } = initSet(
particleCountTrunk,
0.12,
() => new THREE.Color(0.36, 0.24, 0.18),
computeTrunkTargets
));
({ geo: geoStar, pos: posStar, col: colStar, vel: velStar, mat: matStar, points: pointsStar, targets: tgtStar } = initSet(
particleCountStar,
0.12,
() => new THREE.Color(1.0, 0.83, 0.3),
computeStarTargets
));
({ geo: geoAccents, pos: posAccents, col: colAccents, vel: velAccents, mat: matAccents, points: pointsAccents, targets: tgtAccents } = initSet(
particleCountAccents,
0.22,
(i) => ornamentColors(i + 3),
computeAccentsTargets
));
const snowTex = createSnowflakeTexture();
const glowTex = createSnowGlowTexture();
snowGroup = new THREE.Group();
snowGroup.renderOrder = 0;
scene.add(snowGroup);
snowSprites = new Array(snowCount);
const snowHalos = new Array(snowCount);
snowVel = new Float32Array(snowCount * 2);
snowRot = new Float32Array(snowCount);
snowScale = new Float32Array(snowCount);
snowOpacityPhase = new Float32Array(snowCount);
for (let i = 0; i < snowCount; i++) {
const mat = new THREE.SpriteMaterial({ map: snowTex, color: 0xffffff, transparent: true, opacity: 1.0, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, fog: false });
const spr = new THREE.Sprite(mat);
const s = (1.6 + Math.random() * 1.6) * (2/3);
spr.scale.set(s, s, 1);
const x = (Math.random() - 0.5) * 120;
const y = 20 + Math.random() * 40;
const z = -22 - Math.random() * 12;
spr.position.set(x, y, z);
snowScale[i] = s;
snowVel[i * 2] = (Math.random() - 0.5) * 0.006;
snowVel[i * 2 + 1] = -0.008 - Math.random() * 0.01;
snowRot[i] = (-0.004 + Math.random() * 0.008);
snowOpacityPhase[i] = Math.random() * Math.PI * 2;
snowSprites[i] = spr;
snowGroup.add(spr);
const haloMat = new THREE.SpriteMaterial({ map: glowTex, color: 0xffffff, transparent: true, opacity: 0.6, depthWrite: false, depthTest: false, blending: THREE.AdditiveBlending, fog: false });
const halo = new THREE.Sprite(haloMat);
halo.scale.set(s * 2.6, s * 2.6, 1);
halo.position.copy(spr.position);
snowHalos[i] = halo;
snowGroup.add(halo);
}
pointsLeaves.renderOrder = 1;
pointsOrnaments.renderOrder = 1;
pointsTrunk.renderOrder = 1;
pointsStar.renderOrder = 1;
pointsAccents.renderOrder = 1;
fitCamera();
startTime = performance.now();
animate();
window.addEventListener('resize', onResize);
}
function animate() {
if (!running) return;
const now = performance.now();
const elapsed = now - startTime;
function updateSet(geo, posArr, velArr, startMorphArr, targetsArr, count) {
const attr = geo.getAttribute('position');
const arr = attr.array;
if (elapsed < disperseDuration) {
const dt = 16;
for (let i = 0; i < count; i++) {
const ix = i * 3;
arr[ix] += velArr[ix] * dt;
arr[ix + 1] += velArr[ix + 1] * dt;
arr[ix + 2] += velArr[ix + 2] * dt;
}
} else {
if (!startMorphArr) {
startMorphArr = Float32Array.from(arr);
if (geo === geoLeaves) startMorphLeaves = startMorphArr;
if (geo === geoOrnaments) startMorphOrnaments = startMorphArr;
if (geo === geoTrunk) startMorphTrunk = startMorphArr;
if (geo === geoStar) startMorphStar = startMorphArr;
if (geo === geoAccents) startMorphAccents = startMorphArr;
}
const t = Math.min(1, (elapsed - disperseDuration) / morphDuration);
const e = easeInOutCubic(t);
for (let i = 0; i < count; i++) {
const ix = i * 3;
arr[ix] = startMorphArr[ix] + (targetsArr[ix] - startMorphArr[ix]) * e;
arr[ix + 1] = startMorphArr[ix + 1] + (targetsArr[ix + 1] - startMorphArr[ix + 1]) * e;
arr[ix + 2] = startMorphArr[ix + 2] + (targetsArr[ix + 2] - startMorphArr[ix + 2]) * e;
}
}
attr.needsUpdate = true;
}
const dt = 16;
for (let i = 0; i < snowCount; i++) {
const spr = snowSprites[i];
spr.position.x += snowVel[i * 2] * dt;
spr.position.y += snowVel[i * 2 + 1] * dt;
spr.material.rotation += snowRot[i];
const f = Math.min(1.0, 0.9 + 0.45 * Math.abs(Math.sin(now * 0.0015 + snowOpacityPhase[i])));
spr.material.opacity = 0.67 * f;
const halo = snowGroup.children[(i * 2) + 1];
if (halo) {
halo.position.copy(spr.position);
halo.material.opacity = Math.min(1.0, 0.47 * f);
}
if (spr.position.y < -30) {
spr.position.set((Math.random() - 0.5) * 120, 20 + Math.random() * 40, -22 - Math.random() * 12);
spr.material.rotation = Math.random() * Math.PI * 2;
if (halo) {
halo.position.copy(spr.position);
halo.material.rotation = spr.material.rotation;
}
}
}
updateSet(geoLeaves, posLeaves, velLeaves, startMorphLeaves, tgtLeaves, particleCountLeaves);
updateSet(geoOrnaments, posOrnaments, velOrnaments, startMorphOrnaments, tgtOrnaments, particleCountOrnaments);
updateSet(geoTrunk, posTrunk, velTrunk, startMorphTrunk, tgtTrunk, particleCountTrunk);
updateSet(geoStar, posStar, velStar, startMorphStar, tgtStar, particleCountStar);
updateSet(geoAccents, posAccents, velAccents, startMorphAccents, tgtAccents, particleCountAccents);
group.rotation.y += 0.0032;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
function onResize() {
const w = window.innerWidth;
const h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
fitCamera();
}
function fitCamera() {
const aspect = camera.aspect;
const vfov = THREE.MathUtils.degToRad(camera.fov);
const halfH = (topY - bottomY) * 0.5 * 1.06;
const radNeeded = (baseRadius + 1.6) * 1.06;
const zH = halfH / Math.tan(vfov / 2);
const zW = radNeeded / (Math.tan(vfov / 2) * aspect);
const z = Math.max(zH, zW);
camera.position.z = Math.max(z, 20);
const centerY = (topY + bottomY) * 0.5;
camera.position.y = centerY;
camera.lookAt(0, centerY, 0);
}
onMounted(() => {
const el = document.createElement('div');
el.id = 'canvas-container';
document.getElementById('app').appendChild(el);
containerEl.value = el;
initThree();
setInterval(spawnBarrage, 1200);
document.addEventListener('fullscreenchange', () => { isFs.value = !!document.fullscreenElement; });
});
return () => h('div', { style: { width: '100%', height: '100%' } }, [
h('div', { class: 'barrage' }, barrageItems.value.map(item => h('div', {
key: item.id,
class: 'barrage-item',
style: {
top: item.top,
color: item.color,
animationDuration: item.duration,
}
}, item.text))),
h('button', { class: 'fullscreen-btn', onClick: toggleFs }, isFs.value ? '退出全屏' : '全屏'),
]);
}
};
createApp(App).mount('#app');
</script>
</body>
</html>

View File

@@ -0,0 +1,39 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
background-color: #f0f0f0;
}
#clock {
font-size: 48px;
background: #333;
color: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body>
<div id="clock">00:00:00</div>
<script>
function updateTime() {
const clock = document.getElementById('clock');
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
clock.textContent = `${hours}:${minutes}:${seconds}`;
}
setInterval(updateTime, 1000);
updateTime();
</script>
</body>

View File

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