feat: 更新 table 与 WebUI 静态资源

This commit is contained in:
2025-10-12 06:15:21 +08:00
parent c7cb1ae6c6
commit fc7eeb3696
4 changed files with 277 additions and 33 deletions

View File

@@ -268,12 +268,15 @@ func (t *Table) normalizeInsertData(data any) ([]map[string]any, error) {
typ = val.Type()
}
// 获取解引用后的实际值
actualData := val.Interface()
switch typ.Kind() {
case reflect.Map:
// map[string]any - 单条
m, ok := data.(map[string]any)
m, ok := actualData.(map[string]any)
if !ok {
return nil, fmt.Errorf("expected map[string]any, got %T", data)
return nil, fmt.Errorf("expected map[string]any, got %T", actualData)
}
return []map[string]any{m}, nil
@@ -283,9 +286,9 @@ func (t *Table) normalizeInsertData(data any) ([]map[string]any, error) {
// []map[string]any
if elemType.Kind() == reflect.Map {
maps, ok := data.([]map[string]any)
maps, ok := actualData.([]map[string]any)
if !ok {
return nil, fmt.Errorf("expected []map[string]any, got %T", data)
return nil, fmt.Errorf("expected []map[string]any, got %T", actualData)
}
return maps, nil
}
@@ -321,14 +324,14 @@ func (t *Table) normalizeInsertData(data any) ([]map[string]any, error) {
case reflect.Struct:
// struct{} - 单个结构体
m, err := t.structToMap(data)
m, err := t.structToMap(actualData)
if err != nil {
return nil, err
}
return []map[string]any{m}, nil
default:
return nil, fmt.Errorf("unsupported data type: %T (kind: %s)", data, typ.Kind())
return nil, fmt.Errorf("unsupported data type: %T (kind: %s)", actualData, typ.Kind())
}
}
@@ -356,32 +359,47 @@ func (t *Table) structToMap(v any) (map[string]any, error) {
continue
}
// 获取字段名
fieldName := field.Name
// 解析 srdb tag
tag := field.Tag.Get("srdb")
// 跳过忽略的字段
if tag == "-" {
// 忽略该字段
continue
}
// 解析 tag 获取字段名
// 默认使用 snake_case 转换字段名
fieldName := camelToSnake(field.Name)
// 解析 tag与 StructToFields 保持一致)
if tag != "" {
parts := strings.Split(tag, ";")
if parts[0] != "" {
fieldName = parts[0]
} else {
// 使用 snake_case 转换
fieldName = camelToSnake(field.Name)
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
// 检查是否为 field:xxx 格式
if strings.HasPrefix(part, "field:") {
fieldName = strings.TrimPrefix(part, "field:")
break // 找到字段名,停止解析
}
// 忽略其他标记indexed, nullable, comment:xxx
}
} else {
// 没有 tag使用 snake_case 转换
fieldName = camelToSnake(field.Name)
}
// 获取字段值
fieldVal := val.Field(i)
result[fieldName] = fieldVal.Interface()
// 处理指针类型如果是指针解引用nil 保持为 nil
if fieldVal.Kind() == reflect.Pointer {
if fieldVal.IsNil() {
result[fieldName] = nil
} else {
result[fieldName] = fieldVal.Elem().Interface()
}
} else {
result[fieldName] = fieldVal.Interface()
}
}
return result, nil

View File

@@ -7,18 +7,18 @@
--primary-light: #818cf8;
--primary-bg: rgba(99, 102, 241, 0.1);
/* 背景色 */
/* 背景色 - 深色主题(默认)*/
--bg-main: #0f0f1a;
--bg-surface: #1a1a2e;
--bg-elevated: #222236;
--bg-hover: #2a2a3e;
/* 文字颜色 */
/* 文字颜色 - 深色主题 */
--text-primary: #ffffff;
--text-secondary: #a0a0b0;
--text-tertiary: #6b6b7b;
/* 边框和分隔线 */
/* 边框和分隔线 - 深色主题 */
--border-color: rgba(255, 255, 255, 0.1);
--border-hover: rgba(255, 255, 255, 0.2);
@@ -47,6 +47,33 @@
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 浅色主题 */
:root[data-theme="light"] {
/* 背景色 - 浅色主题 */
--bg-main: #f5f5f7;
--bg-surface: #ffffff;
--bg-elevated: #ffffff;
--bg-hover: #f0f0f2;
/* 文字颜色 - 浅色主题 */
--text-primary: #1d1d1f;
--text-secondary: #6e6e73;
--text-tertiary: #86868b;
/* 边框和分隔线 - 浅色主题 */
--border-color: rgba(0, 0, 0, 0.1);
--border-hover: rgba(0, 0, 0, 0.2);
/* 阴影 - 浅色主题 */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md:
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg:
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl:
0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
* {
box-sizing: border-box;
margin: 0;

View File

@@ -18,6 +18,7 @@
"imports": {
"lit": "https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js",
"lit/": "https://cdn.jsdelivr.net/gh/lit/dist@3/",
"dom-align": "https://cdn.jsdelivr.net/npm/dom-align@1.12.4/+esm",
"~/": "/static/js/"
}
}

View File

@@ -1,5 +1,6 @@
import { LitElement, html, css } from 'lit';
import { sharedStyles, cssVariables } from '~/common/shared-styles.js';
import domAlign from 'dom-align';
export class DataView extends LitElement {
static properties = {
@@ -127,11 +128,7 @@ export class DataView extends LitElement {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-table td:hover {
white-space: normal;
word-break: break-all;
cursor: pointer;
}
.data-table tr:hover {
@@ -167,6 +164,113 @@ export class DataView extends LitElement {
this.tableData = null;
this.selectedColumns = [];
this.loading = false;
this.hidePopoverTimeout = null;
this.popoverElement = null;
this.themeObserver = null;
}
connectedCallback() {
super.connectedCallback();
// 创建 popover 元素并添加到 body
this.createPopoverElement();
}
disconnectedCallback() {
super.disconnectedCallback();
// 移除 popover 元素
this.removePopoverElement();
}
createPopoverElement() {
if (this.popoverElement) return;
this.popoverElement = document.createElement('div');
this.popoverElement.className = 'srdb-cell-popover';
// 从 CSS 变量中获取主题颜色
this.updatePopoverTheme();
// 添加滚动条样式(使用 CSS 变量)
if (!document.getElementById('srdb-popover-scrollbar-style')) {
const style = document.createElement('style');
style.id = 'srdb-popover-scrollbar-style';
style.textContent = `
.srdb-cell-popover::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.srdb-cell-popover::-webkit-scrollbar-track {
background: var(--bg-surface);
border-radius: 4px;
}
.srdb-cell-popover::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
.srdb-cell-popover::-webkit-scrollbar-thumb:hover {
background: var(--border-hover);
}
`;
document.head.appendChild(style);
}
this.popoverElement.addEventListener('mouseenter', () => this.keepPopover());
this.popoverElement.addEventListener('mouseleave', () => this.hidePopover());
document.body.appendChild(this.popoverElement);
// 监听主题变化
this.themeObserver = new MutationObserver(() => {
this.updatePopoverTheme();
});
this.themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
}
updatePopoverTheme() {
if (!this.popoverElement) return;
const rootStyles = getComputedStyle(document.documentElement);
const bgElevated = rootStyles.getPropertyValue('--bg-elevated').trim();
const textPrimary = rootStyles.getPropertyValue('--text-primary').trim();
const borderColor = rootStyles.getPropertyValue('--border-color').trim();
const shadowMd = rootStyles.getPropertyValue('--shadow-md').trim();
const radiusMd = rootStyles.getPropertyValue('--radius-md').trim();
this.popoverElement.style.cssText = `
position: fixed;
z-index: 9999;
background: ${bgElevated};
border: 1px solid ${borderColor};
border-radius: ${radiusMd};
box-shadow: ${shadowMd};
padding: 12px;
max-width: 500px;
max-height: 400px;
overflow: auto;
font-size: 13px;
color: ${textPrimary};
white-space: pre-wrap;
word-break: break-word;
font-family: 'Courier New', monospace;
opacity: 0;
transition: opacity 0.15s ease-in-out, background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
display: none;
pointer-events: auto;
`;
}
removePopoverElement() {
if (this.popoverElement) {
this.popoverElement.remove();
this.popoverElement = null;
}
if (this.themeObserver) {
this.themeObserver.disconnect();
this.themeObserver = null;
}
}
updated(changedProperties) {
@@ -244,6 +348,97 @@ export class DataView extends LitElement {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
formatValue(value) {
// 处理 null 和 undefined
if (value === null) return 'null';
if (value === undefined) return 'undefined';
// 处理 Object 和 Array 类型
if (typeof value === 'object') {
try {
return JSON.stringify(value);
} catch (e) {
return '[Object]';
}
}
// 其他类型直接返回
return value;
}
showPopover(event, value, col) {
if (!this.popoverElement) return;
// 清除之前的隐藏定时器
if (this.hidePopoverTimeout) {
clearTimeout(this.hidePopoverTimeout);
this.hidePopoverTimeout = null;
}
// 格式化值
let content = col === '_time' ? this.formatTime(value) : this.formatValue(value);
// 对于 JSON 对象/数组,格式化显示
if (typeof value === 'object' && value !== null) {
try {
content = JSON.stringify(value, null, 2);
} catch (e) {
content = String(content);
}
} else {
content = String(content);
}
// 只在内容较长时显示 popover
if (content.length < 50) {
return;
}
// 更新 popover 内容
this.popoverElement.textContent = content;
this.popoverElement.style.display = 'block';
// 使用 dom-align 进行智能定位
// 减小间隙到 2px方便鼠标移入
domAlign(this.popoverElement, event.target, {
points: ['tl', 'tr'], // popover左上角 对齐到 单元格右上角
offset: [2, 0], // 右侧间距仅2px便于鼠标移入
overflow: { adjustX: true, adjustY: true }
});
// 使用 setTimeout 确保 dom-align 完成后再显示
setTimeout(() => {
if (this.popoverElement) {
this.popoverElement.style.opacity = '1';
}
}, 10);
}
hidePopover() {
if (!this.popoverElement) return;
// 延迟隐藏,给用户足够时间移动鼠标到 popover300ms
this.hidePopoverTimeout = setTimeout(() => {
if (this.popoverElement) {
this.popoverElement.style.opacity = '0';
// 等待动画完成后再隐藏
setTimeout(() => {
if (this.popoverElement) {
this.popoverElement.style.display = 'none';
}
}, 150);
}
}, 300);
}
keepPopover() {
// 鼠标进入 popover 时,取消隐藏
if (this.hidePopoverTimeout) {
clearTimeout(this.hidePopoverTimeout);
this.hidePopoverTimeout = null;
}
}
getColumns() {
let columns = [];
@@ -274,9 +469,9 @@ export class DataView extends LitElement {
return html`
${this.renderSchemaSection()}
<h3>Data (${this.formatCount(this.tableData.totalRows)} rows)</h3>
${this.tableData.data.length === 0 ? html`
<div class="empty"><p>No data available</p></div>
` : html`
@@ -292,13 +487,16 @@ export class DataView extends LitElement {
${this.tableData.data.map(row => html`
<tr>
${columns.map(col => html`
<td>
${col === '_time' ? this.formatTime(row[col]) : row[col]}
<td
@mouseenter=${(e) => this.showPopover(e, row[col], col)}
@mouseleave=${() => this.hidePopover()}
>
${col === '_time' ? this.formatTime(row[col]) : this.formatValue(row[col])}
${row[col + '_truncated'] ? html`<span class="truncated-icon">✂️</span>` : ''}
</td>
`)}
<td style="text-align: center;">
<button
<button
class="row-detail-btn"
@click=${() => this.showRowDetail(row._seq)}
>