前端:重构 Web UI 代码结构

- 添加 Import Map 支持 Lit 和本地模块的简洁导入
- 创建统一的 API 管理模块 (common/api.js)
- 重命名 styles/ 为 common/ 目录
- 修复分页时列选择被重置的问题
- 将 app.js 重命名为 main.js
- 所有导入路径使用 ~ 别名映射
This commit is contained in:
2025-10-09 15:53:58 +08:00
parent 8019f2d794
commit 4aade1cff1
20 changed files with 1349 additions and 421 deletions

View File

@@ -11,6 +11,17 @@
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/css/styles.css" />
<!-- Import Map for Lit and local modules -->
<script type="importmap">
{
"imports": {
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js",
"lit/": "https://cdn.jsdelivr.net/gh/lit/dist@3/",
"~/": "/static/js/"
}
}
</script>
</head>
<body>
<!-- 应用容器 -->
@@ -20,6 +31,6 @@
<srdb-modal-dialog></srdb-modal-dialog>
<!-- 加载 Lit 组件 -->
<script type="module" src="/static/js/app.js"></script>
<script type="module" src="/static/js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,174 @@
/**
* API 请求管理模块
* 统一管理所有后端接口请求
*/
const API_BASE = '/api';
/**
* 通用请求处理函数
* @param {string} url - 请求 URL
* @param {RequestInit} options - fetch 选项
* @returns {Promise<any>}
*/
async function request(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = response;
throw error;
}
return await response.json();
} catch (error) {
console.error('API request failed:', url, error);
throw error;
}
}
/**
* 表相关 API
*/
export const tableAPI = {
/**
* 获取所有表列表
* @returns {Promise<Array>}
*/
async list() {
return request(`${API_BASE}/tables`);
},
/**
* 获取表的 Schema
* @param {string} tableName - 表名
* @returns {Promise<Object>}
*/
async getSchema(tableName) {
return request(`${API_BASE}/tables/${tableName}/schema`);
},
/**
* 获取表数据(分页)
* @param {string} tableName - 表名
* @param {Object} params - 查询参数
* @param {number} params.page - 页码
* @param {number} params.pageSize - 每页大小
* @param {string} params.select - 选择的列(逗号分隔)
* @returns {Promise<Object>}
*/
async getData(tableName, { page = 1, pageSize = 20, select = '' } = {}) {
const params = new URLSearchParams({
page: page.toString(),
pageSize: pageSize.toString(),
});
if (select) {
params.append('select', select);
}
return request(`${API_BASE}/tables/${tableName}/data?${params}`);
},
/**
* 获取单行数据详情
* @param {string} tableName - 表名
* @param {number} seq - 序列号
* @returns {Promise<Object>}
*/
async getRow(tableName, seq) {
return request(`${API_BASE}/tables/${tableName}/data/${seq}`);
},
/**
* 获取表的 Manifest 信息
* @param {string} tableName - 表名
* @returns {Promise<Object>}
*/
async getManifest(tableName) {
return request(`${API_BASE}/tables/${tableName}/manifest`);
},
/**
* 插入数据
* @param {string} tableName - 表名
* @param {Object} data - 数据对象
* @returns {Promise<Object>}
*/
async insert(tableName, data) {
return request(`${API_BASE}/tables/${tableName}/data`, {
method: 'POST',
body: JSON.stringify(data),
});
},
/**
* 批量插入数据
* @param {string} tableName - 表名
* @param {Array<Object>} data - 数据数组
* @returns {Promise<Object>}
*/
async batchInsert(tableName, data) {
return request(`${API_BASE}/tables/${tableName}/data/batch`, {
method: 'POST',
body: JSON.stringify(data),
});
},
/**
* 删除表
* @param {string} tableName - 表名
* @returns {Promise<Object>}
*/
async delete(tableName) {
return request(`${API_BASE}/tables/${tableName}`, {
method: 'DELETE',
});
},
/**
* 获取表统计信息
* @param {string} tableName - 表名
* @returns {Promise<Object>}
*/
async getStats(tableName) {
return request(`${API_BASE}/tables/${tableName}/stats`);
},
};
/**
* 数据库相关 API
*/
export const databaseAPI = {
/**
* 获取数据库信息
* @returns {Promise<Object>}
*/
async getInfo() {
return request(`${API_BASE}/database/info`);
},
/**
* 获取数据库统计信息
* @returns {Promise<Object>}
*/
async getStats() {
return request(`${API_BASE}/database/stats`);
},
};
/**
* 导出默认 API 对象
*/
export default {
table: tableAPI,
database: databaseAPI,
};

View File

@@ -1,4 +1,4 @@
import { css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { css } from 'lit';
// 共享的基础样式
export const sharedStyles = css`

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class AppContainer extends LitElement {
static properties = {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class Badge extends LitElement {
static properties = {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class DataView extends LitElement {
static properties = {

View File

@@ -1,10 +1,9 @@
import { LitElement, html, css, svg } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { LitElement, html, css } from 'lit';
export class FieldIcon extends LitElement {
static properties = {
indexed: { type: Boolean }
};
static styles = css`
:host {
display: inline-flex;

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class ManifestView extends LitElement {
static properties = {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class ModalDialog extends LitElement {
static properties = {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
export class PageHeader extends LitElement {
static properties = {
@@ -247,11 +247,11 @@ export class PageHeader extends LitElement {
>
<span>Data</span>
</button>
<button
<button
class="view-tab ${this.view === 'manifest' ? 'active' : ''}"
@click=${() => this.switchView('manifest')}
>
<span>Manifest / LSM-Tree</span>
<span>Manifest / Storage Layers</span>
</button>
<button class="refresh-btn" @click=${this.refreshView} title="Refresh current view">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">

View File

@@ -1,5 +1,6 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
import { tableAPI } from '~/common/api.js';
export class TableList extends LitElement {
static properties = {
@@ -209,9 +210,7 @@ export class TableList extends LitElement {
async loadTables() {
try {
const response = await fetch('/api/tables');
if (!response.ok) throw new Error('Failed to load tables');
this.tables = await response.json();
this.tables = await tableAPI.list();
} catch (error) {
console.error('Error loading tables:', error);
}

View File

@@ -1,5 +1,6 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles, cssVariables } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
import { tableAPI } from '~/common/api.js';
export class TableView extends LitElement {
static properties = {
@@ -127,13 +128,20 @@ export class TableView extends LitElement {
try {
// Load schema
const schemaResponse = await fetch(`/api/tables/${this.tableName}/schema`);
if (!schemaResponse.ok) throw new Error('Failed to load schema');
this.schema = await schemaResponse.json();
this.schema = await tableAPI.getSchema(this.tableName);
// Initialize selected columns (all by default)
// Initialize selected columns from localStorage or all by default
if (this.schema.fields) {
this.selectedColumns = this.schema.fields.map(f => f.name);
const saved = this.loadSelectedColumns();
if (saved && saved.length > 0) {
// 验证保存的列是否仍然存在于当前 schema 中
const validColumns = saved.filter(col =>
this.schema.fields.some(field => field.name === col)
);
this.selectedColumns = validColumns.length > 0 ? validColumns : this.schema.fields.map(f => f.name);
} else {
this.selectedColumns = this.schema.fields.map(f => f.name);
}
}
if (this.view === 'data') {
@@ -149,24 +157,28 @@ export class TableView extends LitElement {
}
async loadTableData() {
const selectParam = this.selectedColumns.join(',');
const url = `/api/tables/${this.tableName}/data?page=${this.page}&pageSize=${this.pageSize}&select=${selectParam}`;
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to load table data');
this.tableData = await response.json();
this.tableData = await tableAPI.getData(this.tableName, {
page: this.page,
pageSize: this.pageSize,
select: this.selectedColumns.join(',')
});
}
async loadManifestData() {
const response = await fetch(`/api/tables/${this.tableName}/manifest`);
if (!response.ok) throw new Error('Failed to load manifest data');
this.manifestData = await response.json();
this.manifestData = await tableAPI.getManifest(this.tableName);
}
switchView(newView) {
this.view = newView;
}
loadSelectedColumns() {
if (!this.tableName) return null;
const key = `srdb_columns_${this.tableName}`;
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : null;
}
toggleColumn(columnName) {
const index = this.selectedColumns.indexOf(columnName);
if (index > -1) {

View File

@@ -1,5 +1,5 @@
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
import { sharedStyles } from '../styles/shared-styles.js';
import { LitElement, html, css } from 'lit';
import { sharedStyles } from '~/common/shared-styles.js';
export class ThemeToggle extends LitElement {
static properties = {

View File

@@ -1,13 +1,14 @@
import './components/app.js';
import './components/table-list.js';
import './components/table-view.js';
import './components/modal-dialog.js';
import './components/theme-toggle.js';
import './components/badge.js';
import './components/field-icon.js';
import './components/data-view.js';
import './components/manifest-view.js';
import './components/page-header.js';
import '~/components/app.js';
import '~/components/table-list.js';
import '~/components/table-view.js';
import '~/components/modal-dialog.js';
import '~/components/theme-toggle.js';
import '~/components/badge.js';
import '~/components/field-icon.js';
import '~/components/data-view.js';
import '~/components/manifest-view.js';
import '~/components/page-header.js';
import { tableAPI } from '~/common/api.js';
class App {
constructor() {
@@ -78,10 +79,7 @@ class App {
async showRowDetail(tableName, seq) {
try {
const response = await fetch(`/api/tables/${tableName}/data/${seq}`);
if (!response.ok) throw new Error('Failed to load row detail');
const data = await response.json();
const data = await tableAPI.getRow(tableName, seq);
const content = JSON.stringify(data, null, 2);
this.modal.title = `Row Detail - Seq: ${seq}`;