Files
srdb/webui/static/js/components/table-view.js
bourdon 4aade1cff1 前端:重构 Web UI 代码结构
- 添加 Import Map 支持 Lit 和本地模块的简洁导入
- 创建统一的 API 管理模块 (common/api.js)
- 重命名 styles/ 为 common/ 目录
- 修复分页时列选择被重置的问题
- 将 app.js 重命名为 main.js
- 所有导入路径使用 ~ 别名映射
2025-10-09 19:30:20 +08:00

377 lines
9.5 KiB
JavaScript

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 = {
tableName: { type: String },
view: { type: String }, // 'data' or 'manifest'
schema: { type: Object },
tableData: { type: Object },
manifestData: { type: Object },
selectedColumns: { type: Array },
page: { type: Number },
pageSize: { type: Number },
loading: { type: Boolean }
};
static styles = [
sharedStyles,
cssVariables,
css`
:host {
display: flex;
flex-direction: column;
width: 100%;
flex: 1;
position: relative;
overflow: hidden;
}
.content-wrapper {
flex: 1;
overflow-y: auto;
padding: 24px;
padding-bottom: 80px;
}
.pagination {
position: fixed;
bottom: 0;
left: 280px;
right: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 16px 24px;
background: var(--bg-elevated);
border-top: 1px solid var(--border-color);
z-index: 10;
}
@media (max-width: 768px) {
.pagination {
left: 0;
}
}
.pagination button {
padding: 8px 16px;
background: var(--bg-surface);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
cursor: pointer;
transition: var(--transition);
}
.pagination button:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--border-hover);
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination select,
.pagination input {
padding: 8px 12px;
background: var(--bg-surface);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
}
.pagination input {
width: 80px;
}
.pagination span {
color: var(--text-primary);
font-size: 14px;
}
`
];
constructor() {
super();
this.tableName = '';
this.view = 'data';
this.schema = null;
this.tableData = null;
this.manifestData = null;
this.selectedColumns = [];
this.page = 1;
this.pageSize = 20;
this.loading = false;
}
updated(changedProperties) {
if (changedProperties.has('tableName') && this.tableName) {
// 切换表时重置选中的列
this.selectedColumns = [];
this.page = 1;
this.loadData();
}
if (changedProperties.has('view') && this.tableName) {
this.loadData();
}
}
async loadData() {
if (!this.tableName) return;
this.loading = true;
try {
// Load schema
this.schema = await tableAPI.getSchema(this.tableName);
// Initialize selected columns from localStorage or all by default
if (this.schema.fields) {
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') {
await this.loadTableData();
} else if (this.view === 'manifest') {
await this.loadManifestData();
}
} catch (error) {
console.error('Error loading data:', error);
} finally {
this.loading = false;
}
}
async loadTableData() {
this.tableData = await tableAPI.getData(this.tableName, {
page: this.page,
pageSize: this.pageSize,
select: this.selectedColumns.join(',')
});
}
async loadManifestData() {
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) {
this.selectedColumns = this.selectedColumns.filter(c => c !== columnName);
} else {
this.selectedColumns = [...this.selectedColumns, columnName];
}
this.loadTableData();
}
changePage(delta) {
this.page = Math.max(1, this.page + delta);
this.loadTableData();
}
changePageSize(newSize) {
this.pageSize = parseInt(newSize);
this.page = 1;
this.loadTableData();
}
jumpToPage(pageNum) {
const num = parseInt(pageNum);
if (num > 0 && this.tableData && num <= this.tableData.totalPages) {
this.page = num;
this.loadTableData();
}
}
showRowDetail(seq) {
this.dispatchEvent(new CustomEvent('show-row-detail', {
detail: { tableName: this.tableName, seq },
bubbles: true,
composed: true
}));
}
toggleLevel(level) {
const levelCard = this.shadowRoot.querySelector(`[data-level="${level}"]`);
if (levelCard) {
const fileList = levelCard.querySelector('.file-list');
const icon = levelCard.querySelector('.expand-icon');
if (fileList.classList.contains('expanded')) {
fileList.classList.remove('expanded');
icon.style.transform = 'rotate(0deg)';
} else {
fileList.classList.add('expanded');
icon.style.transform = 'rotate(90deg)';
}
}
}
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
}
formatCount(count) {
if (count >= 1000000) return (count / 1000000).toFixed(1) + 'M';
if (count >= 1000) return (count / 1000).toFixed(1) + 'K';
return count.toString();
}
render() {
if (!this.tableName) {
return html`
<div class="empty">
<h2>Select a table to view data</h2>
<p>Choose a table from the sidebar to get started</p>
</div>
`;
}
if (this.loading) {
return html`<div class="loading">Loading...</div>`;
}
return html`
<div class="content-wrapper">
${this.view === 'data' ? html`
<srdb-data-view
.tableName=${this.tableName}
.schema=${this.schema}
.tableData=${this.tableData}
.selectedColumns=${this.selectedColumns}
.loading=${this.loading}
@columns-changed=${(e) => {
this.selectedColumns = e.detail.columns;
this.loadTableData();
}}
@show-row-detail=${(e) => this.showRowDetail(e.detail.seq)}
></srdb-data-view>
` : html`
<srdb-manifest-view
.manifestData=${this.manifestData}
.loading=${this.loading}
></srdb-manifest-view>
`}
</div>
${this.view === 'data' && this.tableData ? this.renderPagination() : ''}
`;
}
renderPagination() {
return html`
<div class="pagination">
<select @change=${(e) => this.changePageSize(e.target.value)}>
${[10, 20, 50, 100].map(size => html`
<option value="${size}" ?selected=${size === this.pageSize}>
${size} / page
</option>
`)}
</select>
<button
@click=${() => this.changePage(-1)}
?disabled=${this.page <= 1}
>
Previous
</button>
<span>
Page ${this.page} of ${this.tableData.totalPages}
(${this.formatCount(this.tableData.totalRows)} rows)
</span>
<input
type="number"
min="1"
max="${this.tableData.totalPages}"
placeholder="Jump to"
@keydown=${(e) => e.key === 'Enter' && this.jumpToPage(e.target.value)}
/>
<button @click=${(e) => this.jumpToPage(e.target.previousElementSibling.value)}>
Go
</button>
<button
@click=${() => this.changePage(1)}
?disabled=${this.page >= this.tableData.totalPages}
>
Next
</button>
</div>
`;
}
formatCount(count) {
if (count >= 1000000) return (count / 1000000).toFixed(1) + 'M';
if (count >= 1000) return (count / 1000).toFixed(1) + 'K';
return count.toString();
}
changePage(delta) {
this.page = Math.max(1, this.page + delta);
this.loadTableData();
}
changePageSize(newSize) {
this.pageSize = parseInt(newSize);
this.page = 1;
this.loadTableData();
}
jumpToPage(pageNum) {
const num = parseInt(pageNum);
if (num > 0 && this.tableData && num <= this.tableData.totalPages) {
this.page = num;
this.loadTableData();
}
}
showRowDetail(seq) {
this.dispatchEvent(new CustomEvent('show-row-detail', {
detail: { tableName: this.tableName, seq },
bubbles: true,
composed: true
}));
}
showCellContent(content) {
this.dispatchEvent(new CustomEvent('show-cell-content', {
detail: { content },
bubbles: true,
composed: true
}));
}
}
customElements.define('srdb-table-view', TableView);