Files
MetonaSqlark/site/index.html
T
thzxx d544501e1c
CI / test (18.x) (push) Failing after 5m11s
CI / test (20.x) (push) Failing after 5m8s
CI / test (22.x) (push) Successful in 9m58s
CI / test (24.x) (push) Successful in 9m56s
release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
v0.2.6 质量加固:
- 修复 AriaEngine 二级索引 SSTable 互相覆盖(命名空间隔离)
- 修复 LSM 多版本读取顺序错误 + MergeIterator 取最新来源
- 重写 LZ4 压缩器(往返一致性 + 缓冲区溢出)
- sstableCache LRU 上限 + 预加载兜底(BufferPool 配置生效)
- 修复 React/Vue 集成 import type 运行时 bug + exports 子路径
- 新增 38 个测试(LZ4往返/Crypto/集成), 删除伪测试

v0.3.0 SQL 功能扩展:
- 多语句 parseAll + 事务语句 BEGIN/COMMIT/ROLLBACK
- INSERT INTO ... SELECT + UNION/UNION ALL + EXISTS 关联子查询
- CREATE/DROP INDEX 五引擎实现 + 别名 WHERE 修复
- benchmark 页面 + 36 个新测试

v0.3.1 表达式与性能:
- CASE WHEN 表达式(SELECT 列/WHERE/聚合)
- JOIN + 关联子查询逐行绑定
- WAL 批量组提交(写放大 O(N)→O(1))
- 修复 pending frozen 可见性 + flush 缓存竞争

v0.3.2 并发:
- CASE WHEN 用于 WHERE/聚合 + JOIN 哈希连接
- 多标签页同步(multiTabSync + BroadcastChannel)
- IndexedDB schema 持久化(reopen 后表结构恢复)
- 修复 where-matcher 顶层 $not
- 修复 CJS 产物 .js 被 ESM 解析(exports 空) — .cjs 后缀 + exports 修正
- 836 测试 / 44 套件 / 81.0% 覆盖率
2026-08-08 10:41:30 +08:00

475 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>◈ MetonaSqlark — 前端 TypeScript 数据库</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
<style>
:root {
--bg: #0a0a0f;
--surface: #14141f;
--surface2: #1a1a2e;
--border: #2a2a45;
--primary: #6366f1;
--primary-glow: #818cf8;
--accent: #06b6d4;
--accent2: #ec4899;
--text: #e2e8f0;
--text2: #94a3b8;
--gradient: linear-gradient(135deg, #6366f1 0%, #06b6d4 50%, #ec4899 100%);
--gradient2: linear-gradient(135deg, #1e1b4b 0%, #0f172a 50%, #1e1b4b 100%);
--radius: 16px;
--shadow: 0 0 60px rgba(99,102,241,0.15);
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
overflow-x: hidden;
}
/* --- Particles background --- */
.bg-canvas { position:fixed; top:0; left:0; width:100%; height:100%; z-index:0; opacity:0.3; pointer-events:none; }
/* --- Layout --- */
.container { max-width:1200px; margin:0 auto; padding:0 24px; position:relative; z-index:1; }
/* --- Header --- */
header {
position:fixed; top:0; left:0; right:0; z-index:100;
backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
background:rgba(10,10,15,0.85); border-bottom:1px solid var(--border);
padding:16px 0;
}
header .container { display:flex; align-items:center; justify-content:space-between; }
.logo { font-size:1.4rem; font-weight:800; letter-spacing:-0.5px; display:flex; align-items:center; gap:10px; }
.logo .icon {
width:36px; height:36px; border-radius:10px;
background:var(--gradient); display:flex; align-items:center; justify-content:center;
font-size:1.2rem; font-weight:900; color:#fff;
}
.logo span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
nav { display:flex; gap:32px; }
nav a { color:var(--text2); text-decoration:none; font-weight:500; font-size:0.95rem; transition:.2s; position:relative; }
nav a:hover { color:var(--text); }
nav a::after { content:''; position:absolute; bottom:-4px; left:0; width:0; height:2px; background:var(--gradient); transition:.3s; border-radius:1px; }
nav a:hover::after { width:100%; }
.btn {
display:inline-flex; align-items:center; gap:8px; padding:10px 24px; border-radius:10px;
font-weight:600; font-size:0.95rem; cursor:pointer; border:none; transition:.3s; text-decoration:none;
}
.btn-primary { background:var(--gradient); color:#fff; box-shadow:0 4px 20px rgba(99,102,241,0.4); }
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 8px 30px rgba(99,102,241,0.6); }
.btn-outline { background:transparent; border:2px solid var(--border); color:var(--text); }
.btn-outline:hover { border-color:var(--primary); background:rgba(99,102,241,0.08); }
/* --- Hero --- */
.hero {
padding:160px 0 80px; text-align:center; position:relative;
background:radial-gradient(ellipse 80% 60% at 50% -10%, rgba(99,102,241,0.15), transparent 60%),
radial-gradient(ellipse 60% 50% at 80% 80%, rgba(6,182,212,0.08), transparent 60%);
}
.hero h1 { font-size:clamp(2.4rem, 6vw, 4rem); font-weight:900; letter-spacing:-1.5px; line-height:1.1; margin-bottom:24px; }
.hero h1 .gradient-text { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.hero p { font-size:1.2rem; color:var(--text2); max-width:620px; margin:0 auto 40px; }
.hero .actions { display:flex; gap:16px; justify-content:center; flex-wrap:wrap; }
.badge {
display:inline-flex; align-items:center; gap:6px; padding:6px 14px; border-radius:999px;
background:var(--surface2); border:1px solid var(--border); font-size:0.85rem; color:var(--text2);
}
.badge .dot { width:7px; height:7px; border-radius:50%; background:#22c55e; animation:pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
/* --- Section --- */
section { padding:100px 0; }
.section-title { text-align:center; margin-bottom:60px; }
.section-title h2 { font-size:2.2rem; font-weight:800; letter-spacing:-0.5px; margin-bottom:12px; }
.section-title h2 span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.section-title p { color:var(--text2); font-size:1.1rem; }
/* --- Feature Grid --- */
.feature-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:24px; }
.feature-card {
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
padding:32px; transition:.3s; position:relative; overflow:hidden;
}
.feature-card:hover { border-color:var(--primary); transform:translateY(-4px); box-shadow:var(--shadow); }
.feature-card::before {
content:''; position:absolute; top:0; left:0; right:0; height:3px;
background:var(--gradient); opacity:0; transition:.3s;
}
.feature-card:hover::before { opacity:1; }
.feature-card .icon { font-size:2rem; margin-bottom:16px; }
.feature-card h3 { font-size:1.2rem; margin-bottom:8px; }
.feature-card p { color:var(--text2); font-size:0.95rem; }
/* --- Code Block --- */
.code-block {
background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius);
padding:28px; font-family:'JetBrains Mono','Fira Code',monospace; font-size:0.88rem;
overflow-x:auto; position:relative; line-height:1.7;
}
.code-block .keyword { color:#c084fc; }
.code-block .string { color:#34d399; }
.code-block .func { color:#60a5fa; }
.code-block .comment { color:#64748b; }
.code-block .number { color:#fbbf24; }
.code-block .type { color:#f472b6; }
/* --- Stats --- */
.stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:24px; text-align:center; }
.stat-card {
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:32px;
}
.stat-card .num { font-size:2.8rem; font-weight:900; background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
.stat-card .label { color:var(--text2); font-size:0.95rem; margin-top:4px; }
/* --- Footer --- */
footer { border-top:1px solid var(--border); padding:40px 0; text-align:center; color:var(--text2); }
footer a { color:var(--primary); text-decoration:none; }
/* --- Responsive --- */
@media(max-width:768px){
nav { display:none; }
.hero { padding:120px 0 60px; }
section { padding:60px 0; }
}
</style>
</head>
<body>
<canvas class="bg-canvas" id="bg"></canvas>
<header>
<div class="container">
<div class="logo">
<div class="icon"></div>
<span>MetonaSqlark</span>
</div>
<nav>
<a href="index.html">首页</a>
<a href="docs.html">文档</a>
<a href="demo.html">演示</a>
<a href="benchmark.html">基准</a>
<a href="https://git.metona.cn/MetonaTeam/MetonaSqlark" target="_blank">Gitea</a>
</nav>
<a href="demo.html" class="btn btn-primary">立即体验</a>
</div>
</header>
<!-- Hero -->
<section class="hero">
<div class="container">
<div class="badge" style="margin-bottom:24px;"><span class="dot"></span> v0.3.2 表达式与并发 — 761测试 41套件 · 五模式全覆盖 · WAL同步修复 · ALTER/TRUNCATE · SQL注入防护 · 零回归</div>
<h1>前端的 <span class="gradient-text">SQL 数据库</span></h1>
<p>TypeScript 原生构建,5 种存储引擎,支持完整 SQL 查询。<br>零运行时依赖,开箱即用。AriaEngine 自研引擎:LSM-Tree + WAL 同步 + MVCC。</p>
<div class="actions">
<a href="demo.html" class="btn btn-primary" style="font-size:1.05rem;padding:14px 32px;">▶ 在线演示</a>
<a href="docs.html" class="btn btn-outline" style="font-size:1.05rem;padding:14px 32px;">📖 API 文档</a>
</div>
</div>
</section>
<!-- Install -->
<section style="padding:20px 0 60px;">
<div class="container">
<div class="section-title">
<h2>📦 <span>安装</span>引入</h2>
<p>多种方式,按需选择</p>
</div>
<div class="feature-grid">
<div class="feature-card">
<div class="icon">📦</div>
<h3>npm (Gitea Registry)</h3>
<div class="code-block" style="font-size:0.8rem;margin-top:12px;padding:14px;">
<span class="comment"># 配置 registry</span>
npm config set @metona-team:registry https://git.metona.cn/api/packages/MetonaTeam/npm/
<span class="comment"># 安装</span>
npm install @metona-team/metona-sqlark
</div>
</div>
<div class="feature-card">
<div class="icon">🌐</div>
<h3>CDN / Browser UMD</h3>
<div class="code-block" style="font-size:0.8rem;margin-top:12px;padding:14px;">
<span class="comment">&lt;!-- 直接引用 dist 产物 --&gt;</span>
&lt;<span class="keyword">script</span> src=<span class="string">"metona-sqlark.min.js"</span>&gt;&lt;/script&gt;
&lt;<span class="keyword">script</span>&gt;
<span class="keyword">const</span> db = <span class="keyword">await</span> window.<span class="func">MetonaSqlark</span>.create({...});
<span class="comment">// 或 window.MeSqlark(别名)</span>
&lt;/script&gt;
</div>
</div>
<div class="feature-card">
<div class="icon"></div>
<h3>ESM / TypeScript</h3>
<div class="code-block" style="font-size:0.8rem;margin-top:12px;padding:14px;">
<span class="keyword">import</span> { MetonaSqlark, MeSqlark }
<span class="keyword">from</span> <span class="string">'@metona-team/metona-sqlark'</span>;
<span class="keyword">const</span> db = <span class="keyword">await</span> <span class="func">MetonaSqlark</span>.create({
<span class="string">name</span>: <span class="string">'my-app'</span>,
<span class="string">mode</span>: <span class="string">'hybrid'</span>,
});
</div>
</div>
<div class="feature-card">
<div class="icon">🔧</div>
<h3>CommonJS</h3>
<div class="code-block" style="font-size:0.8rem;margin-top:12px;padding:14px;">
<span class="keyword">const</span> { MetonaSqlark } =
<span class="func">require</span>(<span class="string">'@metona-team/metona-sqlark'</span>);
(<span class="keyword">async</span> () => {
<span class="keyword">const</span> db = <span class="keyword">await</span> <span class="func">MetonaSqlark</span>.create({...});
})();
</div>
</div>
</div>
</div>
</section>
<!-- Features -->
<section>
<div class="container">
<div class="section-title">
<h2>为什么选择 <span>MetonaSqlark</span></h2>
<p>专为前端打造的数据库引擎</p>
</div>
<div class="feature-grid">
<div class="feature-card">
<div class="icon">🧠</div>
<h3>5 种存储引擎</h3>
<p>Memory / IndexedDB / OPFS / Hybrid / <strong>AriaEngine</strong>。Aria 是自研 LSM-Tree 页面式引擎,支持 WAL 同步崩溃恢复和 MVCC 事务隔离。</p>
</div>
<div class="feature-card">
<div class="icon"></div>
<h3>完整 SQL 解析器</h3>
<p>手写递归下降 SQL 解析器。SELECT / INSERT / UPDATE / DELETE / JOIN / GROUP BY / HAVING / DISTINCT / 子查询 / ALTER TABLE / TRUNCATE TABLE。</p>
</div>
<div class="feature-card">
<div class="icon">🌲</div>
<h3>AriaEngine <span style="font-size:0.65rem;color:var(--accent);vertical-align:super;">v0.3.2</span></h3>
<p>自研 LSM-Tree 页面式存储引擎。MemTable 红黑树 + 多级 SSTable、Bloom Filter 快速判存、WAL full模式真正同步、MVCC 版本链接入读写路径。</p>
</div>
<div class="feature-card">
<div class="icon">🔒</div>
<h3>事务回滚 + MVCC</h3>
<p>beginTransaction / commitTransaction / rollbackTransaction 三件套。失败自动回滚,Memory 快照 + IndexedDB 延迟写入 + Aria MVCC 版本链。</p>
</div>
<div class="feature-card">
<div class="icon">🔍</div>
<h3>子查询 + ALTER TABLE</h3>
<p>IN (SELECT ...) + 标量子查询。ALTER TABLE ADD/DROP COLUMN 动态修改表结构,TRUNCATE TABLE 快速清空。</p>
</div>
<div class="feature-card">
<div class="icon">🔗</div>
<h3>外键级联 + SQL安全</h3>
<p>references + ON DELETE CASCADE / SET NULL / RESTRICT。React/Vue hooks 表名合法性校验,防 SQL 注入。</p>
</div>
<div class="feature-card">
<div class="icon">🏊</div>
<h3>连接池 + 性能优化</h3>
<p>MetonaSqlark.connect() 单例复用。SSTableReader 二分查找统一、IndexedDB 索引利用、crypto 实例化。</p>
</div>
<div class="feature-card">
<div class="icon">🧩</div>
<h3>插件系统</h3>
<p>14 种生命周期钩子:beforeCreateTable / afterInsert / beforeQuery / afterTransaction……按优先级注册。</p>
</div>
<div class="feature-card">
<div class="icon">📦</div>
<h3>零运行时依赖</h3>
<p>纯 TypeScript 实现,不依赖任何第三方库。Tree-shakableUMD/ESM/CJS 多格式输出,~27KB gzip。</p>
</div>
<div class="feature-card">
<div class="icon">📊</div>
<h3>聚合 & 分组</h3>
<p>COUNT / SUM / AVG / MIN / MAX 五大聚合函数,GROUP BY + HAVING 子句,DISTINCT 去重。</p>
</div>
<div class="feature-card">
<div class="icon">🔄</div>
<h3>数据迁移</h3>
<p>内置版本迁移系统,addMigration + migrateTo API。导入导出支持单表/全库 JSON 序列化。</p>
</div>
<div class="feature-card">
<div class="icon">📡</div>
<h3>发布订阅 + 框架集成</h3>
<p>subscribe() 订阅表变更。React useQuery / Vue useSqlarkQuery 开箱即用。</p>
</div>
<div class="feature-card">
<div class="icon">🔗</div>
<h3>Query Builder</h3>
<p>链式 API + TypeScript 泛型。.select().where().innerJoin().orderBy().limit() — 类型安全,IDE 友好。</p>
</div>
</div>
</div>
</section>
<!-- Architecture -->
<section>
<div class="container">
<div class="section-title">
<h2>🏗 <span>架构</span>设计</h2>
<p>清晰的分层结构,各模块职责单一</p>
</div>
<div class="code-block" style="text-align:center;font-size:0.82rem;line-height:2;background:transparent;border:none;">
<pre style="color:var(--text2);">
┌──────────────────────────────────────────────────────────┐
<span style="color:#f472b6;">MetonaSqlark / MeSqlark</span>
│ create() · query() · table() · tx() │
├──────────┬──────────┬──────────┬──────────┬──────────────┤
<span style="color:#c084fc;">SQL Parser</span><span style="color:#c084fc;">QueryBuilder</span><span style="color:#c084fc;">Transaction</span><span style="color:#c084fc;">Plugin</span><span style="color:#c084fc;">Migration</span>
│ Lexer → │ .select() │ Manager │ Manager │ System │
│ Parser │ .where() │ │ 14 hooks │ │
├──────────┴──────────┴──────────┴──────────┴──────────────┤
<span style="color:#60a5fa;">QueryExecutor</span> (AST → Results) │
│ JOIN · GROUP BY · HAVING · DISTINCT · Subquery │
├──────────────────────────────────────────────────────────┤
<span style="color:#34d399;">IStorageEngine</span> Interface │
├──────────┬──────────┬──────────┬──────────┬──────────────┤
<span style="color:#fbbf24;">Memory </span><span style="color:#fbbf24;">IndexedDB </span><span style="color:#fbbf24;">OPFS </span><span style="color:#fbbf24;">Hybrid </span><span style="color:#ec4899;font-weight:bold;">AriaEngine 🆕</span>
│ Engine │ Engine │ Engine │ Engine │ LSM-Tree │
│ (Map) │ (IDB) │ (File) │ (W-Thru) │ WAL + MVCC │
└──────────┴──────────┴──────────┴──────────┴──────────────┘
</pre>
</div>
</div>
</section>
<section style="background:var(--gradient2);">
<div class="container">
<div class="section-title">
<h2>5 行<span>代码</span>开始</h2>
<p>SQL + Query Builder 双 API — JOIN · 子查询 · 聚合 · 事务回滚 · 外键级联 · 连接池 · AriaEngine · ALTER TABLE · TRUNCATE</p>
</div>
<div class="code-block">
<pre><span class="comment">// 创建数据库 — MeSqlark 是别名,完全等价</span>
<span class="keyword">const</span> db = <span class="keyword">await</span> <span class="func">MetonaSqlark.create</span>({ <span class="string">name</span>: <span class="string">'my-app'</span>, <span class="string">mode</span>: <span class="string">'hybrid'</span> });
<span class="comment">// 定义表结构 — 支持外键级联</span>
<span class="keyword">await</span> db.<span class="func">defineTable</span>(<span class="string">'users'</span>, {
<span class="string">id</span>: { <span class="string">type</span>: <span class="string">'string'</span>, <span class="string">primaryKey</span>: <span class="keyword">true</span> },
<span class="string">name</span>: { <span class="string">type</span>: <span class="string">'string'</span>, <span class="string">required</span>: <span class="keyword">true</span> },
<span class="string">age</span>: { <span class="string">type</span>: <span class="string">'number'</span>, <span class="string">default</span>: <span class="number">0</span> },
<span class="string">email</span>: { <span class="string">type</span>: <span class="string">'string'</span>, <span class="string">unique</span>: <span class="keyword">true</span>, <span class="string">index</span>: <span class="keyword">true</span> },
});
<span class="comment">// SQL 查询</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">"INSERT INTO users VALUES ('1', 'Alice', 30, 'alice@demo.com')"</span>);
<span class="keyword">const</span> rows = <span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">'SELECT * FROM users WHERE age > 18 ORDER BY name'</span>);
<span class="comment">// Query Builder — 类型安全,链式调用</span>
<span class="keyword">const</span> rows2 = <span class="keyword">await</span> db.<span class="func">table</span>(<span class="string">'users'</span>)
.<span class="func">select</span>([<span class="string">'name'</span>, <span class="string">'age'</span>]).<span class="func">where</span>({ <span class="string">age</span>: { <span class="string">$gt</span>: <span class="number">18</span> } })
.<span class="func">orderBy</span>(<span class="string">'name'</span>).<span class="func">limit</span>(<span class="number">10</span>).<span class="func">execute</span>();
<span class="comment">// JOIN 多表关联</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">`SELECT u.name, o.product, o.amount
FROM users u INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100`</span>);
<span class="comment">// GROUP BY 聚合分析</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">`SELECT dept, COUNT(*) as cnt, AVG(salary) as avg
FROM employees GROUP BY dept HAVING COUNT(*) > 1`</span>);
<span class="comment">// 事务 — 失败自动回滚</span>
<span class="keyword">await</span> db.<span class="func">transaction</span>(<span class="keyword">async</span> (trx) => {
<span class="keyword">await</span> trx.<span class="func">table</span>(<span class="string">'users'</span>).<span class="func">insert</span>({ id: <span class="string">'3'</span>, name: <span class="string">'Charlie'</span> });
<span class="keyword">await</span> trx.<span class="func">table</span>(<span class="string">'orders'</span>).<span class="func">insert</span>({ id: <span class="string">'o1'</span>, userId: <span class="string">'3'</span>, amount: <span class="number">99</span> });
<span class="comment">// 任何一步失败 → 自动回滚</span>
});
<span class="comment">// 子查询 — IN (SELECT ...)</span>
<span class="keyword">await</span> db.<span class="func">query</span>(<span class="string">`SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)`</span>);
<span class="comment">// 连接池 — 单例复用,避免重复打开 IndexedDB</span>
<span class="keyword">const</span> db1 = <span class="keyword">await</span> <span class="func">MetonaSqlark.connect</span>({ name: <span class="string">'my-app'</span> });
<span class="keyword">const</span> db2 = <span class="keyword">await</span> <span class="func">MetonaSqlark.connect</span>({ name: <span class="string">'my-app'</span> });
<span class="comment">// db1 === db2(复用已有实例)</span>
<span class="keyword">await</span> db2.<span class="func">disconnect</span>(); <span class="comment">// 引用计数 -1,归零时自动 close</span></pre>
</div>
</div>
</section>
<!-- Stats -->
<section>
<div class="container">
<div class="section-title">
<h2><span>数字</span>说话</h2>
<p>MetonaSqlark 的核心指标</p>
</div>
<div class="stats">
<div class="stat-card"><div class="num">761</div><div class="label">测试用例</div></div>
<div class="stat-card"><div class="num">81.0%</div><div class="label">行覆盖率</div></div>
<div class="stat-card"><div class="num">~27KB</div><div class="label">gzip 体积</div></div>
<div class="stat-card"><div class="num">5</div><div class="label">存储引擎</div></div>
<div class="stat-card"><div class="num">36</div><div class="label">SQL 关键字</div></div>
<div class="stat-card"><div class="num">37</div><div class="label">测试套件</div></div>
</div>
</div>
</section>
<footer>
<div class="container">
<p>© 2026 <a href="https://git.metona.cn/MetonaTeam/MetonaSqlark">MetonaTeam</a>. MIT License. Built with TypeScript.</p>
</div>
</footer>
<script>
// Particle background
const canvas = document.getElementById('bg');
const ctx = canvas.getContext('2d');
let particles = [];
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resize(); window.addEventListener('resize', resize);
class Particle {
constructor() {
this.reset();
}
reset() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 0.5;
this.vy = (Math.random() - 0.5) * 0.5;
this.size = Math.random() * 2 + 0.5;
this.opacity = Math.random() * 0.4 + 0.1;
}
update() {
this.x += this.vx; this.y += this.vy;
if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(99,102,241,${this.opacity})`;
ctx.fill();
}
}
for (let i = 0; i < 80; i++) particles.push(new Particle());
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles.forEach(p => { p.update(); p.draw(); });
// Draw connections
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 120) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = `rgba(99,102,241,${0.06 * (1 - dist / 120)})`;
ctx.stroke();
}
}
}
requestAnimationFrame(animate);
}
animate();
</script>
</body>
</html>