feat: metona-sqlark v0.1.12 — 前端TypeScript关系型数据库
- 4种存储引擎:Memory / IndexedDB / OPFS / Hybrid - 完整SQL支持:SELECT/INSERT/UPDATE/DELETE/JOIN/GROUP BY/HAVING/DISTINCT - Query Builder链式API + TypeScript泛型支持 - 聚合函数:COUNT/SUM/AVG/MIN/MAX - 事务、插件系统(14 hooks)、发布订阅、数据迁移、导入导出 - React/Vue框架集成 - 264个测试用例,93.46%覆盖率 - 零运行时依赖
This commit is contained in:
+344
@@ -0,0 +1,344 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>在线演示 — metona-sqlark</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d;
|
||||
--primary:#6366f1; --accent:#06b6d4; --accent2:#ec4899; --green:#22c55e;
|
||||
--text:#e2e8f0; --text2:#8b949e; --radius:12px;
|
||||
--gradient:linear-gradient(135deg,#6366f1,#06b6d4,#ec4899);
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:'Inter',-apple-system,system-ui,sans-serif; background:var(--bg); color:var(--text); height:100vh; overflow:hidden; }
|
||||
/* Header */
|
||||
header {
|
||||
height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 24px;
|
||||
background:var(--surface); border-bottom:1px solid var(--border); z-index:10; position:relative;
|
||||
}
|
||||
.logo { font-weight:800; font-size:1.1rem; display:flex; align-items:center; gap:10px; }
|
||||
.logo .icon { width:28px; height:28px; border-radius:7px; background:var(--gradient); display:flex; align-items:center; justify-content:center; font-size:0.85rem; font-weight:900; color:#fff; }
|
||||
.logo span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
|
||||
nav { display:flex; gap:20px; }
|
||||
nav a { color:var(--text2); text-decoration:none; font-size:0.88rem; transition:.2s; }
|
||||
nav a:hover,.nav-active { color:var(--text); }
|
||||
.status { display:flex; align-items:center; gap:8px; font-size:0.82rem; color:var(--text2); }
|
||||
.status .dot { width:7px; height:7px; border-radius:50%; background:var(--green); animation:pulse 2s infinite; }
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||||
/* Main Layout */
|
||||
.main { display:flex; height:calc(100vh - 56px); }
|
||||
.editor-panel { flex:1; display:flex; flex-direction:column; border-right:1px solid var(--border); min-width:400px; }
|
||||
.result-panel { flex:1; display:flex; flex-direction:column; min-width:400px; }
|
||||
.panel-header {
|
||||
padding:12px 20px; font-size:0.78rem; font-weight:600; text-transform:uppercase;
|
||||
letter-spacing:1px; color:var(--text2); border-bottom:1px solid var(--border);
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
}
|
||||
.panel-header .badge {
|
||||
font-size:0.7rem; padding:3px 10px; border-radius:999px; background:var(--surface2);
|
||||
border:1px solid var(--border); text-transform:none; letter-spacing:0;
|
||||
}
|
||||
/* SQL Editor */
|
||||
.editor-area { flex:1; display:flex; flex-direction:column; }
|
||||
#sql-input {
|
||||
flex:1; background:var(--surface); color:var(--text); border:none; padding:20px;
|
||||
font-family:'JetBrains Mono','Fira Code',monospace; font-size:0.9rem; line-height:1.8;
|
||||
resize:none; outline:none; caret-color:var(--primary);
|
||||
}
|
||||
#sql-input::placeholder { color:var(--text2); opacity:0.5; }
|
||||
.toolbar { display:flex; gap:8px; padding:12px 20px; border-top:1px solid var(--border); background:var(--surface2); }
|
||||
.btn {
|
||||
display:inline-flex; align-items:center; gap:6px; padding:10px 20px; border-radius:8px;
|
||||
font-weight:600; font-size:0.85rem; cursor:pointer; border:none; transition:.25s; font-family:inherit;
|
||||
}
|
||||
.btn-run { background:var(--gradient); color:#fff; box-shadow:0 4px 15px rgba(99,102,241,0.35); }
|
||||
.btn-run:hover { transform:translateY(-1px); box-shadow:0 6px 25px rgba(99,102,241,0.5); }
|
||||
.btn-run:active { transform:scale(0.97); }
|
||||
.btn-clear { background:var(--surface2); color:var(--text2); border:1px solid var(--border); }
|
||||
.btn-clear:hover { color:var(--text); border-color:var(--text2); }
|
||||
.btn-preset { background:transparent; color:var(--accent); border:1px solid var(--accent); font-size:0.78rem; padding:6px 14px; }
|
||||
.btn-preset:hover { background:rgba(6,182,212,0.12); }
|
||||
/* Result */
|
||||
.result-content { flex:1; overflow-y:auto; padding:20px; }
|
||||
.result-table { width:100%; border-collapse:collapse; font-size:0.85rem; }
|
||||
.result-table th { text-align:left; padding:10px 14px; background:var(--surface2); color:var(--accent); font-weight:600; border-bottom:2px solid var(--border); position:sticky; top:0; z-index:1; }
|
||||
.result-table td { padding:8px 14px; border-bottom:1px solid var(--border); color:var(--text2); max-width:250px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.result-table tr:hover td { background:rgba(99,102,241,0.04); }
|
||||
.empty-state { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--text2); gap:12px; }
|
||||
.empty-state .icon { font-size:3rem; opacity:0.3; }
|
||||
/* Error */
|
||||
.error-box { background:rgba(239,68,68,0.1); border:1px solid rgba(239,68,68,0.3); border-radius:10px; padding:16px; color:#f87171; font-family:'JetBrains Mono',monospace; font-size:0.82rem; margin-bottom:16px; }
|
||||
.success-box { background:rgba(34,197,94,0.1); border:1px solid rgba(34,197,94,0.3); border-radius:10px; padding:12px 16px; color:#4ade80; font-size:0.85rem; margin-bottom:16px; }
|
||||
/* Tabs */
|
||||
.tabs { display:flex; gap:4px; padding:8px 20px 0; background:var(--surface); }
|
||||
.tab { padding:8px 18px; border-radius:8px 8px 0 0; font-size:0.82rem; cursor:pointer; color:var(--text2); border:1px solid transparent; transition:.2s; }
|
||||
.tab:hover { color:var(--text); }
|
||||
.tab.active { color:var(--text); background:var(--bg); border-color:var(--border); border-bottom-color:var(--bg); }
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width:6px; height:6px; }
|
||||
::-webkit-scrollbar-track { background:transparent; }
|
||||
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
|
||||
/* Responsive */
|
||||
@media(max-width:900px){ .main { flex-direction:column; } .editor-panel,.result-panel { min-width:auto; } .editor-panel { height:50%; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="logo"><div class="icon">◈</div><span>metona-sqlark</span></div>
|
||||
<nav>
|
||||
<a href="index.html">首页</a>
|
||||
<a href="docs.html">文档</a>
|
||||
<a href="demo.html" class="nav-active">演示</a>
|
||||
</nav>
|
||||
<div class="status"><span class="dot"></span> Memory 模式</div>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
<!-- Editor Panel -->
|
||||
<div class="editor-panel">
|
||||
<div class="tabs">
|
||||
<div class="tab active" data-tab="sql">SQL 查询</div>
|
||||
<div class="tab" data-tab="builder">Query Builder</div>
|
||||
</div>
|
||||
<div class="editor-area">
|
||||
<textarea id="sql-input" placeholder="输入 SQL 语句... SELECT * FROM users WHERE age > 18 INSERT INTO users VALUES ('4', 'Diana', 'diana@test.com', 28) SELECT u.name, o.amount FROM users u INNER JOIN orders o ON u.id = o.user_id">-- 欢迎使用 metona-sqlark 在线演示 🚀
|
||||
-- 已预置 users / orders 表数据,试试以下查询:
|
||||
|
||||
SELECT * FROM users;
|
||||
|
||||
SELECT name, age FROM users WHERE age > 25 ORDER BY age DESC;
|
||||
|
||||
SELECT u.name, o.amount, o.product
|
||||
FROM users u INNER JOIN orders o ON u.id = o.user_id;
|
||||
|
||||
SELECT COUNT(*) as total_users, AVG(age) as avg_age FROM users;</textarea>
|
||||
<div class="toolbar">
|
||||
<button class="btn btn-run" onclick="runQuery()">▶ 执行 (Ctrl+Enter)</button>
|
||||
<button class="btn btn-clear" onclick="clearResults()">🗑 清空</button>
|
||||
<span style="flex:1"></span>
|
||||
<button class="btn btn-preset" onclick="loadPreset('all')">📋 全部数据</button>
|
||||
<button class="btn btn-preset" onclick="loadPreset('join')">🔗 JOIN</button>
|
||||
<button class="btn btn-preset" onclick="loadPreset('agg')">📊 聚合</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Panel -->
|
||||
<div class="result-panel">
|
||||
<div class="panel-header">
|
||||
<span>📊 查询结果</span>
|
||||
<span class="badge" id="row-count">0 行</span>
|
||||
</div>
|
||||
<div class="result-content" id="result-area">
|
||||
<div class="empty-state">
|
||||
<div class="icon">⚡</div>
|
||||
<div>执行 SQL 查询以查看结果</div>
|
||||
<div style="font-size:0.8rem;opacity:0.6">Ctrl + Enter 快捷执行</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../dist/metona-sqlark.js"></script>
|
||||
<script>
|
||||
const { create, MetonaSqlark } = window.MetonaSqlark || window.MeSqlark || {};
|
||||
|
||||
let db = null;
|
||||
const resultArea = document.getElementById('result-area');
|
||||
const rowCount = document.getElementById('row-count');
|
||||
const sqlInput = document.getElementById('sql-input');
|
||||
|
||||
// Init database
|
||||
async function initDB() {
|
||||
db = new MetonaSqlark({ name: 'demo', mode: 'memory' });
|
||||
await db.init();
|
||||
|
||||
// Create tables
|
||||
await db.defineTable('users', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
name: { type: 'string', required: true },
|
||||
email: { type: 'string' },
|
||||
age: { type: 'number', default: 0 },
|
||||
});
|
||||
await db.defineTable('orders', {
|
||||
id: { type: 'string', primaryKey: true },
|
||||
user_id: { type: 'string' },
|
||||
product: { type: 'string' },
|
||||
amount: { type: 'number' },
|
||||
});
|
||||
|
||||
// Seed data
|
||||
await db.table('users').insertMany([
|
||||
{ id: '1', name: 'Alice', email: 'alice@demo.com', age: 30 },
|
||||
{ id: '2', name: 'Bob', email: 'bob@demo.com', age: 25 },
|
||||
{ id: '3', name: 'Charlie', email: 'charlie@demo.com', age: 35 },
|
||||
]);
|
||||
await db.table('orders').insertMany([
|
||||
{ id: 'o1', user_id: '1', product: 'Laptop', amount: 1200 },
|
||||
{ id: 'o2', user_id: '1', product: 'Mouse', amount: 50 },
|
||||
{ id: 'o3', user_id: '2', product: 'Keyboard', amount: 150 },
|
||||
]);
|
||||
}
|
||||
|
||||
// Run SQL
|
||||
async function runQuery() {
|
||||
const sql = sqlInput.value.trim();
|
||||
if (!sql) return;
|
||||
|
||||
// Handle multiple statements
|
||||
const statements = sql.split(';').map(s => s.trim()).filter(s => s.length > 0);
|
||||
|
||||
// For display purposes, if only one statement, use it; otherwise use first
|
||||
try {
|
||||
let lastResult = null;
|
||||
const allResults = [];
|
||||
|
||||
for (const stmt of statements) {
|
||||
const result = await db.query(stmt);
|
||||
allResults.push({ sql: stmt, result });
|
||||
lastResult = result;
|
||||
}
|
||||
|
||||
if (allResults.length === 1) {
|
||||
renderResult(allResults[0]);
|
||||
} else {
|
||||
renderAllResults(allResults);
|
||||
}
|
||||
} catch (e) {
|
||||
renderError(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderResult({ sql, result }) {
|
||||
if (Array.isArray(result)) {
|
||||
renderTable(result);
|
||||
} else {
|
||||
renderSuccess(`✅ 执行成功 (影响 ${result ?? 0} 行)`);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAllResults(allResults) {
|
||||
let html = '';
|
||||
for (const { sql, result } of allResults) {
|
||||
html += `<div style="color:var(--accent);font-size:0.78rem;margin-bottom:8px;font-family:monospace;">> ${sql}</div>`;
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 0) {
|
||||
html += '<div class="success-box">✅ 查询返回 0 行</div>';
|
||||
} else {
|
||||
html += buildTableHTML(result);
|
||||
}
|
||||
} else {
|
||||
html += `<div class="success-box">✅ 执行成功</div>`;
|
||||
}
|
||||
html += '<div style="height:16px"></div>';
|
||||
}
|
||||
resultArea.innerHTML = html;
|
||||
updateRowCount(allResults.reduce((sum, r) => sum + (Array.isArray(r.result) ? r.result.length : 0), 0));
|
||||
}
|
||||
|
||||
function renderTable(rows) {
|
||||
if (!rows || rows.length === 0) {
|
||||
resultArea.innerHTML = '<div class="success-box">✅ 查询返回 0 行</div>';
|
||||
updateRowCount(0);
|
||||
return;
|
||||
}
|
||||
resultArea.innerHTML = buildTableHTML(rows);
|
||||
updateRowCount(rows.length);
|
||||
}
|
||||
|
||||
function buildTableHTML(rows) {
|
||||
const columns = Object.keys(rows[0]);
|
||||
let html = '<table class="result-table"><thead><tr>';
|
||||
for (const col of columns) {
|
||||
html += `<th>${escapeHTML(col)}</th>`;
|
||||
}
|
||||
html += '</tr></thead><tbody>';
|
||||
for (const row of rows) {
|
||||
html += '<tr>';
|
||||
for (const col of columns) {
|
||||
const val = row[col];
|
||||
html += `<td>${escapeHTML(val === null ? 'NULL' : String(val))}</td>`;
|
||||
}
|
||||
html += '</tr>';
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderError(msg) {
|
||||
resultArea.innerHTML = `<div class="error-box">❌ ${escapeHTML(msg)}</div>`;
|
||||
updateRowCount(0);
|
||||
}
|
||||
|
||||
function renderSuccess(msg) {
|
||||
resultArea.innerHTML = `<div class="success-box">${msg}</div>`;
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
resultArea.innerHTML = `<div class="empty-state"><div class="icon">⚡</div><div>结果已清空</div></div>`;
|
||||
updateRowCount(0);
|
||||
}
|
||||
|
||||
function updateRowCount(n) {
|
||||
rowCount.textContent = `${n} 行`;
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
// Presets
|
||||
const presets = {
|
||||
all: `-- 查看所有用户
|
||||
SELECT * FROM users;
|
||||
|
||||
-- 查看所有订单
|
||||
SELECT * FROM orders;`,
|
||||
join: `-- INNER JOIN:用户 + 订单
|
||||
SELECT u.name, o.product, o.amount
|
||||
FROM users u
|
||||
INNER JOIN orders o ON u.id = o.user_id
|
||||
ORDER BY o.amount DESC;`,
|
||||
agg: `-- 聚合:按用户统计订单总额
|
||||
SELECT u.name, COUNT(*) as orders, SUM(o.amount) as total_spent
|
||||
FROM users u
|
||||
INNER JOIN orders o ON u.id = o.user_id
|
||||
GROUP BY u.name
|
||||
ORDER BY total_spent DESC;`
|
||||
};
|
||||
|
||||
function loadPreset(name) {
|
||||
if (presets[name]) {
|
||||
sqlInput.value = presets[name];
|
||||
runQuery();
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcut
|
||||
document.addEventListener('keydown', e => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
runQuery();
|
||||
}
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', function() {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Boot
|
||||
initDB().then(() => {
|
||||
console.log('✅ metona-sqlark demo ready');
|
||||
// Auto-run initial query
|
||||
setTimeout(runQuery, 200);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user