Compare commits

...

5 Commits

Author SHA1 Message Date
5ad4e7cef6 refactor: 移除侧边栏中的系统信息和设置按钮 2025-08-06 15:26:19 +08:00
38cbf669f5 fix: 黑暗模式下左侧菜单展示不清晰问题 2025-08-06 15:23:05 +08:00
690e9bdbf4 fix: dark mode 2025-08-06 15:20:08 +08:00
aa20b6d7e6 fix: errors 2025-08-06 14:59:39 +08:00
c4ad0c1dc9 feat: update ui 2025-08-06 11:12:16 +08:00
16 changed files with 5132 additions and 237 deletions

1610
UI.md Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

11
errors.md Normal file
View File

@@ -0,0 +1,11 @@
## 待修复问题
1. 左侧表展示排序会随机变化,需要固定排序
2. 左侧表搜索无法支持原始表名搜索,需要同时展示原始表名并支持搜索
3. 详情弹出层内容无 CSS 样式,展示混乱。且弹出层关闭按钮 icon 肉眼不可见
4. toggleTheme 报错
```
应用错误: ReferenceError: toggleTheme is not defined
at HTMLButtonElement.onclick (?table=posts:32:40)
```
5. 文章表多写入些数据,方便测试分页功能。

View File

@@ -412,6 +412,14 @@ func (cm *ConnectionManager) isSearchableColumn(columnType string) bool {
return false
}
// GetTableRowCount returns the total number of rows in a table
func (cm *ConnectionManager) GetTableRowCount(tableName string) (int64, error) {
var count int64
query := fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)
err := cm.db.Raw(query).Scan(&count).Error
return count, err
}
// GetTableDataByID retrieves a single record by ID
func (cm *ConnectionManager) GetTableDataByID(tableName string, id interface{}) (map[string]interface{}, error) {
// Find primary key column

View File

@@ -61,6 +61,8 @@ type TableResponse struct {
type TableInfo struct {
Name string `json:"name"`
Alias string `json:"alias"`
Description string `json:"description"`
RowCount int64 `json:"row_count"`
}
// DetailResponse represents a single record detail response

View File

@@ -3,6 +3,7 @@ package repository
import (
"fmt"
"log/slog"
"sort"
"github.com/rogeecn/database_render/internal/config"
"github.com/rogeecn/database_render/internal/database"
@@ -30,12 +31,34 @@ func (r *DataRepository) GetTables() ([]model.TableInfo, error) {
var tables []model.TableInfo
for name, tableConfig := range r.config.Tables {
// Get row count for each table
rowCount, err := r.db.GetTableRowCount(name)
if err != nil {
r.logger.Error("failed to get row count for table", "table", name, "error", err)
rowCount = 0
}
// Use table alias as description if not provided
description := tableConfig.Alias
if desc, ok := tableConfig.Options["description"]; ok {
if descStr, ok := desc.(string); ok {
description = descStr
}
}
tables = append(tables, model.TableInfo{
Name: name,
Alias: tableConfig.Alias,
Description: description,
RowCount: rowCount,
})
}
// Sort tables by name to ensure consistent ordering
sort.Slice(tables, func(i, j int) bool {
return tables[i].Name < tables[j].Name
})
return tables, nil
}

View File

@@ -123,6 +123,16 @@ func (r *Renderer) loadTemplates() error {
"eq": func(a, b interface{}) bool {
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
},
"until": func(n int) []int {
result := make([]int, n)
for i := 0; i < n; i++ {
result[i] = i + 1
}
return result
},
"ge": func(a, b int) bool {
return a >= b
},
}
// Load templates
@@ -184,6 +194,14 @@ func (r *Renderer) RenderList(c fiber.Ctx, tableName string) error {
return c.Status(http.StatusInternalServerError).SendString("Failed to load table configuration")
}
// Calculate record range
startRecord := int64((data.Page-1)*data.PerPage + 1)
endRecord := startRecord + int64(len(data.Data)) - 1
if data.Total == 0 {
startRecord = 0
endRecord = 0
}
// Prepare template data
templateData := map[string]interface{}{
"Table": tableName,
@@ -199,6 +217,9 @@ func (r *Renderer) RenderList(c fiber.Ctx, tableName string) error {
"SortOrder": sortOrder,
"Tables": tables,
"CurrentPath": c.Path(),
"StartRecord": startRecord,
"EndRecord": endRecord,
"LastUpdate": time.Now().Format("2006-01-02 15:04:05"),
}
// set content-type html

View File

@@ -0,0 +1,17 @@
-- 为posts表插入更多测试数据
INSERT INTO posts (title, content, category, tags, author_id, status, view_count, created_at, updated_at) VALUES
('深入理解Go语言并发编程', 'Go语言的并发模型基于CSPCommunicating Sequential Processes理论通过goroutine和channel实现高效的并发编程...', '技术', 'go,并发,编程', 1, 'published', 1250, datetime('now', '-1 day'), datetime('now')),
('微服务架构设计实践', '微服务架构通过将单体应用拆分为一组小型服务,每个服务运行在自己的进程中,服务间通过轻量级通信机制进行交互...', '架构', '微服务,架构,设计', 2, 'published', 890, datetime('now', '-2 days'), datetime('now')),
('Docker容器化技术详解', 'Docker是一种开源的容器化平台它允许开发者将应用程序及其依赖打包到一个可移植的容器中...', '容器', 'docker,容器,kubernetes', 3, 'published', 2100, datetime('now', '-3 days'), datetime('now')),
('Kubernetes集群管理入门', 'Kubernetes是一个开源的容器编排平台用于自动化容器化应用程序的部署、扩展和管理...', '容器', 'kubernetes,容器,编排', 4, 'published', 1560, datetime('now', '-4 days'), datetime('now')),
('现代前端框架对比分析', 'React、Vue和Angular是目前最流行的三大前端框架本文将从性能、学习曲线、生态系统等方面进行对比...', '前端', 'react,vue,angular,前端', 1, 'published', 2340, datetime('now', '-5 days'), datetime('now')),
('数据库性能优化策略', '数据库性能优化是系统性能调优的重要组成部分,本文将介绍索引优化、查询优化、缓存策略等多个方面...', '数据库', '数据库,性能优化,索引', 2, 'published', 1780, datetime('now', '-6 days'), datetime('now')),
('RESTful API设计最佳实践', 'RESTful API是目前最流行的API设计风格本文将介绍RESTful API的设计原则、最佳实践和常见误区...', 'API', 'restful,api,设计', 3, 'published', 3200, datetime('now', '-7 days'), datetime('now')),
('CI/CD流水线搭建指南', '持续集成和持续部署是现代软件开发的重要实践本文将详细介绍如何搭建高效的CI/CD流水线...', 'DevOps', 'ci/cd,devops,自动化', 4, 'published', 980, datetime('now', '-8 days'), datetime('now')),
('云原生应用开发实战', '云原生是一种构建和运行应用程序的方法,充分利用云计算的弹性、可扩展性和分布式特性...', '云原生', '云原生,kubernetes,docker', 1, 'published', 1450, datetime('now', '-9 days'), datetime('now')),
('分布式系统架构设计', '分布式系统是现代互联网应用的基础本文将探讨分布式系统的架构设计原则、CAP理论、一致性算法等...', '架构', '分布式,架构,cap理论', 2, 'published', 2670, datetime('now', '-10 days'), datetime('now')),
('DevOps文化与实践', 'DevOps是一种文化和实践强调开发团队和运维团队之间的协作以实现更快、更可靠的软件交付...', 'DevOps', 'devops,文化,协作', 3, 'published', 1120, datetime('now', '-11 days'), datetime('now')),
('大数据处理技术综述', '大数据技术栈包括数据采集、存储、处理、分析和可视化等多个环节,本文将全面介绍当前主流的大数据技术...', '大数据', '大数据,hadoop,spark', 4, 'published', 1890, datetime('now', '-12 days'), datetime('now')),
('人工智能与机器学习入门', '人工智能和机器学习正在改变我们的生活和工作方式本文将介绍AI/ML的基本概念、算法和应用场景...', 'AI/ML', '人工智能,机器学习,深度学习', 1, 'published', 3560, datetime('now', '-13 days'), datetime('now')),
('网络安全防护策略', '网络安全是信息技术发展的重要保障,本文将介绍常见的网络攻击类型、防护策略和安全最佳实践...', '安全', '网络安全,安全防护,最佳实践', 2, 'published', 2340, datetime('now', '-14 days'), datetime('now')),
('移动应用开发趋势', '移动应用开发技术日新月异本文将分析2024年移动应用开发的最新趋势和技术栈选择...', '移动开发', '移动开发,react native,flutter', 3, 'published', 1670, datetime('now', '-15 days'), datetime('now'));

38
scripts/test_fixes.sh Executable file
View File

@@ -0,0 +1,38 @@
#!/bin/bash
echo "🧪 测试修复结果"
echo "=================="
# 1. 测试排序稳定性
echo "1. 检查表格排序稳定性..."
sqlite3 data/app.db "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;" > /tmp/table_order1.txt
sqlite3 data/app.db "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;" > /tmp/table_order2.txt
if diff /tmp/table_order1.txt /tmp/table_order2.txt > /dev/null; then
echo " ✅ 表格排序稳定"
else
echo " ❌ 表格排序不稳定"
fi
# 2. 测试数据量
echo "2. 检查posts表数据量..."
count=$(sqlite3 data/app.db "SELECT COUNT(*) FROM posts;")
echo " 📊 posts表共有 $count 条记录"
if [ "$count" -gt 20 ]; then
echo " ✅ 数据量充足,支持分页测试"
else
echo " ❌ 数据量不足"
fi
# 3. 检查数据库连接
echo "3. 检查数据库连接..."
if sqlite3 data/app.db ".tables" > /dev/null 2>&1; then
echo " ✅ 数据库连接正常"
else
echo " ❌ 数据库连接失败"
fi
echo ""
echo "🎉 所有修复测试完成!"
echo "请启动应用验证:"
echo " make dev" 或者 "go run cmd/server/main.go"

541
web/static/css/app.css Normal file
View File

@@ -0,0 +1,541 @@
/* 基础样式重置和CSS变量定义 */
:root {
/* 主色调 - 中性专业 */
--primary-50: #f8fafc;
--primary-100: #f1f5f9;
--primary-200: #e2e8f0;
--primary-300: #cbd5e1;
--primary-400: #94a3b8;
--primary-500: #64748b;
--primary-600: #475569;
--primary-700: #334155;
--primary-800: #1e293b;
--primary-900: #0f172a;
/* 强调色 - 蓝色系 */
--accent-50: #eff6ff;
--accent-100: #dbeafe;
--accent-200: #bfdbfe;
--accent-300: #93c5fd;
--accent-400: #60a5fa;
--accent-500: #3b82f6;
--accent-600: #2563eb;
--accent-700: #1d4ed8;
--accent-800: #1e40af;
--accent-900: #1e3a8a;
/* 状态色 */
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--info: #06b6d4;
/* 背景色 */
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--bg-tertiary: #f1f5f9;
/* 文字色 */
--text-primary: #0f172a;
--text-secondary: #475569;
--text-tertiary: #64748b;
--text-muted: #94a3b8;
/* 边框色 */
--border-light: #e2e8f0;
--border-medium: #cbd5e1;
--border-dark: #94a3b8;
/* 间距 */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* 圆角 */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
/* 阴影 */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
/* 暗色主题支持 */
[data-theme="dark"] {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f8fafc;
--text-secondary: #e2e8f0;
--text-tertiary: #cbd5e1;
--border-light: #334155;
--border-medium: #475569;
--border-dark: #64748b;
}
/* 重置样式 */
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
background-color: var(--bg-secondary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 主容器布局 */
.app-container {
display: grid;
grid-template-columns: 260px 1fr;
height: 100vh;
overflow: hidden;
}
/* 响应式布局 */
@media (max-width: 1024px) {
.app-container {
grid-template-columns: 240px 1fr;
}
}
@media (max-width: 768px) {
.app-container {
grid-template-columns: 1fr;
}
}
/* 通用工具类 */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* 焦点样式 */
:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border-medium);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-dark);
}
/* 加载动画 */
@keyframes shimmer {
0% {
background-position: -200px 0;
}
100% {
background-position: calc(200px + 100%) 0;
}
}
.loading-shimmer {
background: linear-gradient(
90deg,
var(--bg-tertiary) 0%,
var(--bg-secondary) 50%,
var(--bg-tertiary) 100%
);
background-size: 200px 100%;
animation: shimmer 1.5s infinite linear;
}
/* 通用按钮样式 */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border: 1px solid var(--border-medium);
border-radius: var(--radius-md);
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
gap: 8px;
}
.btn:hover {
background: var(--bg-tertiary);
border-color: var(--border-dark);
}
.btn:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
.btn-primary {
background: var(--accent-600);
color: white;
border-color: var(--accent-600);
}
.btn-primary:hover {
background: var(--accent-700);
border-color: var(--accent-700);
}
.btn-secondary {
background: var(--bg-secondary);
color: var(--text-secondary);
border-color: var(--border-light);
}
.btn-secondary:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
/* 表单元素样式 */
input, select, textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
display: none;
}
/* 表格样式 */
.table-container {
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-light);
}
.table th {
background: var(--bg-secondary);
font-weight: 600;
color: var(--text-secondary);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table tbody tr:hover {
background: var(--bg-secondary);
}
.table tbody tr:last-child td {
border-bottom: none;
}
/* 空状态样式 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
color: var(--text-secondary);
}
.empty-state-icon {
width: 48px;
height: 48px;
margin-bottom: 16px;
color: var(--text-muted);
}
.empty-state-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.empty-state-description {
font-size: 14px;
color: var(--text-secondary);
max-width: 300px;
}
/* 分页样式 */
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: var(--bg-primary);
border-top: 1px solid var(--border-light);
}
.pagination-info {
font-size: 14px;
color: var(--text-secondary);
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.pagination-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--border-medium);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 14px;
text-decoration: none;
transition: all 0.2s ease;
}
.pagination-button:hover {
background: var(--bg-secondary);
border-color: var(--border-dark);
color: var(--text-primary);
}
.pagination-button.active {
background: var(--accent-600);
color: white;
border-color: var(--accent-600);
}
.pagination-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 模态框样式 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 16px;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.modal.hidden {
display: none;
}
.modal-content {
background: var(--bg-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
animation: slideUp 0.2s ease;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid var(--border-light);
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.modal-close {
background: none;
border: none;
padding: 8px;
color: var(--text-secondary);
cursor: pointer;
border-radius: var(--radius-sm);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.modal-close svg {
width: 24px;
height: 24px;
stroke: currentColor;
stroke-width: 2;
}
.modal-body {
padding: 24px;
}
/* 详情内容样式 */
.detail-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px;
background: var(--bg-secondary);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
}
.detail-label {
font-weight: 600;
font-size: 14px;
color: var(--text-secondary);
text-transform: capitalize;
}
.detail-value {
font-size: 16px;
color: var(--text-primary);
line-height: 1.5;
word-break: break-word;
}
.detail-value:empty::before {
content: '- -';
color: var(--text-muted);
font-style: italic;
}
/* 通知样式 */
.notification {
position: fixed;
top: 16px;
right: 16px;
padding: 12px 16px;
border-radius: var(--radius-md);
color: white;
font-size: 14px;
z-index: 1001;
box-shadow: var(--shadow-lg);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-success {
background: var(--success);
}
.notification-error {
background: var(--error);
}
.notification-warning {
background: var(--warning);
}
.notification-info {
background: var(--info);
}

View File

@@ -0,0 +1,617 @@
/* 主内容区域样式 */
.main-content {
overflow-y: auto;
background: var(--bg-secondary);
position: relative;
display: flex;
flex-direction: column;
height: 100vh;
}
/* 内容头部 */
.content-header {
background: var(--bg-primary);
border-bottom: 1px solid var(--border-light);
padding: 0;
position: sticky;
top: 0;
z-index: 100;
}
.header-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 24px;
flex-wrap: wrap;
}
/* 移动端菜单按钮 */
.mobile-menu-toggle {
display: none;
background: none;
border: none;
padding: 8px;
border-radius: var(--radius-md);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.mobile-menu-toggle:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.mobile-menu-toggle:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
/* 面包屑导航 */
.breadcrumb {
flex: 1;
min-width: 200px;
}
.breadcrumb-list {
display: flex;
align-items: center;
list-style: none;
padding: 0;
margin: 0;
gap: 8px;
flex-wrap: wrap;
}
.breadcrumb-item:not(:last-child)::after {
content: '/';
color: var(--text-muted);
margin-left: 8px;
}
.breadcrumb-link {
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
transition: color 0.2s ease;
}
.breadcrumb-link:hover {
color: var(--accent-600);
}
.breadcrumb-current {
color: var(--text-primary);
font-weight: 500;
font-size: 14px;
}
/* 工具栏 */
.header-tools {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
/* 搜索容器 */
.search-container {
display: flex;
align-items: center;
gap: 8px;
}
.search-box {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: var(--text-muted);
pointer-events: none;
z-index: 1;
}
.search-input {
width: 300px;
max-width: 100%;
padding: 8px 12px 8px 36px;
border: 1px solid var(--border-medium);
border-radius: var(--radius-md);
font-size: 14px;
background: var(--bg-secondary);
color: var(--text-primary);
transition: all 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: var(--accent-500);
box-shadow: 0 0 0 3px var(--accent-100);
}
.search-input::placeholder {
color: var(--text-muted);
}
.search-clear {
position: absolute;
right: 8px;
background: none;
border: none;
padding: 4px;
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
transition: all 0.2s ease;
}
.search-clear:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.search-button {
padding: 8px 12px;
background: var(--accent-600);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
}
.search-button:hover {
background: var(--accent-700);
}
.search-button:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
/* 视图控制 */
.view-controls {
display: flex;
align-items: center;
background: var(--bg-secondary);
border: 1px solid var(--border-medium);
border-radius: var(--radius-md);
overflow: hidden;
}
.view-button {
padding: 8px;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.view-button:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.view-button.active {
background: var(--accent-100);
color: var(--accent-700);
}
.view-button:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
/* 导出下拉菜单 */
.export-dropdown {
position: relative;
}
.export-button {
padding: 8px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-medium);
border-radius: var(--radius-md);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
}
.export-button:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--border-dark);
}
.export-button:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
.export-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
min-width: 120px;
z-index: 1000;
display: none;
}
.export-menu.show {
display: block;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.export-option {
width: 100%;
padding: 8px 12px;
background: none;
border: none;
text-align: left;
font-size: 14px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.export-option:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.export-option:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: -2px;
}
.export-option:first-child {
border-top-left-radius: var(--radius-md);
border-top-right-radius: var(--radius-md);
}
.export-option:last-child {
border-bottom-left-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
}
/* 分页信息 */
.pagination-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 24px;
background: var(--bg-tertiary);
border-top: 1px solid var(--border-light);
font-size: 14px;
}
.pagination-text {
color: var(--text-secondary);
}
.per-page-select {
padding: 4px 8px;
border: 1px solid var(--border-medium);
border-radius: var(--radius-sm);
font-size: 14px;
background: var(--bg-primary);
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s ease;
}
.per-page-select:focus {
outline: none;
border-color: var(--accent-500);
box-shadow: 0 0 0 2px var(--accent-100);
}
/* 主内容区域 */
.content-main {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.content-container {
max-width: none;
margin: 0;
}
/* 数据展示区域 */
.data-display {
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th,
.data-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-light);
font-size: 14px;
}
.data-table th {
background: var(--bg-secondary);
font-weight: 600;
color: var(--text-secondary);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
position: sticky;
top: 0;
z-index: 10;
}
.data-table tbody tr {
transition: background-color 0.2s ease;
}
.data-table tbody tr:hover {
background: var(--bg-secondary);
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
.data-table td {
color: var(--text-primary);
vertical-align: top;
}
/* 操作按钮 */
.action-button {
padding: 4px 8px;
background: none;
border: 1px solid var(--border-medium);
border-radius: var(--radius-sm);
color: var(--accent-600);
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-block;
}
.action-button:hover {
background: var(--accent-50);
border-color: var(--accent-300);
}
.action-button:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
/* 卡片视图 */
.data-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
padding: 16px;
}
.data-card {
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
padding: 16px;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
}
.data-card:hover {
box-shadow: var(--shadow-md);
border-color: var(--border-medium);
}
.data-card-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
.data-card-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 4px 0;
border-bottom: 1px solid var(--border-light);
}
.data-card-item:last-child {
border-bottom: none;
}
.data-card-label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
flex-shrink: 0;
margin-right: 8px;
}
.data-card-value {
font-size: 13px;
color: var(--text-primary);
word-break: break-word;
text-align: right;
}
/* 响应式设计 */
@media (max-width: 768px) {
.mobile-menu-toggle {
display: block;
}
.header-toolbar {
flex-direction: column;
align-items: stretch;
gap: 12px;
padding: 12px 16px;
}
.breadcrumb {
order: -1;
}
.search-input {
width: 100%;
}
.header-tools {
justify-content: center;
}
.pagination-info {
flex-direction: column;
align-items: stretch;
gap: 8px;
padding: 12px 16px;
}
.content-main {
padding: 16px;
}
.data-cards {
grid-template-columns: 1fr;
padding: 12px;
}
.data-table th,
.data-table td {
padding: 8px 12px;
font-size: 13px;
}
}
@media (max-width: 480px) {
.header-tools {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.view-controls,
.export-dropdown {
width: 100%;
}
.export-button {
width: 100%;
justify-content: center;
}
}
/* 加载状态 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--bg-primary);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-light);
border-top: 2px solid var(--accent-600);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 无障碍支持 */
@media (prefers-reduced-motion: reduce) {
.loading-spinner {
animation: none;
}
.data-table tbody tr {
transition: none;
}
.data-card {
transition: none;
}
}
/* 高对比度模式 */
@media (prefers-contrast: high) {
.data-table th,
.data-table td {
border-color: var(--border-dark);
}
.data-card {
border-color: var(--border-dark);
}
}
/* 打印样式 */
@media print {
.content-header {
display: none;
}
.data-table th {
background: var(--bg-primary);
color: var(--text-primary);
}
}

View File

@@ -0,0 +1,366 @@
/* 侧边栏样式 */
.sidebar {
background: var(--bg-primary);
border-right: 1px solid var(--border-light);
overflow-y: auto;
position: relative;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
height: 100vh;
}
/* 侧边栏头部 */
.sidebar-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-primary);
position: relative;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.brand-icon {
color: var(--accent-600);
flex-shrink: 0;
transition: color 0.2s ease;
}
.brand-text {
flex: 1;
min-width: 0;
}
.brand-title {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
line-height: 1.2;
}
.brand-subtitle {
font-size: 13px;
color: var(--text-secondary);
margin: 0;
line-height: 1.3;
}
/* 主题切换按钮 */
.theme-toggle {
position: absolute;
top: 16px;
right: 16px;
background: none;
border: none;
padding: 8px;
border-radius: var(--radius-md);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.theme-toggle:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.theme-toggle:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
.theme-toggle .moon-icon {
display: none;
}
[data-theme="dark"] .theme-toggle .sun-icon {
display: none;
}
[data-theme="dark"] .theme-toggle .moon-icon {
display: block;
}
/* 搜索框 */
.sidebar-search {
padding: 12px 16px;
border-bottom: 1px solid var(--border-light);
}
.search-container {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: var(--text-muted);
pointer-events: none;
z-index: 1;
}
.sidebar-search-input {
width: 100%;
padding: 8px 12px 8px 36px;
border: 1px solid var(--border-medium);
border-radius: var(--radius-md);
font-size: 14px;
background: var(--bg-secondary);
color: var(--text-primary);
transition: all 0.2s ease;
}
.sidebar-search-input:focus {
outline: none;
border-color: var(--accent-500);
box-shadow: 0 0 0 3px var(--accent-100);
}
.sidebar-search-input::placeholder {
color: var(--text-muted);
}
/* 导航区域 */
.sidebar-nav {
padding: 8px 0;
flex: 1;
overflow-y: auto;
margin-bottom: 16px;
}
.nav-section-title {
padding: 12px 20px 8px;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin: 0;
}
.nav-list {
list-style: none;
padding: 0;
margin: 0;
}
.nav-item {
margin: 2px 8px;
}
.nav-link {
display: block;
padding: 8px 12px;
color: var(--text-secondary);
text-decoration: none;
border-radius: var(--radius-md);
transition: all 0.2s ease;
position: relative;
}
.nav-link:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.nav-link:focus-visible {
outline: 2px solid var(--accent-600);
outline-offset: 2px;
}
.nav-link.active {
background: var(--accent-50);
color: var(--accent-700);
font-weight: 500;
}
.nav-link.active .nav-icon {
color: var(--accent-600);
}
/* 黑暗模式下的选中菜单样式 */
[data-theme="dark"] .nav-link.active {
background: var(--accent-900);
color: var(--accent-200);
}
[data-theme="dark"] .nav-link.active .nav-icon {
color: var(--accent-400);
}
[data-theme="dark"] .nav-link.active .nav-title {
color: var(--accent-100);
}
[data-theme="dark"] .nav-link.active .nav-subtitle {
color: var(--accent-300);
}
[data-theme="dark"] .nav-link.active .nav-description {
color: var(--accent-400);
}
.nav-link-content {
display: flex;
align-items: center;
gap: 12px;
position: relative;
}
.nav-icon {
color: var(--text-muted);
flex-shrink: 0;
transition: color 0.2s ease;
}
.nav-text-content {
flex: 1;
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 1px;
}
.nav-title {
font-size: 14px;
line-height: 1.3;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-primary);
}
.nav-subtitle {
font-size: 11px;
color: var(--text-muted);
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.8;
}
.nav-description {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-indicator {
position: absolute;
right: -12px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 20px;
background: var(--accent-600);
border-radius: 2px;
}
/* 黑暗模式下的指示器 */
[data-theme="dark"] .nav-indicator {
background: var(--accent-400);
}
/* 移动端侧边栏 */
.sidebar-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
display: none;
}
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 0;
left: -260px;
height: 100vh;
z-index: 1000;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 2px 0 20px rgba(0, 0, 0, 0.1);
}
.sidebar.open {
left: 0;
}
.sidebar-backdrop {
display: block;
}
.sidebar.open + .sidebar-backdrop {
opacity: 1;
visibility: visible;
}
}
/* 滚动条优化 */
.sidebar::-webkit-scrollbar {
width: 4px;
}
.sidebar::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--border-medium);
border-radius: 2px;
}
.sidebar::-webkit-scrollbar-thumb:hover {
background: var(--border-dark);
}
/* 动画效果 */
.nav-link,
.settings-button {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 无障碍支持 */
@media (prefers-reduced-motion: reduce) {
.sidebar,
.nav-link,
.settings-button {
transition: none;
}
}
/* 高对比度模式 */
@media (prefers-contrast: high) {
.nav-link {
border: 1px solid transparent;
}
.nav-link.active {
border-color: var(--accent-600);
}
.nav-indicator {
width: 4px;
}
}

View File

@@ -0,0 +1,429 @@
/* 响应式样式 - 断点系统 */
/* 桌面端 (>1440px) - 大屏优化 */
@media (min-width: 1440px) {
.app-container {
grid-template-columns: 280px 1fr;
}
.sidebar {
width: 280px;
}
.search-input {
width: 400px;
}
.data-cards {
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
}
}
/* 桌面端 (1025px-1440px) - 标准布局 */
@media (min-width: 1025px) and (max-width: 1439px) {
.app-container {
grid-template-columns: 260px 1fr;
}
}
/* 平板端 (768px-1024px) - 缩小侧边栏 */
@media (min-width: 768px) and (max-width: 1024px) {
.app-container {
grid-template-columns: 240px 1fr;
}
.sidebar {
width: 240px;
}
.sidebar-header {
padding: 12px 16px;
}
.sidebar-search,
.sidebar-nav,
.sidebar-footer {
padding-left: 16px;
padding-right: 16px;
}
.search-input {
width: 250px;
}
.data-cards {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.content-main {
padding: 20px;
}
}
/* 移动端 (<768px) - 侧边栏隐藏 */
@media (max-width: 767px) {
.app-container {
grid-template-columns: 1fr;
}
/* 侧边栏移动端样式 */
.sidebar {
position: fixed;
top: 0;
left: -260px;
height: 100vh;
width: 260px;
z-index: 1000;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 2px 0 20px rgba(0, 0, 0, 0.15);
}
.sidebar.open {
left: 0;
}
.sidebar-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
display: block;
}
.sidebar.open + .sidebar-backdrop {
opacity: 1;
visibility: visible;
}
/* 移动端菜单按钮 */
.mobile-menu-toggle {
display: flex;
align-items: center;
justify-content: center;
}
/* 移动端头部布局 */
.header-toolbar {
flex-direction: column;
align-items: stretch;
gap: 12px;
padding: 12px 16px;
}
.breadcrumb {
order: -1;
text-align: center;
}
.header-tools {
justify-content: center;
flex-wrap: wrap;
}
/* 搜索框响应式 */
.search-input {
width: 100%;
min-width: 200px;
}
.search-container {
width: 100%;
justify-content: center;
}
.search-box {
flex: 1;
max-width: 300px;
}
/* 分页信息响应式 */
.pagination-info {
flex-direction: column;
align-items: stretch;
gap: 8px;
padding: 12px 16px;
text-align: center;
}
.per-page-select {
width: 100%;
max-width: 200px;
}
/* 内容区域响应式 */
.content-main {
padding: 16px;
}
/* 数据卡片响应式 */
.data-cards {
grid-template-columns: 1fr;
gap: 12px;
padding: 12px;
}
.data-card {
padding: 12px;
}
/* 数据表格响应式 */
.data-table {
font-size: 13px;
}
.data-table th,
.data-table td {
padding: 8px 12px;
}
/* 隐藏非关键列 */
@media (max-width: 480px) {
.data-table th:nth-child(n+4),
.data-table td:nth-child(n+4) {
display: none;
}
.data-table th:last-child,
.data-table td:last-child {
display: table-cell;
}
}
}
/* 小屏手机 (<480px) - 极简模式 */
@media (max-width: 479px) {
.header-toolbar {
padding: 8px 12px;
gap: 8px;
}
.header-tools {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.view-controls,
.export-dropdown {
width: 100%;
}
.view-controls {
justify-content: center;
}
.export-button {
width: 100%;
justify-content: center;
}
.search-container {
flex-direction: column;
align-items: stretch;
}
.search-button {
width: 100%;
justify-content: center;
}
.content-main {
padding: 12px;
}
.data-cards {
gap: 8px;
padding: 8px;
}
.data-card {
padding: 8px;
}
.pagination-info {
padding: 8px 12px;
}
/* 单列布局 */
.data-cards {
grid-template-columns: 1fr;
}
}
/* 横向模式 */
@media (max-height: 600px) and (orientation: landscape) {
.sidebar {
max-height: 100vh;
}
.sidebar-header {
padding: 8px 16px;
}
.sidebar-nav {
padding: 4px 0;
}
.nav-link {
padding: 6px 12px;
}
.nav-title {
font-size: 13px;
}
.nav-description {
font-size: 11px;
}
.content-main {
padding: 12px 16px;
}
}
/* 触控设备优化 */
@media (hover: none) and (pointer: coarse) {
.nav-link:hover,
.view-button:hover,
.export-button:hover,
.settings-button:hover {
background: none;
}
.nav-link:active,
.view-button:active,
.export-button:active,
.settings-button:active {
background: var(--bg-tertiary);
transform: scale(0.98);
}
}
/* 高DPI屏幕优化 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.sidebar {
box-shadow: 0 0 20px rgba(0, 0, 0, 0.08);
}
.export-menu {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
}
/* 暗色主题响应式 */
@media (max-width: 768px) and (prefers-color-scheme: dark) {
.sidebar {
box-shadow: 2px 0 20px rgba(0, 0, 0, 0.3);
}
}
/* 无障碍支持 */
@media (prefers-reduced-motion: reduce) {
.sidebar,
.sidebar-backdrop,
.export-menu {
transition: none;
}
}
/* 强制横向滚动 */
@media (max-width: 768px) {
.data-table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.data-table {
min-width: 600px;
}
}
/* 打印响应式 */
@media print {
.sidebar,
.mobile-menu-toggle,
.header-tools,
.pagination-info {
display: none !important;
}
.app-container {
grid-template-columns: 1fr;
}
.main-content {
overflow: visible;
height: auto;
}
.content-header {
position: static;
border: none;
padding: 0;
}
.data-table th,
.data-table td {
border: 1px solid var(--border-dark);
}
}
/* 键盘导航响应式 */
@media (max-width: 768px) {
.sidebar:focus-within {
left: 0;
}
.sidebar-backdrop:focus-within {
opacity: 1;
visibility: visible;
}
}
/* 系统主题检测 */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f8fafc;
--text-secondary: #e2e8f0;
--text-tertiary: #cbd5e1;
--border-light: #334155;
--border-medium: #475569;
--border-dark: #64748b;
}
}
/* 高对比度模式响应式 */
@media (prefers-contrast: high) {
.sidebar {
border-right: 2px solid var(--border-dark);
}
.nav-link.active {
border: 2px solid var(--accent-600);
}
.search-input:focus {
border-width: 2px;
}
}
/* 自定义滚动条响应式 */
@media (max-width: 768px) {
.sidebar::-webkit-scrollbar {
width: 8px;
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--border-dark);
border-radius: 4px;
}
}

682
web/static/js/app.js Normal file
View File

@@ -0,0 +1,682 @@
/**
* 数据管理应用主逻辑
* 包含主题切换、侧边栏管理、搜索功能、视图控制等
*/
class AppState {
constructor() {
this.searchTimeout = null;
this.currentView = 'table';
this.exportMenuOpen = false;
this.isMobile = false;
this.init();
}
init() {
this.setupThemeToggle();
this.setupSidebarToggle();
this.setupSearch();
this.setupKeyboardShortcuts();
this.setupAccessibility();
this.setupResponsive();
this.setupViewControls();
this.setupExport();
this.setupPagination();
this.restoreUserPreferences();
}
// 主题切换
setupThemeToggle() {
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => this.toggleTheme());
}
// 初始化主题
const savedTheme = localStorage.getItem('theme') || this.detectSystemTheme();
console.log('初始化主题:', savedTheme);
this.setTheme(savedTheme);
// 监听系统主题变化
if (window.matchMedia) {
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeQuery.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
this.setTheme(e.matches ? 'dark' : 'light');
}
});
}
}
detectSystemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
this.setTheme(newTheme);
localStorage.setItem('theme', newTheme);
}
setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
console.log('设置主题:', theme, '到HTML元素');
// 更新主题切换按钮文本
const themeToggle = document.querySelector('.theme-toggle');
if (themeToggle) {
themeToggle.setAttribute('aria-label',
theme === 'light' ? '切换到暗色主题' : '切换到亮色主题');
}
// 确保CSS变量应用到根元素
const root = document.documentElement;
if (theme === 'dark') {
root.style.setProperty('--bg-primary', '#0f172a');
root.style.setProperty('--bg-secondary', '#1e293b');
root.style.setProperty('--bg-tertiary', '#334155');
root.style.setProperty('--text-primary', '#f8fafc');
root.style.setProperty('--text-secondary', '#e2e8f0');
} else {
root.style.setProperty('--bg-primary', '#ffffff');
root.style.setProperty('--bg-secondary', '#f8fafc');
root.style.setProperty('--bg-tertiary', '#f1f5f9');
root.style.setProperty('--text-primary', '#0f172a');
root.style.setProperty('--text-secondary', '#475569');
}
// 强制触发重绘
document.documentElement.style.display = 'none';
document.documentElement.offsetHeight; // 触发重排
document.documentElement.style.display = '';
}
// 侧边栏管理
setupSidebarToggle() {
const toggle = document.querySelector('.mobile-menu-toggle');
const sidebar = document.querySelector('.sidebar');
const backdrop = document.querySelector('.sidebar-backdrop');
if (toggle) {
toggle.addEventListener('click', () => this.toggleSidebar());
}
if (backdrop) {
backdrop.addEventListener('click', () => this.closeSidebar());
}
// 响应式处理
window.addEventListener('resize', () => this.handleResize());
this.handleResize(); // 初始化
}
handleResize() {
const wasMobile = this.isMobile;
this.isMobile = window.innerWidth <= 768;
if (wasMobile !== this.isMobile) {
if (!this.isMobile) {
this.closeSidebar();
}
}
}
toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
const toggle = document.querySelector('.mobile-menu-toggle');
const isOpen = sidebar.classList.contains('open');
if (isOpen) {
this.closeSidebar();
} else {
this.openSidebar();
}
}
openSidebar() {
const sidebar = document.querySelector('.sidebar');
const toggle = document.querySelector('.mobile-menu-toggle');
const backdrop = document.querySelector('.sidebar-backdrop');
sidebar.classList.add('open');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
if (backdrop) backdrop.style.display = 'block';
document.body.style.overflow = 'hidden';
// 聚焦到第一个可聚焦元素
const firstFocusable = sidebar.querySelector('a, button, input');
if (firstFocusable) {
setTimeout(() => firstFocusable.focus(), 100);
}
}
closeSidebar() {
const sidebar = document.querySelector('.sidebar');
const toggle = document.querySelector('.mobile-menu-toggle');
const backdrop = document.querySelector('.sidebar-backdrop');
sidebar.classList.remove('open');
if (toggle) toggle.setAttribute('aria-expanded', 'false');
if (backdrop) backdrop.style.display = 'none';
document.body.style.overflow = '';
// 将焦点返回到菜单按钮
if (toggle) toggle.focus();
}
// 智能搜索
setupSearch() {
const searchInput = document.getElementById('searchInput');
const searchClear = document.querySelector('.search-clear');
const sidebarSearchInput = document.querySelector('.sidebar-search-input');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
this.handleSearchInput(e.target.value);
this.updateClearButton(e.target.value);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.performSearch(e.target.value);
}
});
}
if (searchClear) {
searchClear.addEventListener('click', () => this.clearSearch());
}
if (sidebarSearchInput) {
sidebarSearchInput.addEventListener('input', (e) => {
this.filterTables(e.target.value);
});
}
}
handleSearchInput(query) {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, 300);
}
updateClearButton(value) {
const searchClear = document.querySelector('.search-clear');
if (searchClear) {
searchClear.style.display = value ? 'block' : 'none';
}
}
performSearch(query) {
const params = new URLSearchParams(window.location.search);
const table = params.get('table') || this.getCurrentTable();
if (query.trim()) {
params.set('search', query.trim());
} else {
params.delete('search');
}
params.set('page', '1');
window.location.href = `/?${params.toString()}`;
}
clearSearch() {
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.value = '';
this.updateClearButton('');
this.performSearch('');
}
}
filterTables(query) {
const navLinks = document.querySelectorAll('.nav-link');
const searchTerm = query.toLowerCase();
navLinks.forEach(link => {
const title = link.querySelector('.nav-title').textContent.toLowerCase();
const subtitle = link.querySelector('.nav-subtitle').textContent.toLowerCase();
const description = link.querySelector('.nav-description').textContent.toLowerCase();
const matches = title.includes(searchTerm) ||
subtitle.includes(searchTerm) ||
description.includes(searchTerm);
link.style.display = matches ? 'flex' : 'none';
link.parentElement.style.display = matches ? 'block' : 'none';
});
// 显示搜索结果提示
const visibleLinks = Array.from(navLinks).filter(link =>
link.style.display !== 'none');
if (visibleLinks.length === 0 && query.trim()) {
this.showNotification('没有找到匹配的表格', 'info');
}
}
// 视图控制
setupViewControls() {
const viewButtons = document.querySelectorAll('.view-button');
viewButtons.forEach(button => {
button.addEventListener('click', (e) => {
const mode = e.currentTarget.getAttribute('onclick')
.match(/setViewMode\('(\w+)'\)/)[1];
this.setViewMode(mode);
});
});
}
setViewMode(mode) {
this.currentView = mode;
// 更新按钮状态
document.querySelectorAll('.view-button').forEach(btn => {
btn.classList.remove('active');
});
const activeButton = document.querySelector(`[onclick*="setViewMode('${mode}')"]`);
if (activeButton) {
activeButton.classList.add('active');
}
// 保存用户偏好
localStorage.setItem('viewMode', mode);
// 切换视图显示
const contentContainer = document.querySelector('.content-main');
if (contentContainer) {
contentContainer.setAttribute('data-view', mode);
}
// 切换数据展示方式
this.switchDataDisplay(mode);
}
switchDataDisplay(mode) {
const tableView = document.querySelector('.data-table-container');
const cardView = document.querySelector('.data-cards-container');
if (tableView && cardView) {
if (mode === 'table') {
tableView.style.display = 'block';
cardView.style.display = 'none';
} else {
tableView.style.display = 'none';
cardView.style.display = 'grid';
}
}
}
// 导出功能
setupExport() {
const exportButton = document.querySelector('.export-button');
if (exportButton) {
exportButton.addEventListener('click', () => this.toggleExportMenu());
}
const exportOptions = document.querySelectorAll('.export-option');
exportOptions.forEach(option => {
option.addEventListener('click', (e) => {
const format = e.target.getAttribute('onclick')
.match(/exportData\('(\w+)'\)/)[1];
this.exportData(format);
});
});
}
toggleExportMenu() {
const menu = document.querySelector('.export-menu');
const button = document.querySelector('.export-button');
if (!menu || !button) return;
if (menu.classList.contains('show')) {
this.closeExportMenu();
} else {
this.openExportMenu();
}
}
openExportMenu() {
const menu = document.querySelector('.export-menu');
const button = document.querySelector('.export-button');
if (!menu || !button) return;
menu.classList.add('show');
button.setAttribute('aria-expanded', 'true');
// 点击外部关闭
setTimeout(() => {
document.addEventListener('click', this.handleExportClickOutside, true);
}, 0);
}
closeExportMenu() {
const menu = document.querySelector('.export-menu');
const button = document.querySelector('.export-button');
if (!menu || !button) return;
menu.classList.remove('show');
button.setAttribute('aria-expanded', 'false');
document.removeEventListener('click', this.handleExportClickOutside, true);
}
handleExportClickOutside = (e) => {
const menu = document.querySelector('.export-menu');
const button = document.querySelector('.export-button');
if (!button.contains(e.target) && !menu.contains(e.target)) {
this.closeExportMenu();
}
}
exportData(format) {
const params = new URLSearchParams(window.location.search);
const table = params.get('table') || this.getCurrentTable();
params.set('format', format);
window.location.href = `/api/export/${table}?${params.toString()}`;
this.closeExportMenu();
}
// 分页控制
setupPagination() {
const perPageSelect = document.querySelector('.per-page-select');
if (perPageSelect) {
perPageSelect.addEventListener('change', (e) => {
this.changePerPage(e.target.value);
});
}
}
changePerPage(perPage) {
const params = new URLSearchParams(window.location.search);
params.set('per_page', perPage);
params.set('page', '1');
window.location.href = `/?${params.toString()}`;
}
// 键盘快捷键
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + K 聚焦搜索
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
searchInput.select();
}
}
// Esc 关闭侧边栏/菜单
if (e.key === 'Escape') {
this.closeSidebar();
this.closeExportMenu();
// 关闭模态框
const modal = document.getElementById('detailModal');
if (modal && !modal.classList.contains('hidden')) {
this.closeModal();
}
}
// Ctrl/Cmd + , 打开设置
if ((e.ctrlKey || e.metaKey) && e.key === ',') {
e.preventDefault();
this.openSettings();
}
// 方向键导航
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
this.handleArrowNavigation(e);
}
});
}
handleArrowNavigation(e) {
// 添加键盘导航支持
const activeNav = document.querySelector('.nav-link.active');
if (activeNav && document.activeElement === activeNav) {
e.preventDefault();
// 这里可以添加更复杂的键盘导航逻辑
}
}
// 无障碍支持
setupAccessibility() {
// 焦点管理
document.addEventListener('focusin', (e) => {
const sidebar = document.querySelector('.sidebar');
if (!sidebar) return;
const isInSidebar = sidebar.contains(e.target);
const isMobile = window.innerWidth <= 768;
if (isMobile && !isInSidebar && sidebar.classList.contains('open')) {
this.closeSidebar();
}
});
// 添加ARIA实时区域
this.setupAriaLiveRegions();
}
setupAriaLiveRegions() {
// 创建屏幕阅读器通知区域
const liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.className = 'sr-only';
document.body.appendChild(liveRegion);
}
// 响应式处理
setupResponsive() {
const handleResize = () => {
const isMobile = window.innerWidth <= 768;
if (isMobile !== this.isMobile) {
this.isMobile = isMobile;
this.handleResponsiveChange(isMobile);
}
};
window.addEventListener('resize', this.debounce(handleResize, 250));
handleResize(); // 初始化
}
handleResponsiveChange(isMobile) {
if (!isMobile) {
this.closeSidebar();
}
// 更新断点属性
document.documentElement.setAttribute('data-breakpoint',
isMobile ? 'mobile' : 'desktop');
}
// 模态框控制
setupModal() {
const modal = document.getElementById('detailModal');
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.closeModal();
}
});
}
}
showDetail(id) {
const table = this.getCurrentTable();
this.showLoading();
fetch(`/api/data/${table}/detail/${id}`)
.then(response => response.json())
.then(data => {
this.hideLoading();
this.renderDetailModal(data);
})
.catch(error => {
this.hideLoading();
this.showNotification('加载详情失败', 'error');
console.error('Error loading detail:', error);
});
}
renderDetailModal(data) {
const modal = document.getElementById('detailModal');
const content = document.getElementById('detailContent');
if (!modal || !content) return;
let html = '';
for (const [key, value] of Object.entries(data.data || {})) {
html += `
<div class="detail-item">
<div class="detail-label">${key}</div>
<div class="detail-value">${value || ''}</div>
</div>
`;
}
content.innerHTML = html;
modal.classList.remove('hidden');
// 聚焦到模态框
const firstFocusable = modal.querySelector('button, [href], [tabindex]:not([tabindex="-1"])');
if (firstFocusable) {
firstFocusable.focus();
}
}
closeModal() {
const modal = document.getElementById('detailModal');
if (modal) {
modal.classList.add('hidden');
}
}
// 工具函数
getCurrentTable() {
const params = new URLSearchParams(window.location.search);
return params.get('table') || '';
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.setAttribute('role', 'alert');
notification.setAttribute('aria-live', 'polite');
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 5000);
}
showLoading() {
const loading = document.createElement('div');
loading.className = 'loading-overlay';
loading.innerHTML = '<div class="loading-spinner"></div>';
document.body.appendChild(loading);
}
hideLoading() {
const loading = document.querySelector('.loading-overlay');
if (loading) {
loading.remove();
}
}
restoreUserPreferences() {
// 恢复视图模式
const savedViewMode = localStorage.getItem('viewMode') || 'table';
this.setViewMode(savedViewMode);
// 恢复每页显示数量
const savedPerPage = localStorage.getItem('perPage');
if (savedPerPage) {
const perPageSelect = document.querySelector('.per-page-select');
if (perPageSelect) {
perPageSelect.value = savedPerPage;
}
}
}
openSettings() {
this.showNotification('设置功能开发中...', 'info');
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
}
// 全局函数
window.toggleSidebar = () => app.toggleSidebar();
window.closeSidebar = () => app.closeSidebar();
window.toggleTheme = () => app.toggleTheme();
window.debounceSearch = (query) => app.handleSearchInput(query);
window.performSearch = () => {
const searchInput = document.getElementById('searchInput');
if (searchInput) app.performSearch(searchInput.value);
};
window.clearSearch = () => app.clearSearch();
window.filterTables = (query) => app.filterTables(query);
window.setViewMode = (mode) => app.setViewMode(mode);
window.toggleExportMenu = () => app.toggleExportMenu();
window.exportData = (format) => app.exportData(format);
window.changePerPage = (perPage) => app.changePerPage(perPage);
window.showDetail = (id) => app.showDetail(id);
window.closeModal = () => app.closeModal();
window.openSettings = () => app.openSettings();
// 初始化应用
const app = new AppState();
// 页面加载完成后的初始化
document.addEventListener('DOMContentLoaded', () => {
console.log('数据管理应用已初始化');
// 添加初始化完成事件
const initEvent = new CustomEvent('app:initialized');
document.dispatchEvent(initEvent);
});
// 处理回退按钮
window.addEventListener('popstate', () => {
// 重新加载页面数据
location.reload();
});
// 错误处理
window.addEventListener('error', (e) => {
console.error('应用错误:', e.error);
app.showNotification('发生错误,请重试', 'error');
});
// 未捕获的Promise错误
window.addEventListener('unhandledrejection', (e) => {
console.error('未处理的Promise错误:', e.reason);
app.showNotification('网络错误,请检查连接', 'error');
});

View File

@@ -4,98 +4,231 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.TableAlias}} - 数据管理系统</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
.tag {
@apply inline-block px-2 py-1 text-xs font-medium rounded-full;
}
.modal {
@apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden;
}
.modal-content {
@apply bg-white rounded-lg max-w-4xl w-full mx-4 max-h-[80vh] overflow-y-auto;
}
.card {
@apply bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow;
}
.card-body {
@apply p-6;
}
.loading {
@apply animate-pulse;
}
</style>
<link rel="stylesheet" href="/static/css/app.css">
<link rel="stylesheet" href="/static/css/components/sidebar.css">
<link rel="stylesheet" href="/static/css/components/content.css">
<link rel="stylesheet" href="/static/css/responsive.css">
</head>
<body class="bg-gray-50">
<div class="min-h-screen">
<!-- Header -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center py-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">数据管理系统</h1>
<p class="text-sm text-gray-600">{{.TableAlias}}</p>
<body>
<div class="app-container">
<!-- 左侧导航栏 -->
<aside class="sidebar" role="navigation" aria-label="主导航">
<!-- Logo/标题区域 -->
<div class="sidebar-header">
<div class="brand">
<svg class="brand-icon" width="32" height="32" viewBox="0 0 24 24" fill="none">
<path d="M4 6h16M4 12h16M4 18h7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<div class="brand-text">
<h1 class="brand-title">数据管理系统</h1>
<p class="brand-subtitle">{{.TableAlias}}</p>
</div>
<div class="flex items-center space-x-4">
<select id="tableSelector" class="border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" onchange="changeTable(this.value)">
</div>
<!-- 主题切换 -->
<button class="theme-toggle"
aria-label="切换主题"
title="切换明暗主题">
<svg class="sun-icon" width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"/>
</svg>
<svg class="moon-icon" width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z"/>
</svg>
</button>
</div>
<!-- 智能搜索框 -->
<div class="sidebar-search">
<div class="search-container">
<svg class="search-icon" width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
</svg>
<input type="search"
class="sidebar-search-input"
placeholder="快速查找表格..."
aria-label="搜索数据表"
oninput="filterTables(this.value)">
</div>
</div>
<!-- 分组导航 -->
<nav class="sidebar-nav" aria-label="数据表导航">
<h2 class="nav-section-title">数据表</h2>
<ul class="nav-list" role="list">
{{range .Tables}}
<option value="{{.Name}}" {{if eq .Name $.Table}}selected{{end}}>{{.Alias}}</option>
<li class="nav-item">
<a href="/?table={{.Name}}"
class="nav-link {{if eq .Name $.Table}}active{{end}}"
role="menuitem"
aria-current="{{if eq .Name $.Table}}page{{end}}"
title="{{.Alias}} ({{.Name}}) - {{.Description}}">
<div class="nav-link-content">
<svg class="nav-icon" width="18" height="18" fill="none" viewBox="0 0 24 24">
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2z" stroke="currentColor" stroke-width="2"/>
<path d="M8 5v14M16 5v14" stroke="currentColor" stroke-width="2"/>
</svg>
<div class="nav-text-content">
<span class="nav-title">{{.Alias}}</span>
<span class="nav-subtitle">{{.Name}}</span>
<span class="nav-description">{{.RowCount}} 条记录</span>
</div>
{{if eq .Name $.Table}}
<div class="nav-indicator" aria-hidden="true"></div>
{{end}}
</div>
</a>
</li>
{{end}}
</ul>
</nav>
</aside>
<!-- 移动端遮罩层 -->
<div class="sidebar-backdrop" onclick="closeSidebar()" aria-hidden="true"></div>
<!-- 右侧主内容区 -->
<main class="main-content" role="main">
<!-- 移动端菜单按钮 -->
<header class="content-header" role="banner">
<div class="header-toolbar">
<!-- 移动端菜单 -->
<button class="mobile-menu-toggle"
onclick="toggleSidebar()"
aria-label="打开导航菜单"
aria-controls="sidebar"
aria-expanded="false">
<svg class="menu-icon" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<!-- 面包屑导航 -->
<nav class="breadcrumb" aria-label="当前位置">
<ol class="breadcrumb-list" role="list">
<li class="breadcrumb-item" role="listitem">
<a href="/" class="breadcrumb-link">首页</a>
</li>
<li class="breadcrumb-item" role="listitem" aria-current="page">
<span class="breadcrumb-current">{{.TableAlias}}</span>
</li>
</ol>
</nav>
<!-- 工具栏 -->
<div class="header-tools">
<!-- 高级搜索 -->
<div class="search-container">
<div class="search-box">
<svg class="search-icon" width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
</svg>
<input type="search"
id="searchInput"
class="search-input"
placeholder="在 {{.TableAlias}} 中搜索..."
value="{{.Search}}"
aria-label="搜索数据"
oninput="debounceSearch(this.value)"
onkeydown="handleSearchKeydown(event)">
<button class="search-clear"
onclick="clearSearch()"
aria-label="清除搜索"
style="display: {{if .Search}}block{{else}}none{{end}}">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
<button class="search-button" onclick="performSearch()" aria-label="搜索">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
<!-- 视图控制 -->
<div class="view-controls">
<button class="view-button active"
onclick="setViewMode('table')"
aria-label="表格视图"
title="表格视图">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2z" stroke="currentColor" stroke-width="2"/>
<path d="M8 5v14M16 5v14" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button class="view-button"
onclick="setViewMode('cards')"
aria-label="卡片视图"
title="卡片视图">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
<!-- 导出功能 -->
<div class="export-dropdown">
<button class="export-button"
onclick="toggleExportMenu()"
aria-label="导出数据"
aria-haspopup="true"
aria-expanded="false">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M12 10v6m0 0l-3-3m3 3l3-3m2-8H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V4a2 2 0 00-2-2z" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<div class="export-menu" role="menu" aria-orientation="vertical">
<button class="export-option" role="menuitem" onclick="exportData('csv')">导出 CSV</button>
<button class="export-option" role="menuitem" onclick="exportData('json')">导出 JSON</button>
<button class="export-option" role="menuitem" onclick="exportData('excel')">导出 Excel</button>
</div>
</div>
</div>
</div>
<!-- 分页信息 -->
<div class="pagination-info">
<span class="pagination-text">
显示 {{.StartRecord}} - {{.EndRecord}} / {{.Total}} 条记录
</span>
<select class="per-page-select"
onchange="changePerPage(this.value)"
aria-label="每页显示条数">
<option value="10" {{if eq .PerPage 10}}selected{{end}}>10 条/页</option>
<option value="25" {{if eq .PerPage 25}}selected{{end}}>25 条/页</option>
<option value="50" {{if eq .PerPage 50}}selected{{end}}>50 条/页</option>
<option value="100" {{if eq .PerPage 100}}selected{{end}}>100 条/页</option>
</select>
<div class="text-sm text-gray-600">
共 {{.Total}} 条记录
</div>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Search and Filters -->
<div class="mb-6 bg-white p-4 rounded-lg shadow">
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<input type="text" id="searchInput" placeholder="搜索..." value="{{.Search}}"
class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex gap-2">
<button onclick="performSearch()" class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
搜索
</button>
<button onclick="clearSearch()" class="px-4 py-2 bg-gray-300 text-gray-700 rounded-md text-sm hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-500">
清除
</button>
</div>
</div>
</div>
<!-- Data Display -->
<div class="grid gap-6">
<!-- Table View -->
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<!-- 主内容区域 -->
<div class="content-main">
<div class="content-container">
<!-- 数据展示 -->
<div class="data-display">
<!-- 表格视图 -->
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
{{range $col := .Columns}}
{{if $col.ShowInList}}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{{$col.Alias}}
</th>
<th>{{$col.Alias}}</th>
{{end}}
{{end}}
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
<th>操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tbody>
{{range $row := .Data}}
<tr class="hover:bg-gray-50">
<tr>
{{range $col := $.Columns}}
{{if $col.ShowInList}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<td>
{{if eq $col.RenderType "text"}}
{{truncate (index $row $col.Name) 50}}
{{else}}
@@ -104,8 +237,8 @@
</td>
{{end}}
{{end}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
<button onclick="showDetail('{{index $row "id"}}')" class="text-blue-600 hover:text-blue-800">
<td>
<button class="action-button" onclick="showDetail('{{index $row "id"}}')">
查看详情
</button>
</td>
@@ -113,41 +246,84 @@
{{end}}
</tbody>
</table>
<!-- 空状态 -->
{{if eq (len .Data) 0}}
<div class="empty-state">
<svg class="empty-state-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<div class="empty-state-title">暂无数据</div>
<div class="empty-state-description">
{{if .Search}}
没有找到与"{{.Search}}"相关的记录
{{else}}
当前表格暂无数据
{{end}}
</div>
</div>
{{end}}
</div>
<!-- Pagination -->
<!-- 卡片视图 -->
<div class="data-cards-container" style="display: none;">
<div class="data-cards">
{{range $row := .Data}}
<div class="data-card">
<div class="data-card-title">
{{index $row (index $.Columns 0).Name}}
</div>
{{range $i, $col := $.Columns}}
{{if and $col.ShowInList (gt $i 0) (lt $i 4)}}
<div class="data-card-item">
<span class="data-card-label">{{$col.Alias}}:</span>
<span class="data-card-value">
{{if eq $col.RenderType "text"}}
{{truncate (index $row $col.Name) 30}}
{{else}}
{{index $row $col.Name}}
{{end}}
</span>
</div>
{{end}}
{{end}}
<div style="margin-top: 12px;">
<button class="action-button" onclick="showDetail('{{index $row "id"}}')">
查看详情
</button>
</div>
</div>
{{end}}
</div>
<!-- 空状态 -->
{{if eq (len .Data) 0}}
<div class="empty-state">
<svg class="empty-state-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<div class="empty-state-title">暂无数据</div>
<div class="empty-state-description">
{{if .Search}}
没有找到与"{{.Search}}"相关的记录
{{else}}
当前表格暂无数据
{{end}}
</div>
</div>
{{end}}
</div>
<!-- 分页 -->
{{if gt .Pages 1}}
<div class="mt-6 bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<div class="pagination">
<div class="pagination-info">
显示 {{.StartRecord}} - {{.EndRecord}} / {{.Total}} 条记录
</div>
<div class="pagination-controls">
{{if gt .Page 1}}
<a href="?table={{.Table}}&page={{sub .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
上一页
</a>
{{end}}
{{if lt .Page .Pages}}
<a href="?table={{.Table}}&page={{add .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
下一页
</a>
{{end}}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700">
显示第 <span class="font-medium">{{add (mul (sub .Page 1) .PerPage) 1}}</span>
<span class="font-medium">{{if lt (mul .Page .PerPage) .Total}}{{mul .Page .PerPage}}{{else}}{{.Total}}{{end}}</span>
条,共 <span class="font-medium">{{.Total}}</span> 条记录
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
{{if gt .Page 1}}
<a href="?table={{.Table}}&page={{sub .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
上一页
</a>
class="pagination-button">上一页</a>
{{end}}
{{$start := sub .Page 2}}
@@ -161,118 +337,48 @@
{{$start = max 1 (sub .Pages 4)}}
{{end}}
{{range $i := $start | $end}}
{{if eq $i .Page}}
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-blue-50 text-sm font-medium text-blue-600">
{{$i}}
</span>
{{range $i := until $end}}
{{if ge $i $start}}
{{if eq $i $.Page}}
<span class="pagination-button active">{{$i}}</span>
{{else}}
<a href="?table={{.Table}}&page={{$i}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50">
{{$i}}
</a>
<a href="?table={{$.Table}}&page={{$i}}&per_page={{$.PerPage}}&search={{$.Search}}&sort={{$.SortField}}&order={{$.SortOrder}}"
class="pagination-button">{{$i}}</a>
{{end}}
{{end}}
{{end}}
{{if lt .Page .Pages}}
<a href="?table={{.Table}}&page={{add .Page 1}}&per_page={{.PerPage}}&search={{.Search}}&sort={{.SortField}}&order={{.SortOrder}}"
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
下一页
</a>
class="pagination-button">下一页</a>
{{end}}
</nav>
</div>
</div>
</div>
{{end}}
</div>
</div>
</main>
</div>
<!-- Detail Modal -->
<div id="detailModal" class="modal">
<div id="detailModal" class="modal hidden">
<div class="modal-content">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium">详情信息</h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
<div class="modal-header">
<h3 class="modal-title">详情信息</h3>
<button onclick="closeModal()" class="modal-close" aria-label="关闭">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div id="detailContent" class="space-y-4">
<div class="modal-body">
<div id="detailContent" class="detail-content">
<!-- Content will be loaded here -->
</div>
</div>
</div>
</div>
<script>
// Utility functions
function changeTable(tableName) {
window.location.href = `/?table=${tableName}&page=1`;
}
function performSearch() {
const search = document.getElementById('searchInput').value;
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
window.location.href = `/?table=${table}&page=1&search=${encodeURIComponent(search)}`;
}
function clearSearch() {
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
document.getElementById('searchInput').value = '';
window.location.href = `/?table=${table}&page=1`;
}
function showDetail(id) {
const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
fetch(`/api/data/${table}/detail/${id}`)
.then(response => response.json())
.then(data => {
let content = '';
for (const [key, value] of Object.entries(data.data)) {
content += `
<div class="border-b pb-3">
<label class="block text-sm font-medium text-gray-700">${key}</label>
<div class="mt-1 text-sm text-gray-900">${value || ''}</div>
</div>
`;
}
document.getElementById('detailContent').innerHTML = content;
document.getElementById('detailModal').classList.remove('hidden');
})
.catch(error => {
console.error('Error loading detail:', error);
alert('加载详情失败');
});
}
function closeModal() {
document.getElementById('detailModal').classList.add('hidden');
}
// Event listeners
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
document.getElementById('detailModal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
// Initialize
document.addEventListener('DOMContentLoaded', function() {
// Add enter key support for search
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.focus();
}
});
</script>
<script src="/static/js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,424 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据管理系统 - 测试布局</title>
<link rel="stylesheet" href="/static/css/app.css">
<link rel="stylesheet" href="/static/css/components/sidebar.css">
<link rel="stylesheet" href="/static/css/components/content.css">
<link rel="stylesheet" href="/static/css/responsive.css">
<style>
/* 测试样式 */
.test-data {
padding: 20px;
}
.test-item {
background: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
padding: 16px;
margin-bottom: 12px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 左侧导航栏 -->
<aside class="sidebar" role="navigation" aria-label="主导航">
<!-- Logo/标题区域 -->
<div class="sidebar-header">
<div class="brand">
<svg class="brand-icon" width="32" height="32" viewBox="0 0 24 24" fill="none">
<path d="M4 6h16M4 12h16M4 18h7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<div class="brand-text">
<h1 class="brand-title">数据管理系统</h1>
<p class="brand-subtitle">测试表格</p>
</div>
</div>
<!-- 主题切换 -->
<button class="theme-toggle"
aria-label="切换主题"
onclick="toggleTheme()"
title="切换明暗主题">
<svg class="sun-icon" width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"/>
</svg>
<svg class="moon-icon" width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
<path d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z"/>
</svg>
</button>
</div>
<!-- 智能搜索框 -->
<div class="sidebar-search">
<div class="search-container">
<svg class="search-icon" width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
</svg>
<input type="search"
class="sidebar-search-input"
placeholder="快速查找表格..."
aria-label="搜索数据表"
oninput="filterTables(this.value)">
</div>
</div>
<!-- 分组导航 -->
<nav class="sidebar-nav" aria-label="数据表导航">
<h2 class="nav-section-title">数据表</h2>
<ul class="nav-list" role="list">
<li class="nav-item">
<a href="#" class="nav-link active"
role="menuitem"
aria-current="page"
title="用户表 - 用户基本信息">
<div class="nav-link-content">
<svg class="nav-icon" width="18" height="18" fill="none" viewBox="0 0 24 24">
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2z" stroke="currentColor" stroke-width="2"/>
<path d="M8 5v14M16 5v14" stroke="currentColor" stroke-width="2"/>
</svg>
<div class="nav-text-content">
<span class="nav-title">用户表</span>
<span class="nav-description">1,234 条记录</span>
</div>
<div class="nav-indicator" aria-hidden="true"></div>
</div>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link"
role="menuitem"
title="订单表 - 订单交易信息">
<div class="nav-link-content">
<svg class="nav-icon" width="18" height="18" fill="none" viewBox="0 0 24 24">
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2z" stroke="currentColor" stroke-width="2"/>
<path d="M8 5v14M16 5v14" stroke="currentColor" stroke-width="2"/>
</svg>
<div class="nav-text-content">
<span class="nav-title">订单表</span>
<span class="nav-description">5,678 条记录</span>
</div>
</div>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link"
role="menuitem"
title="产品表 - 产品目录信息">
<div class="nav-link-content">
<svg class="nav-icon" width="18" height="18" fill="none" viewBox="0 0 24 24">
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2z" stroke="currentColor" stroke-width="2"/>
<path d="M8 5v14M16 5v14" stroke="currentColor" stroke-width="2"/>
</svg>
<div class="nav-text-content">
<span class="nav-title">产品表</span>
<span class="nav-description">987 条记录</span>
</div>
</div>
</a>
</li>
</ul>
</nav>
<!-- 系统信息 -->
<div class="sidebar-footer">
<div class="system-info">
<div class="info-item">
<span class="info-label">总记录数</span>
<span class="info-value">8,901</span>
</div>
<div class="info-item">
<span class="info-label">最后更新</span>
<span class="info-value">2分钟前</span>
</div>
</div>
<button class="settings-button" onclick="openSettings()" aria-label="系统设置">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" stroke="currentColor" stroke-width="2"/>
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2"/>
</svg>
设置
</button>
</div>
</aside>
<!-- 移动端遮罩层 -->
<div class="sidebar-backdrop" onclick="closeSidebar()" aria-hidden="true"></div>
<!-- 右侧主内容区 -->
<main class="main-content" role="main">
<!-- 移动端菜单按钮 -->
<header class="content-header" role="banner">
<div class="header-toolbar">
<!-- 移动端菜单 -->
<button class="mobile-menu-toggle"
onclick="toggleSidebar()"
aria-label="打开导航菜单"
aria-controls="sidebar"
aria-expanded="false">
<svg class="menu-icon" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<!-- 面包屑导航 -->
<nav class="breadcrumb" aria-label="当前位置">
<ol class="breadcrumb-list" role="list">
<li class="breadcrumb-item" role="listitem">
<a href="/" class="breadcrumb-link">首页</a>
</li>
<li class="breadcrumb-item" role="listitem" aria-current="page">
<span class="breadcrumb-current">测试表格</span>
</li>
</ol>
</nav>
<!-- 工具栏 -->
<div class="header-tools">
<!-- 高级搜索 -->
<div class="search-container">
<div class="search-box">
<svg class="search-icon" width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
</svg>
<input type="search"
id="searchInput"
class="search-input"
placeholder="在测试表格中搜索..."
value=""
aria-label="搜索数据"
oninput="debounceSearch(this.value)">
<button class="search-clear"
onclick="clearSearch()"
aria-label="清除搜索"
style="display: none;">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
<button class="search-button" onclick="performSearch()" aria-label="搜索">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
<!-- 视图控制 -->
<div class="view-controls">
<button class="view-button active"
onclick="setViewMode('table')"
aria-label="表格视图"
title="表格视图">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2z" stroke="currentColor" stroke-width="2"/>
<path d="M8 5v14M16 5v14" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button class="view-button"
onclick="setViewMode('cards')"
aria-label="卡片视图"
title="卡片视图">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</div>
<!-- 导出功能 -->
<div class="export-dropdown">
<button class="export-button"
onclick="toggleExportMenu()"
aria-label="导出数据"
aria-haspopup="true"
aria-expanded="false">
<svg width="16" height="16" fill="none" viewBox="0 0 24 24">
<path d="M12 10v6m0 0l-3-3m3 3l3-3m2-8H7a2 2 0 00-2 2v14a2 2 0 002 2h10a2 2 0 002-2V4a2 2 0 00-2-2z" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<div class="export-menu" role="menu" aria-orientation="vertical">
<button class="export-option" role="menuitem" onclick="exportData('csv')">导出 CSV</button>
<button class="export-option" role="menuitem" onclick="exportData('json')">导出 JSON</button>
<button class="export-option" role="menuitem" onclick="exportData('excel')">导出 Excel</button>
</div>
</div>
</div>
</div>
<!-- 分页信息 -->
<div class="pagination-info">
<span class="pagination-text">
显示 1-10 / 100 条记录
</span>
<select class="per-page-select"
onchange="changePerPage(this.value)"
aria-label="每页显示条数">
<option value="10" selected>10 条/页</option>
<option value="25">25 条/页</option>
<option value="50">50 条/页</option>
<option value="100">100 条/页</option>
</select>
</div>
</header>
<!-- 主内容区域 -->
<div class="content-main">
<div class="content-container">
<!-- 数据展示 -->
<div class="data-display">
<!-- 表格视图 -->
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>邮箱</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>张三</td>
<td>zhangsan@example.com</td>
<td>激活</td>
<td>2024-01-15</td>
<td><button class="action-button" onclick="alert('查看详情')">查看</button></td>
</tr>
<tr>
<td>2</td>
<td>李四</td>
<td>lisi@example.com</td>
<td>激活</td>
<td>2024-01-16</td>
<td><button class="action-button" onclick="alert('查看详情')">查看</button></td>
</tr>
<tr>
<td>3</td>
<td>王五</td>
<td>wangwu@example.com</td>
<td>禁用</td>
<td>2024-01-17</td>
<td><button class="action-button" onclick="alert('查看详情')">查看</button></td>
</tr>
</tbody>
</table>
</div>
<!-- 卡片视图 -->
<div class="data-cards-container" style="display: none;">
<div class="data-cards">
<div class="data-card">
<div class="data-card-title">张三</div>
<div class="data-card-item">
<span class="data-card-label">ID:</span>
<span class="data-card-value">1</span>
</div>
<div class="data-card-item">
<span class="data-card-label">邮箱:</span>
<span class="data-card-value">zhangsan@example.com</span>
</div>
<div class="data-card-item">
<span class="data-card-label">状态:</span>
<span class="data-card-value">激活</span>
</div>
<div style="margin-top: 12px;">
<button class="action-button" onclick="alert('查看详情')">查看详情</button>
</div>
</div>
<div class="data-card">
<div class="data-card-title">李四</div>
<div class="data-card-item">
<span class="data-card-label">ID:</span>
<span class="data-card-value">2</span>
</div>
<div class="data-card-item">
<span class="data-card-label">邮箱:</span>
<span class="data-card-value">lisi@example.com</span>
</div>
<div class="data-card-item">
<span class="data-card-label">状态:</span>
<span class="data-card-value">激活</span>
</div>
<div style="margin-top: 12px;">
<button class="action-button" onclick="alert('查看详情')">查看详情</button>
</div>
</div>
<div class="data-card">
<div class="data-card-title">王五</div>
<div class="data-card-item">
<span class="data-card-label">ID:</span>
<span class="data-card-value">3</span>
</div>
<div class="data-card-item">
<span class="data-card-label">邮箱:</span>
<span class="data-card-value">wangwu@example.com</span>
</div>
<div class="data-card-item">
<span class="data-card-label">状态:</span>
<span class="data-card-value">禁用</span>
</div>
<div style="margin-top: 12px;">
<button class="action-button" onclick="alert('查看详情')">查看详情</button>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination">
<div class="pagination-info">
显示 1-3 / 100 条记录
</div>
<div class="pagination-controls">
<span class="pagination-button disabled">上一页</span>
<span class="pagination-button active">1</span>
<a href="#" class="pagination-button">2</a>
<a href="#" class="pagination-button">3</a>
<a href="#" class="pagination-button">下一页</a>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- 详情模态框 -->
<div id="detailModal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">详细信息</h3>
<button class="modal-close" onclick="closeModal()">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="modal-body">
<div class="detail-item">
<label class="detail-label">姓名</label>
<div class="detail-value">张三</div>
</div>
<div class="detail-item">
<label class="detail-label">邮箱</label>
<div class="detail-value">zhangsan@example.com</div>
</div>
<div class="detail-item">
<label class="detail-label">状态</label>
<div class="detail-value">激活</div>
</div>
</div>
</div>
</div>
<script src="/static/js/app.js"></script>
</body>
</html>