一个轻量级的 BI 工具,专注于核心功能,易于理解和扩展。
Mintbase 是 Metabase 的简化实现版本,旨在提供一个易于理解、便于学习的 BI 工具实现。它保留了 Metabase 的核心功能,同时大幅简化了架构和代码复杂度。
- ✅ 教育目的: 帮助开发者理解 BI 工具的核心原理
- ✅ 快速原型: 适合作为定制化 BI 工具的基础
- ✅ 轻量级: 代码简洁,易于维护和扩展
- ❌ 非生产级: 不适合直接用于生产环境(缺少企业级特性)
| 特性 | Metabase | Mintbase |
|---|---|---|
| 语言 | Clojure (JVM) | Node.js (JavaScript) |
| 前端框架 | React + Redux | 原生 JS + Tailwind CSS |
| 查询语言 | 完整 MBQL | 简化 MBQL |
| 数据库支持 | 50+ | SQLite (可扩展) |
| Collections | ✅ | ❌ |
| Models | ✅ | ❌ |
| Metrics | ✅ | ❌ |
| 复杂权限 | ✅ | ❌ |
| 学习难度 | 高 | 低 |
-
数据源管理
- SQLite 数据库连接
- 自动元数据同步(表、字段、类型)
- ER 图可视化(基于 Mermaid.js)
- 外键关系识别
-
可视化查询构建器
- 选择数据表
- 智能字段选择(基于聚合/分组动态更新)
- 多表 JOIN(支持智能推荐)
- 过滤条件构建
- 聚合函数(Count, Sum, Avg, Min, Max)
- 分组(Breakout)
- 自定义表达式(算术运算)
- 排序
- 结果限制
-
数据可视化
- 数据表格视图
- 柱状图
- 折线图
- 饼图
- 数字卡片(大数字显示)
- 实时图表类型切换
-
问题(Questions)管理
- 保存查询为问题
- 问题列表和搜索
- 问题编辑和删除
- 查询结果导出
-
仪表板(Dashboards)
- 创建和管理仪表板
- 添加问题到仪表板
- 网格布局系统
- 仪表板查看和刷新
- 更多数据库支持(PostgreSQL, MySQL)
- Native SQL 编辑器
- 数据权限管理
- 用户认证和授权
- 定时刷新和缓存
- 更多图表类型
┌─────────────────────────────────────────────────────────────┐
│ 前端层 │
│ ┌────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 查询构建器 │ │ 可视化引擎 │ │ 仪表板管理 │ │
│ │ (原生 JS) │ │ (Chart.js) │ │ (原生 JS) │ │
│ └────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └────────────────┴──────────────────┘ │
│ │ │
│ HTTP (JSON) │
└─────────────────────────┼───────────────────────────────────┘
│
┌─────────────────────────┼───────────────────────────────────┐
│ 后端层 (Express) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ API 路由层 │ │
│ │ /api/databases /api/cards /api/dashboards │ │
│ │ /api/query /api/tables /api/fields │ │
│ └──────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┴───────────────────────────────────┐ │
│ │ 查询处理层 │ │
│ │ ┌─────────────┐ ┌────────────┐ ┌──────────────┐ │ │
│ │ │ MBQL 解析器 │→ │ SQL 转换器 │→ │ 查询执行器 │ │ │
│ │ └─────────────┘ └────────────┘ └──────────────┘ │ │
│ └──────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌──────────────────┴───────────────────────────────────┐ │
│ │ 数据访问层 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 模型层 │ │ 驱动层 │ │ │
│ │ │ (Card/DB) │ │ (SQLite) │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────────────┘
│
┌──────────────────────────┴───────────────────────────────────┐
│ 数据库层 │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 应用数据库 │ │ 目标数据库 │ │
│ │ (mintbase.db) │ │ (northwind.db) │ │
│ │ - Cards │ │ - Customers │ │
│ │ - Dashboards │ │ - Orders │ │
│ │ - Databases │ │ - Products │ │
│ └──────────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────────┘
| 技术 | 版本 | 用途 |
|---|---|---|
| Node.js | 18+ | 运行时环境 |
| Express | 4.x | Web 框架 |
| SQLite3 | 5.x | 数据库驱动 |
| better-sqlite3 | 9.x | 同步 SQLite API |
| dotenv | 16.x | 环境变量管理 |
为什么选择 Node.js?
- 开发效率: JavaScript 语法简洁,无需编译,热重载开发体验好
- 前后端统一: 前后端都使用 JavaScript,降低学习成本
- JSON 原生支持: 查询语言是 JSON 格式,处理更自然
- 轻量级: 适合快速原型开发和迭代
- 生态丰富: npm 生态系统提供丰富的第三方库
| 技术 | 版本 | 用途 |
|---|---|---|
| 原生 JavaScript | ES6+ | 业务逻辑 |
| Tailwind CSS | 3.x | UI 样式 |
| Chart.js | 4.x | 图表渲染 |
| Mermaid.js | 10.x | ER 图渲染 |
| Choices.js | 10.x | 多选下拉框 |
为什么不用 React/Vue?
- 学习曲线: 原生 JS 更容易理解,适合教学
- 代码透明: 没有框架魔法,所有逻辑清晰可见
- 零构建: 不需要复杂的构建工具链
- 轻量级: 页面加载更快
用户操作
↓
构建 MBQL JSON
↓
发送到后端 /api/query
↓
验证查询 (validator.js)
↓
转换为 SQL (mbql-to-sql.js)
↓
执行查询 (executor.js)
↓
返回结果
↓
前端渲染(表格/图表)
应用数据模型 (存储在 mintbase.db):
-- 数据库连接配置
CREATE TABLE databases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
engine TEXT NOT NULL,
details TEXT NOT NULL -- JSON: {dbPath, ...}
);
-- 数据表元数据
CREATE TABLE tables (
id INTEGER PRIMARY KEY AUTOINCREMENT,
database_id INTEGER NOT NULL,
name TEXT NOT NULL,
schema TEXT,
FOREIGN KEY (database_id) REFERENCES databases(id)
);
-- 字段元数据
CREATE TABLE fields (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_id INTEGER NOT NULL,
name TEXT NOT NULL,
base_type TEXT NOT NULL, -- 'type/Text', 'type/Integer', etc.
semantic_type TEXT, -- 'type/PK', 'type/FK', etc.
FOREIGN KEY (table_id) REFERENCES tables(id)
);
-- 保存的问题
CREATE TABLE cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
dataset_query TEXT NOT NULL, -- JSON: MBQL 查询
display TEXT NOT NULL, -- 'table', 'bar', 'line', etc.
visualization_settings TEXT, -- JSON: 可视化配置
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 仪表板
CREATE TABLE dashboards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 仪表板卡片
CREATE TABLE dashboard_cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dashboard_id INTEGER NOT NULL,
card_id INTEGER NOT NULL,
row INTEGER NOT NULL,
col INTEGER NOT NULL,
size_x INTEGER NOT NULL,
size_y INTEGER NOT NULL,
FOREIGN KEY (dashboard_id) REFERENCES dashboards(id),
FOREIGN KEY (card_id) REFERENCES cards(id)
);Mintbase 使用简化版的 MBQL (Metabase Query Language),这是一种基于 JSON 的声明式查询语言。
MBQL 的设计哲学:
- 声明式: 描述"要什么"而不是"怎么做"
- JSON 格式: 易于序列化和传输
- 数据库无关: 可转换为不同数据库的 SQL
- 可组合: 各个部分可以独立组合
{
"type": "query",
"database": 1,
"query": {
"source-table": 1,
"fields": [...],
"joins": [...],
"filter": [...],
"aggregation": [...],
"breakout": [...],
"expressions": {...},
"order-by": [...],
"limit": 100
}
}["field", 5, null]"field": 字段类型标识符5: 字段 ID(在 fields 表中的 ID)null: 字段选项(暂未使用)
["field", 12, {"join-alias": "Products"}]- 引用 JOIN 的表中的字段
join-alias指定 JOIN 的别名
["expression", "total_price"]- 引用自定义表达式
- 表达式必须在
expressions中定义
// 等于
["=", ["field", 5, null], "John"]
// 不等于
["!=", ["field", 5, null], "John"]
// 大于
[">", ["field", 6, null], 100]
// 小于
["<", ["field", 6, null], 1000]
// 大于等于
[">=", ["field", 6, null], 100]
// 小于等于
["<=", ["field", 6, null], 1000]// AND
["and",
["=", ["field", 5, null], "John"],
[">", ["field", 6, null], 100]
]
// OR
["or",
["=", ["field", 5, null], "John"],
["=", ["field", 5, null], "Jane"]
]
// NOT
["not", ["=", ["field", 5, null], "John"]]// BETWEEN
["between", ["field", 6, null], 100, 1000]
// IN
["in", ["field", 5, null], "John", "Jane", "Bob"]
// IS NULL
["is-null", ["field", 5, null]]
// NOT NULL
["not-null", ["field", 5, null]]
// LIKE (模糊匹配)
["contains", ["field", 5, null], "john"]
["starts-with", ["field", 5, null], "john"]
["ends-with", ["field", 5, null], "smith"]// 计数
[["count"]]
// 求和
[["sum", ["field", 6, null]]]
// 平均值
[["avg", ["field", 6, null]]]
// 最小值
[["min", ["field", 6, null]]]
// 最大值
[["max", ["field", 6, null]]]
// 多个聚合
[
["count"],
["sum", ["field", 6, null]],
["avg", ["field", 6, null]]
]// 单字段分组
{
"breakout": [["field", 5, null]],
"aggregation": [["count"]]
}
// 多字段分组
{
"breakout": [
["field", 5, null],
["field", 7, null]
],
"aggregation": [["sum", ["field", 6, null]]]
}{
"expressions": {
"total_price": ["+", ["field", 10, null], ["field", 11, null]],
"discount_price": ["*", ["field", 10, null], 0.9]
},
"fields": [
["field", 9, null],
["expression", "total_price"],
["expression", "discount_price"]
]
}支持的运算符:
+: 加法-: 减法*: 乘法/: 除法
{
"source-table": 1,
"joins": [
{
"alias": "Products",
"source-table": 2,
"condition": [
"=",
["field", 8, null], // 主表字段
["field", 15, {"join-alias": "Products"}] // JOIN 表字段
]
}
]
}// 单字段排序
{
"order-by": [[["field", 5, null], "asc"]]
}
// 多字段排序
{
"order-by": [
[["field", 5, null], "desc"],
[["field", 6, null], "asc"]
]
}
// 对聚合结果排序
{
"aggregation": [["count"]],
"order-by": [[["aggregation", 0], "desc"]]
}// 选择特定字段
{
"source-table": 1,
"fields": [
["field", 5, null],
["field", 6, null]
]
}
// 在聚合查询中选择字段(过滤分组字段)
{
"aggregation": [["count"]],
"breakout": [
["field", 5, null],
["field", 6, null]
],
"fields": [
["field", 5, null] // 只显示第一个分组字段
]
}{
"source-table": 1,
"limit": 100
}{
"type": "query",
"database": 1,
"query": {
"source-table": 1,
"fields": [
["field", 1, null],
["field", 2, null]
],
"filter": ["=", ["field", 3, null], "USA"],
"limit": 10
}
}转换为 SQL:
SELECT "field_1", "field_2"
FROM "table_1"
WHERE "field_3" = 'USA'
LIMIT 10{
"type": "query",
"database": 1,
"query": {
"source-table": 2,
"aggregation": [
["count"],
["sum", ["field", 10, null]]
],
"breakout": [["field", 5, null]],
"order-by": [[["aggregation", 0], "desc"]]
}
}转换为 SQL:
SELECT "field_5", COUNT(*) as count, SUM("field_10") as sum
FROM "table_2"
GROUP BY "field_5"
ORDER BY count DESC{
"type": "query",
"database": 1,
"query": {
"source-table": 1,
"joins": [
{
"alias": "Products",
"source-table": 2,
"condition": [
"=",
["field", 8, null],
["field", 15, {"join-alias": "Products"}]
]
}
],
"fields": [
["field", 5, null],
["field", 16, {"join-alias": "Products"}]
]
}
}转换为 SQL:
SELECT main."field_5", Products."field_16"
FROM "table_1" AS main
LEFT JOIN "table_2" AS Products ON main."field_8" = Products."field_15"{
"type": "query",
"database": 1,
"query": {
"source-table": 2,
"expressions": {
"total_price": ["*", ["field", 10, null], ["field", 11, null]],
"discounted": ["*", ["expression", "total_price"], 0.9]
},
"fields": [
["field", 9, null],
["expression", "total_price"],
["expression", "discounted"]
],
"filter": [">", ["expression", "total_price"], 100]
}
}转换为 SQL:
SELECT
"field_9",
"field_10" * "field_11" as total_price,
("field_10" * "field_11") * 0.9 as discounted
FROM "table_2"
WHERE ("field_10" * "field_11") > 100MBQL JSON
↓
解析查询结构
↓
处理表达式定义 → 存储到 this.expressions
↓
转换 SELECT 子句 ← 处理 fields/aggregation/breakout
↓
转换 FROM 子句 ← 处理 source-table/joins
↓
转换 WHERE 子句 ← 处理 filter (递归)
↓
转换 GROUP BY 子句 ← 基于 breakout
↓
转换 ORDER BY 子句 ← 处理 order-by
↓
转换 LIMIT 子句 ← 处理 limit
↓
组装 SQL 字符串
↓
返回完整 SQL
class MQLToSQL {
constructor(database, table, fieldMap, allTables) {
this.database = database;
this.table = table;
this.fieldMap = fieldMap; // 字段 ID → 字段对象
this.allTables = allTables; // 所有表 ID → 表对象
this.expressions = {}; // 表达式定义
this.joinAliases = {}; // JOIN 别名映射
this.mainTableAlias = null; // 主表别名
}
convert(mqlQuery) {
// 转换入口
}
}有 fields 指定?
├─ 是 → 有聚合/分组?
│ ├─ 是 → fields 过滤 breakout,所有 aggregation
│ └─ 否 → 直接使用 fields
└─ 否 → 有聚合/分组?
├─ 是 → 输出所有 breakout + aggregation
└─ 否 → 输出 *
场景 1: 无聚合,指定字段
{
"fields": [["field", 1, null], ["field", 2, null]]
}→ SELECT "name", "email"
场景 2: 有聚合,指定字段
{
"aggregation": [["count"]],
"breakout": [["field", 1, null], ["field", 2, null]],
"fields": [["field", 1, null]]
}→ SELECT "name", COUNT(*) as count
场景 3: 有聚合,无字段指定
{
"aggregation": [["count"], ["sum", ["field", 3, null]]],
"breakout": [["field", 1, null]]
}→ SELECT "name", COUNT(*) as count, SUM("price") as sum
递归处理过滤条件树:
convertFilter(filter) {
const [op, ...args] = filter;
switch (op) {
case '=':
case '!=':
case '>':
case '<':
case '>=':
case '<=':
return `${this.convertFieldOrExpression(args[0])} ${op} ${this.convertValue(args[1])}`;
case 'and':
case 'or':
const conditions = args.map(arg => this.convertFilter(arg));
return `(${conditions.join(` ${op.toUpperCase()} `)})`;
case 'not':
return `NOT (${this.convertFilter(args[0])})`;
// ... 其他运算符
}
}convertFrom(query) {
let from = `"${this.table.name}"`;
if (this.mainTableAlias) {
from += ` AS ${this.mainTableAlias}`;
}
if (query.joins) {
query.joins.forEach(join => {
const joinTable = this.allTables[join['source-table']];
const alias = join.alias;
this.joinAliases[alias] = joinTable;
const condition = this.convertFilter(join.condition);
from += ` LEFT JOIN "${joinTable.name}" AS ${alias} ON ${condition}`;
});
}
return from;
}表达式在多处使用时需要展开为实际 SQL:
convertExpression(exprName) {
const expr = this.expressions[exprName];
if (!expr) {
throw new Error(`Expression not found: ${exprName}`);
}
const [op, ...args] = expr;
if (['+', '-', '*', '/'].includes(op)) {
const left = this.convertFieldOrExpression(args[0]);
const right = this.convertValue(args[1]);
return `(${left} ${op} ${right})`;
}
throw new Error(`Unsupported expression operator: ${op}`);
}{
"type": "query",
"database": 1,
"query": {
"source-table": 1,
"expressions": {
"total": ["*", ["field", 10, null], ["field", 11, null]]
},
"aggregation": [["sum", ["expression", "total"]]],
"breakout": [["field", 5, null]],
"filter": [">", ["expression", "total"], 100],
"order-by": [[["aggregation", 0], "desc"]],
"limit": 10
}
}SELECT
"customer_name",
SUM("quantity" * "price") as sum
FROM "orders"
WHERE ("quantity" * "price") > 100
GROUP BY "customer_name"
ORDER BY sum DESC
LIMIT 10-
表达式处理:
"total"→("quantity" * "price")
-
SELECT 转换:
- Breakout:
["field", 5, null]→"customer_name" - Aggregation:
["sum", ["expression", "total"]]→SUM("quantity" * "price") as sum
- Breakout:
-
FROM 转换:
"source-table": 1→"orders"
-
WHERE 转换:
[">", ["expression", "total"], 100]→("quantity" * "price") > 100
-
GROUP BY 转换:
- 基于 breakout →
GROUP BY "customer_name"
- 基于 breakout →
-
ORDER BY 转换:
[["aggregation", 0], "desc"]→ORDER BY sum DESC
-
LIMIT 转换:
"limit": 10→LIMIT 10
convertFieldReference(fieldRef) {
const [type, fieldId, options] = fieldRef;
if (options && options['join-alias']) {
const alias = options['join-alias'];
const joinTable = this.joinAliases[alias];
const field = joinTable.fields.find(f => f.id === fieldId);
return `${alias}."${field.name}"`;
} else {
const field = this.fieldMap[fieldId];
const prefix = this.mainTableAlias || '';
return prefix ? `${prefix}."${field.name}"` : `"${field.name}"`;
}
}convertOrderBy(query) {
if (!query['order-by']) return null;
return query['order-by'].map(([fieldOrAgg, direction]) => {
const [type, index] = fieldOrAgg;
if (type === 'aggregation') {
// 使用聚合别名
const aggType = query.aggregation[index][0];
return `${aggType} ${direction.toUpperCase()}`;
} else {
// 普通字段
return `${this.convertFieldOrExpression(fieldOrAgg)} ${direction.toUpperCase()}`;
}
}).join(', ');
}convertValue(value) {
if (value === null) {
return 'NULL';
} else if (typeof value === 'string') {
return `'${value.replace(/'/g, "''")}'`; // SQL 转义
} else if (typeof value === 'number') {
return value.toString();
} else if (typeof value === 'boolean') {
return value ? '1' : '0';
}
throw new Error(`Unsupported value type: ${typeof value}`);
}mintbase/
├── backend/ # Node.js 后端
│ ├── src/
│ │ ├── index.js # 应用入口,Express 服务器
│ │ ├── db/
│ │ │ └── index.js # 数据库初始化和连接管理
│ │ ├── models/ # 数据模型
│ │ │ ├── card.js # 问题(保存的查询)
│ │ │ ├── dashboard.js # 仪表板
│ │ │ ├── database.js # 数据库连接配置
│ │ │ ├── table.js # 数据表元数据
│ │ │ └── field.js # 字段元数据
│ │ ├── routes/ # API 路由
│ │ │ ├── index.js # 路由汇总
│ │ │ ├── databases.js # 数据库相关 API
│ │ │ ├── cards.js # 问题相关 API
│ │ │ ├── dashboards.js # 仪表板相关 API
│ │ │ └── query.js # 查询执行 API
│ │ ├── query/ # 查询处理
│ │ │ ├── mbql-to-sql.js # MBQL → SQL 转换器 ⭐
│ │ │ ├── executor.js # 查询执行器
│ │ │ └── validator.js # 查询验证器
│ │ └── drivers/ # 数据库驱动
│ │ ├── index.js # 驱动管理器
│ │ └── sqlite.js # SQLite 驱动 ⭐
│ ├── scripts/
│ │ └── init_northwind.js # Northwind 示例数据库初始化
│ ├── package.json
│ └── .env.example # 环境变量示例
│
├── frontend/ # 前端(原生 JS)
│ ├── pages/
│ │ ├── query-builder.html # 查询构建器 ⭐
│ │ ├── cards.html # 问题列表
│ │ ├── card-detail.html # 问题详情
│ │ ├── dashboards.html # 仪表板列表
│ │ └── dashboard-view.html # 仪表板查看
│ ├── js/
│ │ ├── api.js # API 客户端封装
│ │ ├── query-builder-ui.js # 查询构建器 UI 逻辑 ⭐
│ │ └── utils.js # 工具函数
│ ├── css/
│ │ └── style.css # Tailwind CSS(编译后)
│ └── package.json # Tailwind CSS 依赖
│
├── data/ # 数据文件
│ ├── mintbase.db # 应用数据库(自动创建)
│ └── northwind.db # 示例数据库(自动创建)
│
├── docs/ # 文档
│ ├── architecture.md # 架构设计文档
│ ├── query-language.md # 查询语言详细文档
│ └── api.md # API 接口文档
│
└── README.md # 本文件
⭐ 标记的是核心文件
- Node.js: 18.x 或更高版本
- npm: 9.x 或更高版本
- 操作系统: Linux, macOS, Windows
git clone https://github.com/caochun/mintbase.git
cd mintbasecd backend
npm installcp .env.example .env编辑 .env 文件:
# 服务器配置
PORT=3001
NODE_ENV=development
# CORS 配置
CORS_ORIGIN=*
# 数据库路径
APP_DB_PATH=../data/mintbase.dbnpm start服务将在 http://0.0.0.0:3001 启动。
打开浏览器访问:
http://localhost:3001
将自动跳转到查询构建器页面。
后端启动时会自动初始化 Northwind 示例数据库,包含以下表:
- customers: 客户信息
- employees: 员工信息
- orders: 订单信息
- order_details: 订单明细
- products: 产品信息
- categories: 产品分类
- suppliers: 供应商信息
http://localhost:3001/api
GET /api/databases响应:
[
{
"id": 1,
"name": "Northwind",
"engine": "sqlite",
"details": {
"dbPath": "../data/northwind.db"
}
}
]GET /api/databases/:id/tables响应:
[
{
"id": 1,
"database_id": 1,
"name": "customers",
"schema": null
}
]GET /api/databases/:id/tables/:tableId/fields响应:
[
{
"id": 1,
"table_id": 1,
"name": "customer_id",
"base_type": "type/Integer",
"semantic_type": "type/PK"
}
]GET /api/databases/:id/er-diagram响应:
{
"tables": {
"1": {
"id": 1,
"name": "customers",
"fields": [...]
}
},
"relationships": [
{
"from_table_id": 2,
"from_field": "customer_id",
"to_table_id": 1,
"to_field": "customer_id"
}
]
}GET /api/databases/:id/tables/:tableId/relationships响应:
{
"outgoing": [
{
"table_id": 2,
"table_name": "orders",
"from_field": "customer_id",
"to_field": "customer_id"
}
],
"incoming": []
}POST /api/query
Content-Type: application/json
{
"type": "query",
"database": 1,
"query": {
"source-table": 1,
"limit": 10
}
}响应:
{
"data": {
"rows": [[...], [...]],
"cols": [
{"name": "customer_id", "base_type": "type/Integer"},
{"name": "customer_name", "base_type": "type/Text"}
]
}
}GET /api/cardsPOST /api/cards
Content-Type: application/json
{
"name": "客户总数",
"description": "统计客户数量",
"dataset_query": {...},
"display": "bar",
"visualization_settings": {...}
}PUT /api/cards/:idDELETE /api/cards/:idGET /api/dashboardsPOST /api/dashboards
Content-Type: application/json
{
"name": "销售分析",
"description": "销售相关指标"
}POST /api/dashboards/:id/cards
Content-Type: application/json
{
"card_id": 1,
"row": 0,
"col": 0,
"size_x": 4,
"size_y": 3
}-
在
backend/src/drivers/创建新驱动文件,如mysql.js -
实现驱动接口:
export class MySQLDriver {
constructor(details) {
this.details = details;
}
async connect() {
// 建立连接
}
async getTables(database) {
// 获取表列表
}
async getFields(database, tableName) {
// 获取字段列表
}
async executeQuery(database, sql) {
// 执行查询
}
disconnect() {
// 关闭连接
}
}- 在
backend/src/drivers/index.js注册驱动:
import { MySQLDriver } from './mysql.js';
export function getDriver(engine) {
switch (engine) {
case 'sqlite':
return SQLiteDriver;
case 'mysql':
return MySQLDriver;
// ...
}
}- 在
backend/src/query/mbql-to-sql.js的convertAggregation方法中添加:
convertAggregation(agg) {
const [func, ...args] = agg;
switch (func) {
case 'count':
return 'COUNT(*)';
case 'sum':
return `SUM(${this.convertFieldOrExpression(args[0])})`;
case 'stddev': // 新增标准差
return `STDDEV(${this.convertFieldOrExpression(args[0])})`;
// ...
}
}- 在前端
query-builder-ui.js中添加 UI 选项:
const aggregationSelect = document.getElementById('aggregation-function');
aggregationSelect.innerHTML = `
<option value="">选择聚合函数</option>
<option value="count">计数</option>
<option value="sum">求和</option>
<option value="stddev">标准差</option>
`;- 在
query-builder-ui.js的renderChart函数中添加处理逻辑:
function renderChart(result) {
const chartType = document.getElementById('chart-type-select').value;
switch (chartType) {
case 'scatter': // 新增散点图
renderScatterChart(result);
break;
// ...
}
}
function renderScatterChart(result) {
// 使用 Chart.js 渲染散点图
}在后端控制台会输出每次查询生成的 SQL:
Executing SQL: SELECT "customer_name", COUNT(*) as count FROM "orders" GROUP BY "customer_name"
打开浏览器开发者工具(F12),在 Console 中查看:
- MBQL 查询对象
- API 请求和响应
- 错误信息
使用 VS Code 的调试功能:
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Backend",
"program": "${workspaceFolder}/backend/src/index.js",
"cwd": "${workspaceFolder}/backend",
"envFile": "${workspaceFolder}/backend/.env"
}
]
}A: SQLite 是文件型数据库,无需单独安装和配置服务器,非常适合快速开始和学习。你可以轻松添加对 PostgreSQL、MySQL 等的支持。
A: Mintbase 是一个教学项目,缺少很多生产环境必需的特性(如认证、权限、缓存、性能优化等)。建议仅用于学习和原型开发。
A: 欢迎提交 Pull Request!请确保:
- 代码风格一致
- 添加必要的注释
- 更新相关文档
A: 检查 .env 文件中的 CORS_ORIGIN 配置,确保允许你的前端域名。
A: 可以集成 JWT 或 Session:
- 在后端添加认证中间件
- 实现登录/注册 API
- 在前端添加登录页面
- 在 API 请求中携带 token
MIT License
Copyright (c) 2024 Mintbase Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- Metabase: 灵感来源
- Chart.js: 图表渲染
- Mermaid.js: ER 图渲染
- Tailwind CSS: UI 样式
- Northwind Database: 示例数据
Happy Coding! 🚀