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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,6 +38,7 @@ testdb/
|
|||||||
# Example binaries and data
|
# Example binaries and data
|
||||||
/examples/*/data/
|
/examples/*/data/
|
||||||
/examples/*/*
|
/examples/*/*
|
||||||
|
!/examples/*/commands
|
||||||
!/examples/*/*.go
|
!/examples/*/*.go
|
||||||
!/examples/*/go.mod
|
!/examples/*/go.mod
|
||||||
!/examples/*/go.sum
|
!/examples/*/go.sum
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.tczkiot.com/wlw/srdb"
|
"code.tczkiot.com/wlw/srdb"
|
||||||
@@ -110,14 +111,20 @@ func StartWebUI(dbPath string, addr string) {
|
|||||||
// 启动后台数据插入协程
|
// 启动后台数据插入协程
|
||||||
go autoInsertData(db)
|
go autoInsertData(db)
|
||||||
|
|
||||||
// 启动 Web UI
|
// 创建 WebUI,使用 /debug 作为 basePath
|
||||||
handler := webui.NewWebUI(db)
|
ui := webui.NewWebUI(db, "/debug")
|
||||||
|
|
||||||
fmt.Printf("SRDB Web UI is running at http://%s\n", addr)
|
// 创建主路由
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
// 挂载 WebUI 到根路径(WebUI 内部会处理 /debug 前缀)
|
||||||
|
mux.Handle("/", ui)
|
||||||
|
|
||||||
|
fmt.Printf("SRDB Web UI is running at: http://%s/debug/\n", strings.TrimPrefix(addr, ":"))
|
||||||
fmt.Println("Press Ctrl+C to stop")
|
fmt.Println("Press Ctrl+C to stop")
|
||||||
fmt.Println("Background data insertion is running...")
|
fmt.Println("Background data insertion is running...")
|
||||||
|
|
||||||
if err := http.ListenAndServe(addr, handler); err != nil {
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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 -->
|
<!-- Import Map for Preact and HTM -->
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
@@ -16,7 +19,8 @@
|
|||||||
"preact": "https://esm.sh/preact@10.19.3",
|
"preact": "https://esm.sh/preact@10.19.3",
|
||||||
"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",
|
"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>
|
</script>
|
||||||
@@ -25,6 +29,6 @@
|
|||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<!-- Load Preact App -->
|
<!-- Load Preact App -->
|
||||||
<script type="module" src="/static/js/main.js"></script>
|
<script type="module" src="~~/static/js/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
import { Sidebar } from './Sidebar.js';
|
import { Sidebar } from '~/components/Sidebar.js';
|
||||||
import { TableView } from './TableView.js';
|
import { TableView } from '~/components/TableView.js';
|
||||||
|
import { getTables } from '~/utils/api.js';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [theme, setTheme] = useState('dark');
|
const [theme, setTheme] = useState('dark');
|
||||||
@@ -26,13 +27,10 @@ export function App() {
|
|||||||
const fetchTables = async () => {
|
const fetchTables = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch('/api/tables');
|
const data = await getTables();
|
||||||
if (response.ok) {
|
setTables(data.tables || []);
|
||||||
const data = await response.json();
|
if (data.tables && data.tables.length > 0) {
|
||||||
setTables(data.tables || []);
|
setSelectedTable(data.tables[0].name);
|
||||||
if (data.tables && data.tables.length > 0) {
|
|
||||||
setSelectedTable(data.tables[0].name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch tables:', error);
|
console.error('Failed to fetch tables:', error);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
import { RowDetailModal } from './RowDetailModal.js';
|
import { RowDetailModal } from '~/components/RowDetailModal.js';
|
||||||
import { Pagination } from './Pagination.js';
|
import { Pagination } from '~/components/Pagination.js';
|
||||||
import { TableRow } from './TableRow.js';
|
import { TableRow } from '~/components/TableRow.js';
|
||||||
import { useCellPopover } from '../hooks/useCellPopover.js';
|
import { useCellPopover } from '~/hooks/useCellPopover.js';
|
||||||
import { useTooltip } from '../hooks/useTooltip.js';
|
import { useTooltip } from '~/hooks/useTooltip.js';
|
||||||
|
import { getTableData } from '~/utils/api.js';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
container: {
|
container: {
|
||||||
@@ -83,11 +84,8 @@ export function DataTable({ schema, tableName, totalRows, selectedColumns = [] }
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const offset = page * pageSize;
|
const offset = page * pageSize;
|
||||||
const response = await fetch(`/api/tables/${tableName}/data?limit=${pageSize}&offset=${offset}`);
|
const result = await getTableData(tableName, { limit: pageSize, offset });
|
||||||
if (response.ok) {
|
setData(result.data || []);
|
||||||
const result = await response.json();
|
|
||||||
setData(result.data || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch data:', error);
|
console.error('Failed to fetch data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { FileCard } from './FileCard.js';
|
import { FileCard } from '~/components/FileCard.js';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
levelSection: {
|
levelSection: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
import { ManifestView } from './ManifestView.js';
|
import { ManifestView } from '~/components/ManifestView.js';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
overlay: {
|
overlay: {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
import { LevelCard } from './LevelCard.js';
|
import { LevelCard } from '~/components/LevelCard.js';
|
||||||
import { StatCard } from './StatCard.js';
|
import { StatCard } from '~/components/StatCard.js';
|
||||||
import { CompactionStats } from './CompactionStats.js';
|
import { CompactionStats } from '~/components/CompactionStats.js';
|
||||||
|
import { getTableManifest } from '~/utils/api.js';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
container: {
|
container: {
|
||||||
@@ -42,11 +43,8 @@ export function ManifestView({ tableName }) {
|
|||||||
|
|
||||||
const fetchManifest = async () => {
|
const fetchManifest = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/tables/${tableName}/manifest`);
|
const data = await getTableManifest(tableName);
|
||||||
if (response.ok) {
|
setManifest(data);
|
||||||
const data = await response.json();
|
|
||||||
setManifest(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch manifest:', error);
|
console.error('Failed to fetch manifest:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
import { useState, useEffect, useRef } from 'preact/hooks';
|
||||||
|
import { getRowBySeq } from '~/utils/api.js';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
overlay: {
|
overlay: {
|
||||||
@@ -116,11 +117,8 @@ export function RowDetailModal({ tableName, seq, onClose }) {
|
|||||||
const fetchRowData = async () => {
|
const fetchRowData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(`/api/tables/${tableName}/data/${seq}`);
|
const data = await getRowBySeq(tableName, seq);
|
||||||
if (response.ok) {
|
setRowData(data);
|
||||||
const data = await response.json();
|
|
||||||
setRowData(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch row data:', error);
|
console.error('Failed to fetch row data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { TableItem } from './TableItem.js';
|
import { TableItem } from '~/components/TableItem.js';
|
||||||
|
|
||||||
export function Sidebar({ tables, selectedTable, onSelectTable, loading }) {
|
export function Sidebar({ tables, selectedTable, onSelectTable, loading }) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { FieldList } from './FieldList.js';
|
import { FieldList } from '~/components/FieldList.js';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
tableItem: (isSelected, isExpanded) => ({
|
tableItem: (isSelected, isExpanded) => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { TableCell } from './TableCell.js';
|
import { TableCell } from '~/components/TableCell.js';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
td: {
|
td: {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { useState, useEffect } from 'preact/hooks';
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
import { DataTable } from './DataTable.js';
|
import { DataTable } from '~/components/DataTable.js';
|
||||||
import { ColumnSelector } from './ColumnSelector.js';
|
import { ColumnSelector } from '~/components/ColumnSelector.js';
|
||||||
import { ManifestModal } from './ManifestModal.js';
|
import { ManifestModal } from '~/components/ManifestModal.js';
|
||||||
import { useTooltip } from '../hooks/useTooltip.js';
|
import { useTooltip } from '~/hooks/useTooltip.js';
|
||||||
|
import { getTableSchema, getTableData } from '~/utils/api.js';
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
container: {
|
container: {
|
||||||
@@ -70,18 +71,12 @@ export function TableView({ tableName }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// 获取 Schema
|
// 获取 Schema
|
||||||
const schemaResponse = await fetch(`/api/tables/${tableName}/schema`);
|
const schemaData = await getTableSchema(tableName);
|
||||||
if (schemaResponse.ok) {
|
setSchema(schemaData);
|
||||||
const schemaData = await schemaResponse.json();
|
|
||||||
setSchema(schemaData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取数据行数(通过一次小查询)
|
// 获取数据行数(通过一次小查询)
|
||||||
const dataResponse = await fetch(`/api/tables/${tableName}/data?limit=1`);
|
const data = await getTableData(tableName, { limit: 1, offset: 0 });
|
||||||
if (dataResponse.ok) {
|
setTotalRows(data.totalRows || 0);
|
||||||
const data = await dataResponse.json();
|
|
||||||
setTotalRows(data.totalRows || 0);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch table info:', error);
|
console.error('Failed to fetch table info:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render } from 'preact';
|
import { render } from 'preact';
|
||||||
import { html } from 'htm/preact';
|
import { html } from 'htm/preact';
|
||||||
import { App } from './components/App.js';
|
import { App } from '~/components/App.js';
|
||||||
|
|
||||||
// 渲染应用
|
// 渲染应用
|
||||||
render(html`<${App} />`, document.getElementById('app'));
|
render(html`<${App} />`, document.getElementById('app'));
|
||||||
|
|||||||
110
webui/static/js/utils/api.js
Normal file
110
webui/static/js/utils/api.js
Normal 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();
|
||||||
|
}
|
||||||
@@ -20,12 +20,18 @@ var staticFS embed.FS
|
|||||||
// WebUI Web 界面处理器 v2 (Preact)
|
// WebUI Web 界面处理器 v2 (Preact)
|
||||||
type WebUI struct {
|
type WebUI struct {
|
||||||
db *srdb.Database
|
db *srdb.Database
|
||||||
|
basePath string
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebUI 创建 WebUI v2 实例
|
// NewWebUI 创建 WebUI v2 实例
|
||||||
func NewWebUI(db *srdb.Database) *WebUI {
|
// basePath 是可选的 URL 前缀路径(例如 "/debug"),如果为空则使用根路径 "/"
|
||||||
ui := &WebUI{db: db}
|
func NewWebUI(db *srdb.Database, basePath ...string) *WebUI {
|
||||||
|
bp := ""
|
||||||
|
if len(basePath) > 0 && basePath[0] != "" {
|
||||||
|
bp = strings.TrimSuffix(basePath[0], "/") // 移除尾部斜杠
|
||||||
|
}
|
||||||
|
ui := &WebUI{db: db, basePath: bp}
|
||||||
ui.handler = ui.setupHandler()
|
ui.handler = ui.setupHandler()
|
||||||
return ui
|
return ui
|
||||||
}
|
}
|
||||||
@@ -35,19 +41,32 @@ func (ui *WebUI) setupHandler() http.Handler {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// API endpoints - 纯 JSON API(与 v1 共享)
|
// API endpoints - 纯 JSON API(与 v1 共享)
|
||||||
mux.HandleFunc("/api/tables", ui.handleListTables)
|
mux.HandleFunc(ui.path("/api/tables"), ui.handleListTables)
|
||||||
mux.HandleFunc("/api/tables/", ui.handleTableAPI)
|
mux.HandleFunc(ui.path("/api/tables/"), ui.handleTableAPI)
|
||||||
|
|
||||||
// 静态文件服务
|
// 静态文件服务
|
||||||
staticFiles, _ := fs.Sub(staticFS, "static")
|
staticFiles, _ := fs.Sub(staticFS, "static")
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFiles))))
|
staticPath := ui.path("/static/")
|
||||||
|
mux.Handle(staticPath, http.StripPrefix(staticPath, http.FileServer(http.FS(staticFiles))))
|
||||||
|
|
||||||
// 首页
|
// 首页
|
||||||
mux.HandleFunc("/", ui.handleIndex)
|
mux.HandleFunc(ui.path("/"), ui.handleIndex)
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// path 返回带 basePath 前缀的路径
|
||||||
|
func (ui *WebUI) path(p string) string {
|
||||||
|
if ui.basePath == "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
// 确保路径以 / 开头
|
||||||
|
if !strings.HasPrefix(p, "/") {
|
||||||
|
p = "/" + p
|
||||||
|
}
|
||||||
|
return ui.basePath + p
|
||||||
|
}
|
||||||
|
|
||||||
// Handler 返回 HTTP Handler
|
// Handler 返回 HTTP Handler
|
||||||
func (ui *WebUI) Handler() http.Handler {
|
func (ui *WebUI) Handler() http.Handler {
|
||||||
return ui.handler
|
return ui.handler
|
||||||
@@ -124,7 +143,12 @@ func (ui *WebUI) handleListTables(w http.ResponseWriter, r *http.Request) {
|
|||||||
// handleTableAPI 处理表相关的 API 请求
|
// handleTableAPI 处理表相关的 API 请求
|
||||||
func (ui *WebUI) handleTableAPI(w http.ResponseWriter, r *http.Request) {
|
func (ui *WebUI) handleTableAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
// 解析路径: /api/tables/{name}/schema 或 /api/tables/{name}/data
|
// 解析路径: /api/tables/{name}/schema 或 /api/tables/{name}/data
|
||||||
path := strings.TrimPrefix(r.URL.Path, "/api/tables/")
|
// 需要先移除 basePath,再移除 /api/tables/ 前缀
|
||||||
|
path := r.URL.Path
|
||||||
|
if ui.basePath != "" {
|
||||||
|
path = strings.TrimPrefix(path, ui.basePath)
|
||||||
|
}
|
||||||
|
path = strings.TrimPrefix(path, "/api/tables/")
|
||||||
parts := strings.Split(path, "/")
|
parts := strings.Split(path, "/")
|
||||||
|
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
@@ -458,7 +482,9 @@ func (ui *WebUI) handleTableData(w http.ResponseWriter, r *http.Request, tableNa
|
|||||||
|
|
||||||
// handleIndex 处理首页请求
|
// handleIndex 处理首页请求
|
||||||
func (ui *WebUI) handleIndex(w http.ResponseWriter, r *http.Request) {
|
func (ui *WebUI) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
// 检查路径是否匹配(支持 basePath)
|
||||||
|
expectedPath := ui.path("/")
|
||||||
|
if r.URL.Path != expectedPath {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -471,6 +497,25 @@ func (ui *WebUI) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 替换路径占位符 ~~ 为 basePath
|
||||||
|
// 如果 basePath 为空,则 ~~ → ""
|
||||||
|
// 如果 basePath 为 "/debug",则 ~~ → "/debug"
|
||||||
|
// 示例:
|
||||||
|
// - 无 basePath: href="~~/static/css/styles.css" → href="/static/css/styles.css"
|
||||||
|
// - 有 basePath: href="~~/static/css/styles.css" → href="/debug/static/css/styles.css"
|
||||||
|
htmlContent := string(content)
|
||||||
|
if ui.basePath == "" {
|
||||||
|
// 无 basePath 时,直接替换为空
|
||||||
|
htmlContent = strings.ReplaceAll(htmlContent, `"~~"`, `""`)
|
||||||
|
htmlContent = strings.ReplaceAll(htmlContent, `"~~/`, `"/`)
|
||||||
|
htmlContent = strings.ReplaceAll(htmlContent, `'~~/`, `'/`)
|
||||||
|
} else {
|
||||||
|
// 有 basePath 时,替换为实际的 basePath
|
||||||
|
htmlContent = strings.ReplaceAll(htmlContent, `"~~"`, fmt.Sprintf(`"%s"`, ui.basePath))
|
||||||
|
htmlContent = strings.ReplaceAll(htmlContent, `"~~/`, fmt.Sprintf(`"%s/`, ui.basePath))
|
||||||
|
htmlContent = strings.ReplaceAll(htmlContent, `'~~/`, fmt.Sprintf(`'%s/`, ui.basePath))
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.Write(content)
|
w.Write([]byte(htmlContent))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user