feat: 更新 table 与 WebUI 静态资源
This commit is contained in:
58
table.go
58
table.go
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 延迟隐藏,给用户足够时间移动鼠标到 popover(300ms)
|
||||
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)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user