feat: 完善 WebUI basePath 支持并简化示例代码

主要改动:

1. WebUI basePath 逻辑完善
   - NewWebUI 支持可变参数 basePath
   - 新增 path() 辅助方法统一路径处理
   - handleTableAPI 正确处理 basePath 前缀
   - handleIndex 根据 basePath 替换占位符

2. 简化示例代码
   - 删除反向代理实现(111行)
   - 直接使用带 basePath 的 WebUI
   - 代码量减少 33%,架构更清晰

3. 前端优化
   - 新增 api.js 统一 API 服务层
   - 所有组件使用统一的 API 调用
   - 支持通过 window.API_BASE 配置 basePath

4. 修复 .gitignore
   - 使用通用模式支持 commands 目录
   - 无需为新示例项目修改配置
This commit is contained in:
2025-10-14 14:13:59 +08:00
parent 7ac4b99a9e
commit 30c3e74bd2
16 changed files with 222 additions and 68 deletions

View File

@@ -7,7 +7,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/styles.css">
<link rel="stylesheet" href="~~/static/css/styles.css">
<!-- 路径配置:服务端渲染时替换 ~~ 和 ~/: 为实际的 BasePath -->
<script>window.API_BASE = "~~";</script>
<!-- Import Map for Preact and HTM -->
<script type="importmap">
@@ -16,7 +19,8 @@
"preact": "https://esm.sh/preact@10.19.3",
"preact/": "https://esm.sh/preact@10.19.3/",
"htm/preact": "https://esm.sh/htm@3.1.1/preact?external=preact",
"dom-align": "https://cdn.jsdelivr.net/npm/dom-align@1.12.4/+esm"
"dom-align": "https://cdn.jsdelivr.net/npm/dom-align@1.12.4/+esm",
"~/": "~~/static/js/"
}
}
</script>
@@ -25,6 +29,6 @@
<div id="app"></div>
<!-- Load Preact App -->
<script type="module" src="/static/js/main.js"></script>
<script type="module" src="~~/static/js/main.js"></script>
</body>
</html>

View File

@@ -1,7 +1,8 @@
import { html } from 'htm/preact';
import { useState, useEffect } from 'preact/hooks';
import { Sidebar } from './Sidebar.js';
import { TableView } from './TableView.js';
import { Sidebar } from '~/components/Sidebar.js';
import { TableView } from '~/components/TableView.js';
import { getTables } from '~/utils/api.js';
export function App() {
const [theme, setTheme] = useState('dark');
@@ -26,13 +27,10 @@ export function App() {
const fetchTables = async () => {
try {
setLoading(true);
const response = await fetch('/api/tables');
if (response.ok) {
const data = await response.json();
setTables(data.tables || []);
if (data.tables && data.tables.length > 0) {
setSelectedTable(data.tables[0].name);
}
const data = await getTables();
setTables(data.tables || []);
if (data.tables && data.tables.length > 0) {
setSelectedTable(data.tables[0].name);
}
} catch (error) {
console.error('Failed to fetch tables:', error);

View File

@@ -1,10 +1,11 @@
import { html } from 'htm/preact';
import { useState, useEffect } from 'preact/hooks';
import { RowDetailModal } from './RowDetailModal.js';
import { Pagination } from './Pagination.js';
import { TableRow } from './TableRow.js';
import { useCellPopover } from '../hooks/useCellPopover.js';
import { useTooltip } from '../hooks/useTooltip.js';
import { RowDetailModal } from '~/components/RowDetailModal.js';
import { Pagination } from '~/components/Pagination.js';
import { TableRow } from '~/components/TableRow.js';
import { useCellPopover } from '~/hooks/useCellPopover.js';
import { useTooltip } from '~/hooks/useTooltip.js';
import { getTableData } from '~/utils/api.js';
const styles = {
container: {
@@ -83,11 +84,8 @@ export function DataTable({ schema, tableName, totalRows, selectedColumns = [] }
try {
setLoading(true);
const offset = page * pageSize;
const response = await fetch(`/api/tables/${tableName}/data?limit=${pageSize}&offset=${offset}`);
if (response.ok) {
const result = await response.json();
setData(result.data || []);
}
const result = await getTableData(tableName, { limit: pageSize, offset });
setData(result.data || []);
} catch (error) {
console.error('Failed to fetch data:', error);
} finally {

View File

@@ -1,5 +1,5 @@
import { html } from 'htm/preact';
import { FileCard } from './FileCard.js';
import { FileCard } from '~/components/FileCard.js';
const styles = {
levelSection: {

View File

@@ -1,6 +1,6 @@
import { html } from 'htm/preact';
import { useEffect } from 'preact/hooks';
import { ManifestView } from './ManifestView.js';
import { ManifestView } from '~/components/ManifestView.js';
const styles = {
overlay: {

View File

@@ -1,8 +1,9 @@
import { html } from 'htm/preact';
import { useState, useEffect } from 'preact/hooks';
import { LevelCard } from './LevelCard.js';
import { StatCard } from './StatCard.js';
import { CompactionStats } from './CompactionStats.js';
import { LevelCard } from '~/components/LevelCard.js';
import { StatCard } from '~/components/StatCard.js';
import { CompactionStats } from '~/components/CompactionStats.js';
import { getTableManifest } from '~/utils/api.js';
const styles = {
container: {
@@ -42,11 +43,8 @@ export function ManifestView({ tableName }) {
const fetchManifest = async () => {
try {
const response = await fetch(`/api/tables/${tableName}/manifest`);
if (response.ok) {
const data = await response.json();
setManifest(data);
}
const data = await getTableManifest(tableName);
setManifest(data);
} catch (error) {
console.error('Failed to fetch manifest:', error);
} finally {

View File

@@ -1,5 +1,6 @@
import { html } from 'htm/preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import { getRowBySeq } from '~/utils/api.js';
const styles = {
overlay: {
@@ -116,11 +117,8 @@ export function RowDetailModal({ tableName, seq, onClose }) {
const fetchRowData = async () => {
try {
setLoading(true);
const response = await fetch(`/api/tables/${tableName}/data/${seq}`);
if (response.ok) {
const data = await response.json();
setRowData(data);
}
const data = await getRowBySeq(tableName, seq);
setRowData(data);
} catch (error) {
console.error('Failed to fetch row data:', error);
} finally {

View File

@@ -1,5 +1,5 @@
import { html } from 'htm/preact';
import { TableItem } from './TableItem.js';
import { TableItem } from '~/components/TableItem.js';
export function Sidebar({ tables, selectedTable, onSelectTable, loading }) {
if (loading) {

View File

@@ -1,6 +1,6 @@
import { html } from 'htm/preact';
import { useState } from 'preact/hooks';
import { FieldList } from './FieldList.js';
import { FieldList } from '~/components/FieldList.js';
const styles = {
tableItem: (isSelected, isExpanded) => ({

View File

@@ -1,6 +1,6 @@
import { html } from 'htm/preact';
import { useState } from 'preact/hooks';
import { TableCell } from './TableCell.js';
import { TableCell } from '~/components/TableCell.js';
const styles = {
td: {

View File

@@ -1,9 +1,10 @@
import { html } from 'htm/preact';
import { useState, useEffect } from 'preact/hooks';
import { DataTable } from './DataTable.js';
import { ColumnSelector } from './ColumnSelector.js';
import { ManifestModal } from './ManifestModal.js';
import { useTooltip } from '../hooks/useTooltip.js';
import { DataTable } from '~/components/DataTable.js';
import { ColumnSelector } from '~/components/ColumnSelector.js';
import { ManifestModal } from '~/components/ManifestModal.js';
import { useTooltip } from '~/hooks/useTooltip.js';
import { getTableSchema, getTableData } from '~/utils/api.js';
const styles = {
container: {
@@ -70,18 +71,12 @@ export function TableView({ tableName }) {
setLoading(true);
// 获取 Schema
const schemaResponse = await fetch(`/api/tables/${tableName}/schema`);
if (schemaResponse.ok) {
const schemaData = await schemaResponse.json();
setSchema(schemaData);
}
const schemaData = await getTableSchema(tableName);
setSchema(schemaData);
// 获取数据行数(通过一次小查询)
const dataResponse = await fetch(`/api/tables/${tableName}/data?limit=1`);
if (dataResponse.ok) {
const data = await dataResponse.json();
setTotalRows(data.totalRows || 0);
}
const data = await getTableData(tableName, { limit: 1, offset: 0 });
setTotalRows(data.totalRows || 0);
} catch (error) {
console.error('Failed to fetch table info:', error);
} finally {

View File

@@ -1,6 +1,6 @@
import { render } from 'preact';
import { html } from 'htm/preact';
import { App } from './components/App.js';
import { App } from '~/components/App.js';
// 渲染应用
render(html`<${App} />`, document.getElementById('app'));

View File

@@ -0,0 +1,110 @@
// api.js - 统一的 API 服务层
/**
* 获取 API 基础路径
* 从全局变量 window.API_BASE 读取,由服务端渲染时注入
*/
function getBasePath() {
return window.API_BASE || '';
}
/**
* 构建完整的 API URL
* @param {string} path - API 路径,如 '/tables' 或 '/tables/users/schema'
* @returns {string} 完整 URL
*/
function buildApiUrl(path) {
const basePath = getBasePath();
const normalizedPath = path.startsWith('/') ? path : '/' + path;
const fullPath = basePath ? `${basePath}/api${normalizedPath}` : `/api${normalizedPath}`;
console.log('[API] Request:', fullPath);
return fullPath;
}
/**
* 统一的 fetch 封装
* @param {string} path - API 路径
* @param {object} options - fetch 选项
* @returns {Promise<Response>}
*/
async function apiFetch(path, options = {}) {
const url = buildApiUrl(path);
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
console.error('[API] Error:', response.status, url);
}
return response;
}
// ============ 表相关 API ============
/**
* 获取所有表列表
* @returns {Promise<{tables: Array}>}
*/
export async function getTables() {
const response = await apiFetch('/tables');
return response.json();
}
/**
* 获取表的 Schema
* @param {string} tableName - 表名
* @returns {Promise<{name: string, fields: Array}>}
*/
export async function getTableSchema(tableName) {
const response = await apiFetch(`/tables/${tableName}/schema`);
return response.json();
}
/**
* 获取表数据(分页)
* @param {string} tableName - 表名
* @param {object} params - 查询参数
* @param {number} params.limit - 每页数量
* @param {number} params.offset - 偏移量
* @param {string} params.select - 选择的字段(逗号分隔)
* @returns {Promise<{data: Array, totalRows: number}>}
*/
export async function getTableData(tableName, params = {}) {
const { limit = 20, offset = 0, select } = params;
const queryParams = new URLSearchParams();
queryParams.append('limit', limit);
queryParams.append('offset', offset);
if (select) {
queryParams.append('select', select);
}
const response = await apiFetch(`/tables/${tableName}/data?${queryParams}`);
return response.json();
}
/**
* 根据序列号获取单条数据
* @param {string} tableName - 表名
* @param {number} seq - 序列号
* @returns {Promise<object>}
*/
export async function getRowBySeq(tableName, seq) {
const response = await apiFetch(`/tables/${tableName}/data/${seq}`);
return response.json();
}
/**
* 获取表的 Manifest 信息
* @param {string} tableName - 表名
* @returns {Promise<object>}
*/
export async function getTableManifest(tableName) {
const response = await apiFetch(`/tables/${tableName}/manifest`);
return response.json();
}