编写静态HTML页面。它内置了示例数据,并支持使用SQL语句进行查询、过滤和聚合分析,无需后端服务器。
提示信息用中文,数据表格文本左对齐,数字右对齐。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>SQLite 在线查询实验室 | 内置数据集 + SQL 控制台</title>
<!-- Bootstrap 5 + Icons + DataTables 风格 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<!-- 代码高亮可选,增强SQL展示 -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css">
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<!-- SQL.js: 在浏览器中运行 SQLite (WebAssembly) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/sql-wasm.js"></script>
<style>
body {
background: #eef2f5;
font-family: 'Inter', system-ui, 'Segoe UI', Roboto, sans-serif;
}
.header-gradient {
background: linear-gradient(145deg, #0b2b3b, #1a4a6f);
color: white;
padding: 2rem 0;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
margin-bottom: 2rem;
}
.card {
border: none;
border-radius: 1.25rem;
box-shadow: 0 10px 25px -5px rgba(0,0,0,0.05), 0 8px 10px -6px rgba(0,0,0,0.02);
transition: transform 0.1s ease;
margin-bottom: 1.8rem;
}
.card-header {
background-color: rgba(25, 85, 115, 0.08);
border-bottom: 1px solid #dee2e6;
font-weight: 600;
font-size: 1.1rem;
border-radius: 1.25rem 1.25rem 0 0 !important;
padding: 1rem 1.5rem;
}
.sql-editor {
font-family: 'Fira Code', 'Cascadia Code', monospace;
font-size: 0.85rem;
background: #1e2a2f;
color: #e9f1e6;
border: none;
border-radius: 1rem;
padding: 1rem;
}
.sql-editor:focus {
box-shadow: 0 0 0 2px #4b9fff;
background-color: #1e2a2f;
color: #f0f9f0;
}
.btn-run {
background: #0f6b4c;
border: none;
border-radius: 2rem;
padding: 0.5rem 1.8rem;
font-weight: 500;
transition: 0.2s;
}
.btn-run:hover {
background: #0a533c;
transform: translateY(-1px);
box-shadow: 0 6px 14px rgba(15,107,76,0.25);
}
.btn-reset-view {
background: #2c5a7a;
border: none;
border-radius: 2rem;
}
.dataset-badge {
background: #e0f2fe;
color: #0369a1;
border-radius: 40px;
padding: 6px 14px;
font-size: 0.8rem;
font-weight: 500;
}
.table-container {
max-height: 450px;
overflow-y: auto;
border-radius: 1rem;
border: 1px solid #e9ecef;
}
table.dataTable {
margin-top: 0 !important;
border-collapse: separate;
width: 100%;
}
.dataTables_wrapper .dataTables_filter input {
border-radius: 2rem;
border: 1px solid #ccc;
padding: 0.3rem 1rem;
}
.sql-badge {
background-color: #234e6e;
color: white;
padding: 4px 12px;
border-radius: 30px;
font-size: 0.75rem;
}
footer {
text-align: center;
padding: 1.5rem;
color: #6c757d;
font-size: 0.85rem;
}
#resultInfo {
background: #f8fafc;
padding: 0.5rem 1rem;
border-radius: 1rem;
font-size: 0.85rem;
}
pre {
background: #f1f5f9;
padding: 0.6rem;
border-radius: 0.8rem;
font-size: 0.8rem;
}
</style>
</head>
<body>
<div class="header-gradient">
<div class="container">
<div class="d-flex justify-content-between align-items-center flex-wrap">
<div>
<h1><i class="fas fa-database me-2"></i>SQL 在线实验室</h1>
<p class="lead mb-0">内置数据集 · 原生 SQL 查询 · 即查即显</p>
<div class="mt-2">
<span class="dataset-badge"><i class="fas fa-table"></i> 示例: 销售订单 + 产品 + 客户</span>
</div>
</div>
<div class="mt-2 mt-md-0">
<i class="fas fa-microchip fa-2x opacity-50"></i>
</div>
</div>
</div>
</div>
<div class="container">
<!-- 数据集描述卡片 -->
<div class="card">
<div class="card-header">
<i class="fas fa-info-circle me-2"></i> 内置数据表结构
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-4 mb-2">
<div class="border rounded-3 p-2 h-100 bg-light">
<i class="fas fa-boxes text-primary"></i> <strong>products</strong><br>
<small>product_id, name, category, price, stock</small>
</div>
</div>
<div class="col-md-4 mb-2">
<div class="border rounded-3 p-2 h-100 bg-light">
<i class="fas fa-users text-success"></i> <strong>customers</strong><br>
<small>customer_id, name, region, join_date</small>
</div>
</div>
<div class="col-md-4 mb-2">
<div class="border rounded-3 p-2 h-100 bg-light">
<i class="fas fa-shopping-cart text-warning"></i> <strong>sales</strong><br>
<small>sale_id, customer_id, product_id, quantity, sale_date, amount</small>
</div>
</div>
</div>
<div class="alert alert-secondary mt-2 mb-0 small">
<i class="fas fa-lightbulb"></i> 支持 SQL 语法:SELECT, WHERE, GROUP BY, ORDER BY, JOIN, 聚合函数 (COUNT, SUM, AVG, MAX, MIN) 等。
</div>
</div>
</div>
<!-- SQL 控制台 + 结果区 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="fas fa-terminal me-2"></i>SQL 查询编辑器</span>
<span class="sql-badge"><i class="fas-regular fa-keyboard"></i> 可执行任意 SELECT</span>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label fw-semibold"><i class="fas fa-code"></i> 编写 SQL 语句</label>
<textarea id="sqlInput" rows="4" class="form-control sql-editor" placeholder="例如: -- 查询销售额大于100的订单详情 SELECT s.sale_id, c.name, p.name, s.quantity, s.amount FROM sales s JOIN customers c ON s.customer_id = c.customer_id JOIN products p ON s.product_id = p.product_id WHERE s.amount > 100 ORDER BY s.amount DESC;">SELECT * FROM sales LIMIT 15;</textarea>
</div>
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="executeBtn" class="btn btn-run text-white"><i class="fas fa-play me-1"></i> 执行查询</button>
<button id="resetPreviewBtn" class="btn btn-reset-view text-white"><i class="fas fa-table-list"></i> 重置预览 (sales 样例)</button>
<button id="showTablesBtn" class="btn btn-outline-secondary"><i class="fas fa-database"></i> 显示所有表</button>
<button id="exampleJoinBtn" class="btn btn-outline-info"><i class="fas fa-link"></i> 示例: 产品销售额汇总</button>
</div>
<div id="resultPanel">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong><i class="fas fa-chart-simple me-1"></i> 查询结果</strong>
<span id="resultInfo" class="text-muted small"></span>
</div>
<div class="table-container" id="queryResultTableWrapper">
<table id="resultTable" class="table table-sm table-striped table-bordered" style="width:100%">
<thead>
<tr><th>执行SQL查看结果</th></tr>
</thead>
<tbody>
<tr><td class="text-muted">点击查询按钮,数据将展示在此</td></tr>
</tbody>
</table>
</div>
<div id="execMessage" class="mt-2 small text-secondary"></div>
</div>
</div>
</div>
<!-- 快速查询示例卡片 -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<h6><i class="fas fa-chart-line"></i> 区域销售额</h6>
<pre class="mb-0"><code>SELECT c.region, SUM(s.amount) as total_sales
FROM sales s JOIN customers c ON s.customer_id = c.customer_id
GROUP BY c.region
ORDER BY total_sales DESC;</code></pre>
<button class="btn btn-sm btn-light mt-2 copy-example" data-sql="SELECT c.region, SUM(s.amount) as total_sales FROM sales s JOIN customers c ON s.customer_id = c.customer_id GROUP BY c.region ORDER BY total_sales DESC;">📋 加载此查询</button>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<h6><i class="fas fa-filter"></i> 高价值产品</h6>
<pre class="mb-0"><code>SELECT p.name, SUM(s.quantity) as total_qty, ROUND(AVG(s.amount),2) as avg_amount
FROM sales s JOIN products p ON s.product_id = p.product_id
GROUP BY p.product_id
HAVING total_qty > 5
ORDER BY avg_amount DESC;</code></pre>
<button class="btn btn-sm btn-light mt-2 copy-example" data-sql="SELECT p.name, SUM(s.quantity) as total_qty, ROUND(AVG(s.amount),2) as avg_amount FROM sales s JOIN products p ON s.product_id = p.product_id GROUP BY p.product_id HAVING total_qty > 5 ORDER BY avg_amount DESC;">📋 加载此查询</button>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<h6><i class="fas fa-calendar"></i> 月度趋势</h6>
<pre class="mb-0"><code>SELECT strftime('%Y-%m', sale_date) as month, COUNT(*) as orders, SUM(amount) as revenue
FROM sales
GROUP BY month
ORDER BY month;</code></pre>
<button class="btn btn-sm btn-light mt-2 copy-example" data-sql="SELECT strftime('%Y-%m', sale_date) as month, COUNT(*) as orders, SUM(amount) as revenue FROM sales GROUP BY month ORDER BY month;">📋 加载此查询</button>
</div>
</div>
</div>
</div>
<footer>
<i class="fas fa-cube"></i> 纯前端 SQLite 引擎 (sql.js) · 内置演示数据 · 所有计算在本地完成,无后端
</footer>
</div>
<script>
// 全局 SQLite 数据库实例
let db = null;
let SQL = null;
// 加载 sql-wasm 并初始化内置数据
function initDatabase() {
return new Promise((resolve, reject) => {
if (db && SQL) {
resolve(db);
return;
}
// 配置 sql.js 的 wasm 文件路径 (使用CDN)
const config = {
locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${filename}`
};
initSqlJs(config).then(function(SqlModule) {
SQL = SqlModule;
db = new SQL.Database();
createTablesAndSeedData(db);
resolve(db);
}).catch(err => {
console.error("SQLite 初始化失败", err);
document.getElementById("execMessage").innerHTML = `<span class="text-danger">⚠️ 加载 SQL 引擎失败,请刷新页面重试。${err}</span>`;
reject(err);
});
});
}
// 建表 + 插入种子数据 (丰富演示)
function createTablesAndSeedData(database) {
// 1. 产品表
database.run(`CREATE TABLE products (
product_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category TEXT,
price REAL,
stock INTEGER
)`);
// 2. 客户表
database.run(`CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
region TEXT,
join_date TEXT
)`);
// 3. 销售订单表
database.run(`CREATE TABLE sales (
sale_id INTEGER PRIMARY KEY,
customer_id INTEGER,
product_id INTEGER,
quantity INTEGER,
sale_date TEXT,
amount REAL,
FOREIGN KEY(customer_id) REFERENCES customers(customer_id),
FOREIGN KEY(product_id) REFERENCES products(product_id)
)`);
// 插入产品数据
const products = [
[1, '极简工学键盘', '外设', 89.9, 45],
[2, '无线静音鼠标', '外设', 35.5, 120],
[3, '27寸 4K 显示器', '显示设备', 1899.0, 22],
[4, 'USB-C 拓展坞', '配件', 129.0, 68],
[5, '人体工学椅', '家具', 2499.0, 8],
[6, '笔记本散热垫', '配件', 79.0, 34],
[7, '降噪耳机 Pro', '音频', 499.0, 27]
];
const insertProduct = database.prepare("INSERT INTO products (product_id, name, category, price, stock) VALUES (?, ?, ?, ?, ?)");
products.forEach(p => insertProduct.run(p));
insertProduct.free();
// 插入客户数据
const customers = [
[101, '云创科技', '华北', '2023-01-15'],
[102, '智达制造', '华东', '2023-02-20'],
[103, '天启零售', '华南', '2023-03-10'],
[104, '蓝海生物', '华东', '2023-05-22'],
[105, '无限设计', '西南', '2023-07-01'],
[106, '恒通集团', '华北', '2023-09-12'],
[107, '先锋数码', '华南', '2023-11-03']
];
const insertCust = database.prepare("INSERT INTO customers (customer_id, name, region, join_date) VALUES (?, ?, ?, ?)");
customers.forEach(c => insertCust.run(c));
insertCust.free();
// 插入销售数据 (近一年订单, 有代表性)
const sales = [
[1001, 101, 1, 2, '2024-01-10', 179.8],
[1002, 102, 3, 1, '2024-01-15', 1899.0],
[1003, 103, 2, 5, '2024-02-05', 177.5],
[1004, 101, 4, 3, '2024-02-18', 387.0],
[1005, 104, 5, 1, '2024-03-01', 2499.0],
[1006, 105, 6, 2, '2024-03-12', 158.0],
[1007, 106, 7, 1, '2024-04-02', 499.0],
[1008, 102, 1, 3, '2024-04-20', 269.7],
[1009, 107, 3, 1, '2024-05-10', 1899.0],
[1010, 103, 2, 10, '2024-05-25', 355.0],
[1011, 104, 4, 2, '2024-06-14', 258.0],
[1012, 105, 7, 2, '2024-06-30', 998.0],
[1013, 101, 5, 1, '2024-07-09', 2499.0],
[1014, 106, 6, 4, '2024-07-22', 316.0],
[1015, 107, 1, 1, '2024-08-01', 89.9],
[1016, 102, 2, 8, '2024-08-15', 284.0],
[1017, 103, 5, 1, '2024-09-02', 2499.0],
[1018, 104, 7, 1, '2024-09-19', 499.0],
[1019, 105, 3, 1, '2024-10-11', 1899.0],
[1020, 106, 4, 3, '2024-10-28', 387.0],
[1021, 107, 2, 6, '2024-11-05', 213.0],
[1022, 101, 6, 2, '2024-11-20', 158.0],
[1023, 103, 1, 4, '2024-12-01', 359.6],
[1024, 104, 3, 1, '2024-12-15', 1899.0],
[1025, 105, 5, 1, '2025-01-05', 2499.0],
[1026, 106, 7, 2, '2025-01-18', 998.0],
[1027, 102, 2, 3, '2025-02-04', 106.5],
[1028, 107, 4, 2, '2025-02-20', 258.0]
];
const insertSale = database.prepare("INSERT INTO sales (sale_id, customer_id, product_id, quantity, sale_date, amount) VALUES (?, ?, ?, ?, ?, ?)");
sales.forEach(s => insertSale.run(s));
insertSale.free();
// 创建索引让查询体验更好
database.run("CREATE INDEX idx_sales_customer ON sales(customer_id);");
database.run("CREATE INDEX idx_sales_product ON sales(product_id);");
}
// 执行查询并渲染为 DataTable
let currentDataTable = null;
function renderResultSet(columns, rows) {
// 完全销毁旧实例并清空容器
if (currentDataTable) {
currentDataTable.destroy();
currentDataTable = null;
}
// 清空表格所有内容
$("#resultTable thead").empty();
$("#resultTable tbody").empty();
if (!columns || columns.length === 0) {
$("#resultTable thead").html("<tr><th>空结果</th></tr>");
$("#resultTable tbody").html("<tr><td>查询返回0行数据</td></tr>");
return;
}
// 构建表头
let theadHtml = "<tr>";
columns.forEach(col => {
theadHtml += `<th class="text-start">${escapeHtml(col)}</th>`;
});
theadHtml += "</tr>";
$("#resultTable thead").html(theadHtml);
// 构建表体
let tbodyHtml = "";
rows.forEach(row => {
let tr = "<tr>";
columns.forEach(col => {
let val = row[col];
if (val === undefined || val === null) val = "";
let displayValue = "";
let alignClass = "text-start"; // 默认左对齐
let isNumber = false;
// 判断是否为数值类型
if (typeof val === 'number') {
isNumber = true;
alignClass = "text-end"; // 数值右对齐
// 处理小数位数
if (Number.isInteger(val)) {
displayValue = val.toString();
} else {
// 保留原始小数位数,最多6位
let decimalStr = val.toString().split('.')[1];
let decimalPlaces = decimalStr ? decimalStr.length : 0;
displayValue = val.toFixed(Math.min(decimalPlaces, 6));
}
} else {
displayValue = String(val);
}
tr += `<td class="${alignClass}">${escapeHtml(displayValue)}</td>`;
});
tr += "</tr>";
tbodyHtml += tr;
});
$("#resultTable tbody").html(tbodyHtml);
// 全新初始化 DataTable
currentDataTable = $("#resultTable").DataTable({
paging: true,
pageLength: 10,
info: true,
searching: true,
scrollX: true,
autoWidth: false,
language: {
search: "搜索:",
lengthMenu: "显示 _MENU_ 条",
info: "显示第 _START_ 到 _END_ 条,共 _TOTAL_ 条",
infoFiltered: "(从 _MAX_ 条中筛选)",
paginate: {
first: "首页",
previous: "上一页",
next: "下一页",
last: "末页"
},
zeroRecords: "没有找到匹配的记录"
}
});
$("#resultInfo").text(`共 ${rows.length} 行, ${columns.length} 列`);
}
function escapeHtml(str) {
if (!str) return "";
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 执行任意 SQL SELECT (确保只读)
function executeSelect(sqlText) {
if (!db) {
alert("数据库未就绪,稍后重试");
return Promise.reject("db not ready");
}
try {
// 使用所有小技巧,仅支持查询语句,但安全性考虑,我们仅执行SELECT,如果有修改语句会报错,返回友好提示。
// 但为了体验,防止drop/delete/update/insert等变更,先简单检查
let upperSql = sqlText.trim().toUpperCase();
if (upperSql.startsWith("INSERT") || upperSql.startsWith("UPDATE") || upperSql.startsWith("DELETE") || upperSql.startsWith("DROP") || upperSql.startsWith("ALTER") || upperSql.startsWith("CREATE")) {
throw new Error("为了数据安全,仅支持 SELECT 查询语句。如需查看原始数据,请使用示例或 SELECT 语句。");
}
// 执行
const stmt = db.prepare(sqlText);
const columns = stmt.getColumnNames();
const rows = [];
while (stmt.step()) {
const row = stmt.getAsObject();
rows.push(row);
}
stmt.free();
return Promise.resolve({ columns, rows });
} catch (err) {
return Promise.reject(err);
}
}
// 默认加载后展示sales表前10行预览
function loadDefaultPreview() {
executeSelect("SELECT sale_id, customer_id, product_id, quantity, sale_date, amount FROM sales LIMIT 12").then(res => {
renderResultSet(res.columns, res.rows);
$("#execMessage").html('<i class="fas fa-check-circle text-success"></i> 已加载销售数据样例 (sales)');
}).catch(err => {
$("#resultTable tbody").html(`<tr><td class="text-danger">预览错误: ${err.message}</td></tr>`);
$("#execMessage").html(`<span class="text-danger">${err.message}</span>`);
});
}
// 显示所有表名 (sqlite_master)
function showTables() {
executeSelect("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name").then(res => {
if (res.rows.length) {
let msg = "📂 数据表: " + res.rows.map(r => r.name).join(", ");
$("#execMessage").html(msg);
renderResultSet(res.columns, res.rows);
} else {
$("#execMessage").html("未找到用户表");
}
}).catch(e => {
$("#execMessage").html(`无法获取表信息: ${e.message}`);
});
}
// 示例:产品销售额汇总 (带JOIN)
function exampleProductRevenue() {
const sql = `SELECT p.name AS 产品名称, SUM(s.quantity) AS 总销量, ROUND(SUM(s.amount),2) AS 总销售额
FROM sales s JOIN products p ON s.product_id = p.product_id
GROUP BY p.product_id ORDER BY 总销售额 DESC LIMIT 8`;
document.getElementById("sqlInput").value = sql;
executeSelect(sql).then(res => {
renderResultSet(res.columns, res.rows);
$("#execMessage").html(`<i class="fas fa-chart-pie"></i> 产品维度销售额排行 (执行成功)`);
}).catch(err => {
alert("查询失败: "+err.message);
});
}
// 重置预览
function resetPreview() {
const defaultSql = "SELECT sale_id, customer_id, product_id, quantity, sale_date, amount FROM sales ORDER BY sale_date DESC LIMIT 15";
document.getElementById("sqlInput").value = defaultSql;
executeSelect(defaultSql).then(res => {
renderResultSet(res.columns, res.rows);
$("#execMessage").html('<i class="fas fa-undo-alt"></i> 重置为最近销售记录');
}).catch(err => {
$("#execMessage").html(`重置失败: ${err.message}`);
});
}
// 主流程
$(document).ready(async function() {
await initDatabase();
loadDefaultPreview();
// 绑定执行按钮
$("#executeBtn").click(function() {
const sql = $("#sqlInput").val();
if (!sql.trim()) {
$("#execMessage").html('<span class="text-warning">请输入SQL语句</span>');
return;
}
$("#execMessage").html('<i class="fas fa-spinner fa-pulse"></i> 执行中...');
executeSelect(sql).then(res => {
renderResultSet(res.columns, res.rows);
$("#execMessage").html(`<i class="fas fa-check-circle text-success"></i> 查询成功,耗时本地计算`);
}).catch(err => {
renderResultSet([], []);
$("#resultTable tbody").html(`<tr><td class="text-danger">SQL错误: ${escapeHtml(err.message)}</td></tr>`);
$("#execMessage").html(`<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> ${err.message}</span>`);
if (currentDataTable) currentDataTable.destroy();
currentDataTable = null;
});
});
$("#resetPreviewBtn").click(resetPreview);
$("#showTablesBtn").click(showTables);
$("#exampleJoinBtn").click(exampleProductRevenue);
// 示例按钮填充SQL到编辑器
$(".copy-example").each(function() {
$(this).click(function() {
const sqlText = $(this).data("sql");
if (sqlText) {
$("#sqlInput").val(sqlText);
$("#execMessage").html("📋 已加载示例SQL,点击「执行查询」运行");
}
});
});
// 可选: 监听回车快速执行 (ctrl+enter)
$("#sqlInput").on("keydown", function(e) {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
$("#executeBtn").click();
}
});
});
</script>
</body>
</html>