Files
database_render/UI.md
2025-08-06 11:12:16 +08:00

43 KiB
Raw Blame History

UI 左右布局优化方案

当前问题分析

  • 布局问题: 当前为单栏垂直布局,所有内容堆叠在页面中央
  • 空间利用率: 屏幕宽度利用率低,特别是宽屏显示器
  • 导航效率: 表格切换和数据筛选都在顶部,操作路径长
  • 用户体验: 缺乏现代化的应用布局,更像传统网页

优化目标

  • 左右分栏: 左侧固定宽度导航栏,右侧主内容区
  • 响应式设计: 适配不同屏幕尺寸
  • 操作便捷: 减少用户操作路径,提高数据访问效率
  • 现代化UI: 采用现代Web应用设计语言

具体优化步骤

🎨 配色系统优化

专业配色方案

: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;
}

/* 暗色主题支持 */
[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;
}

🎯 交互易用性增强

无障碍设计原则

  • 键盘导航: 所有交互元素支持Tab键导航
  • 焦点指示: 清晰的焦点样式符合WCAG 2.1标准
  • 屏幕阅读器: 适当的ARIA标签和语义化HTML
  • 色彩对比: 所有文本满足4.5:1的对比度要求

阶段1: 基础布局重构

1.1 HTML结构调整

<!-- 新增的布局结构 -->
<div class="app-container">
    <!-- 左侧导航栏 -->
    <aside class="sidebar">
        <!-- 导航内容 -->
    </aside>
    
    <!-- 右侧主内容区 -->
    <main class="main-content">
        <!-- 原内容移到这里 -->
    </main>
</div>

1.2 现代化CSS Grid布局

.app-container {
    display: grid;
    grid-template-columns: 260px 1fr;
    height: 100vh;
    overflow: hidden;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: var(--bg-secondary);
}

.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);
}

.main-content {
    overflow-y: auto;
    background: var(--bg-secondary);
    position: relative;
}

/* 响应式设计 - 更智能的断点 */
@media (max-width: 1024px) {
    .app-container {
        grid-template-columns: 240px 1fr;
    }
}

@media (max-width: 768px) {
    .app-container {
        grid-template-columns: 1fr;
    }
    
    .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 {
        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;
    }
    
    .sidebar.open + .sidebar-backdrop {
        opacity: 1;
        visibility: visible;
    }
}

阶段2: 左侧导航栏设计

2.1 无障碍导航栏结构

<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>
        
        <!-- 主题切换 -->
        <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">
            {{range .Tables}}
            <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}} - {{.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-description">{{.RowCount}} 条记录</span>
                        </div>
                        {{if eq .Name $.Table}}
                        <div class="nav-indicator" aria-hidden="true"></div>
                        {{end}}
                    </div>
                </a>
            </li>
            {{end}}
        </ul>
    </nav>
    
    <!-- 系统信息 -->
    <div class="sidebar-footer">
        <div class="system-info">
            <div class="info-item">
                <span class="info-label">总记录数</span>
                <span class="info-value">{{.Total | formatNumber}}</span>
            </div>
            <div class="info-item">
                <span class="info-label">最后更新</span>
                <span class="info-value">{{.LastUpdate | formatDate}}</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>

2.2 无障碍导航栏样式

.sidebar-header {
    padding: 16px 20px;
    border-bottom: 1px solid var(--border-light);
    background: var(--bg-primary);
}

.brand {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 16px;
}

.brand-icon {
    color: var(--accent-600);
    flex-shrink: 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;
}

.theme-toggle {
    position: absolute;
    top: 16px;
    right: 16px;
    background: none;
    border: none;
    padding: 8px;
    border-radius: 6px;
    color: var(--text-secondary);
    cursor: pointer;
    transition: all 0.2s ease;
}

.theme-toggle:hover {
    background: var(--bg-tertiary);
    color: var(--text-primary);
}

.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;
}

.sidebar-search-input {
    width: 100%;
    padding: 8px 12px 8px 36px;
    border: 1px solid var(--border-medium);
    border-radius: 6px;
    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;
}

.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: 6px;
    transition: all 0.2s ease;
    position: relative;
}

.nav-link:hover {
    background: var(--bg-tertiary);
    color: var(--text-primary);
}

.nav-link:focus {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-200);
}

.nav-link.active {
    background: var(--accent-50);
    color: var(--accent-700);
    font-weight: 500;
}

.nav-link.active .nav-icon {
    color: var(--accent-600);
}

.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;
}

.nav-title {
    display: block;
    font-size: 14px;
    line-height: 1.3;
}

.nav-description {
    display: block;
    font-size: 12px;
    color: var(--text-muted);
    margin-top: 1px;
}

.nav-indicator {
    position: absolute;
    right: -12px;
    top: 50%;
    transform: translateY(-50%);
    width: 3px;
    height: 20px;
    background: var(--accent-600);
    border-radius: 2px;
}

.sidebar-footer {
    padding: 16px 20px;
    border-top: 1px solid var(--border-light);
    background: var(--bg-tertiary);
}

.system-info {
    margin-bottom: 12px;
}

.info-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 12px;
    color: var(--text-secondary);
    margin-bottom: 4px;
}

.info-label {
    font-weight: 500;
}

.info-value {
    color: var(--text-primary);
    font-weight: 600;
}

.settings-button {
    width: 100%;
    padding: 8px 12px;
    background: none;
    border: 1px solid var(--border-medium);
    border-radius: 6px;
    color: var(--text-secondary);
    font-size: 13px;
    cursor: pointer;
    transition: all 0.2s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
}

.settings-button:hover {
    background: var(--bg-primary);
    color: var(--text-primary);
    border-color: var(--border-dark);
}

.settings-button:focus {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent-200);
}

/* 滚动条优化 */
.sidebar::-webkit-scrollbar {
    width: 6px;
}

.sidebar::-webkit-scrollbar-track {
    background: transparent;
}

.sidebar::-webkit-scrollbar-thumb {
    background: var(--border-medium);
    border-radius: 3px;
}

.sidebar::-webkit-scrollbar-thumb:hover {
    background: var(--border-dark);
}

阶段3: 右侧内容区优化

3.1 智能头部区域重构

<!-- 移动端菜单按钮 -->
<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 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>
    </div>
</header>

3.2 智能数据表格优化

.content-header {
    background: var(--bg-primary);
    border-bottom: 1px solid var(--border-light);
    padding: 0;
}

.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: 6px;
    color: var(--text-secondary);
    cursor: pointer;
    transition: all 0.2s ease;
}

.mobile-menu-toggle:hover {
    background: var(--bg-tertiary);
    color: var(--text-primary);
}

.breadcrumb {
    flex: 1;
    min-width: 200px;
}

.breadcrumb-list {
    display: flex;
    align-items: center;
    list-style: none;
    padding: 0;
    margin: 0;
    gap: 8px;
}

.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: 6px;
    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-clear {
    position: absolute;
    right: 8px;
    background: none;
    border: none;
    padding: 4px;
    color: var(--text-muted);
    cursor: pointer;
    border-radius: 3px;
    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: 6px;
    font-size: 14px;
    cursor: pointer;
    transition: all 0.2s ease;
}

.search-button:hover {
    background: var(--accent-700);
}

.search-button:focus {
    outline: none;
    box-shadow: 0 0 0 3px var(--accent-200);
}

.view-controls {
    display: flex;
    align-items: center;
    background: var(--bg-secondary);
    border: 1px solid var(--border-medium);
    border-radius: 6px;
    overflow: hidden;
}

.view-button {
    padding: 8px;
    background: none;
    border: none;
    color: var(--text-secondary);
    cursor: pointer;
    transition: all 0.2s ease;
}

.view-button:hover {
    background: var(--bg-tertiary);
    color: var(--text-primary);
}

.view-button.active {
    background: var(--accent-100);
    color: var(--accent-700);
}

.export-dropdown {
    position: relative;
}

.export-button {
    padding: 8px 12px;
    background: var(--bg-secondary);
    border: 1px solid var(--border-medium);
    border-radius: 6px;
    color: var(--text-secondary);
    cursor: pointer;
    transition: all 0.2s ease;
}

.export-button:hover {
    background: var(--bg-tertiary);
    color: var(--text-primary);
}

.export-menu {
    position: absolute;
    top: 100%;
    right: 0;
    margin-top: 4px;
    background: var(--bg-primary);
    border: 1px solid var(--border-light);
    border-radius: 6px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    min-width: 120px;
    z-index: 1000;
    display: none;
}

.export-menu.show {
    display: block;
}

.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:first-child {
    border-top-left-radius: 6px;
    border-top-right-radius: 6px;
}

.export-option:last-child {
    border-bottom-left-radius: 6px;
    border-bottom-right-radius: 6px;
}

.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);
}

.pagination-text {
    font-size: 14px;
    color: var(--text-secondary);
}

.per-page-select {
    padding: 4px 8px;
    border: 1px solid var(--border-medium);
    border-radius: 4px;
    font-size: 14px;
    background: var(--bg-primary);
    color: var(--text-primary);
    cursor: pointer;
}

.per-page-select:focus {
    outline: none;
    border-color: var(--accent-500);
    box-shadow: 0 0 0 2px var(--accent-100);
}

@media (max-width: 768px) {
    .mobile-menu-toggle {
        display: block;
    }
    
    .header-toolbar {
        flex-direction: column;
        align-items: stretch;
        gap: 12px;
    }
    
    .search-input {
        width: 100%;
    }
    
    .header-tools {
        justify-content: center;
    }
    
    .pagination-info {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
}

阶段4: 响应式交互增强

4.1 现代化JavaScript交互

// 应用状态管理
class AppState {
    constructor() {
        this.searchTimeout = null;
        this.currentView = 'table';
        this.exportMenuOpen = false;
        this.init();
    }

    init() {
        this.setupThemeToggle();
        this.setupSidebarToggle();
        this.setupSearch();
        this.setupKeyboardShortcuts();
        this.setupAccessibility();
    }

    // 主题切换
    setupThemeToggle() {
        const themeToggle = document.querySelector('.theme-toggle');
        if (themeToggle) {
            themeToggle.addEventListener('click', () => this.toggleTheme());
        }

        // 初始化主题
        const savedTheme = localStorage.getItem('theme') || 'light';
        this.setTheme(savedTheme);
    }

    toggleTheme() {
        const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
        const newTheme = currentTheme === 'light' ? 'dark' : 'light';
        this.setTheme(newTheme);
    }

    setTheme(theme) {
        document.documentElement.setAttribute('data-theme', theme);
        localStorage.setItem('theme', theme);
    }

    // 侧边栏管理
    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', () => {
            if (window.innerWidth > 768) {
                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';
    }

    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 = '';
    }

    // 智能搜索
    setupSearch() {
        const searchInput = document.getElementById('searchInput');
        const searchClear = document.querySelector('.search-clear');

        if (searchInput) {
            searchInput.addEventListener('input', (e) => {
                this.debounceSearch(e.target.value);
            });
        }

        if (searchClear) {
            searchClear.addEventListener('click', () => this.clearSearch());
        }
    }

    debounceSearch(query) {
        clearTimeout(this.searchTimeout);
        this.searchTimeout = setTimeout(() => {
            this.performSearch(query);
        }, 300);
    }

    performSearch(query) {
        const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
        const params = new URLSearchParams(window.location.search);
        
        if (query.trim()) {
            params.set('search', query);
        } else {
            params.delete('search');
        }
        params.set('page', '1');
        
        window.location.href = `/?${params.toString()}`;
    }

    clearSearch() {
        const searchInput = document.getElementById('searchInput');
        const searchClear = document.querySelector('.search-clear');
        
        if (searchInput) searchInput.value = '';
        if (searchClear) searchClear.style.display = 'none';
        
        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 description = link.querySelector('.nav-description').textContent.toLowerCase();
            
            if (title.includes(searchTerm) || description.includes(searchTerm)) {
                link.style.display = 'block';
                link.parentElement.style.display = 'block';
            } else {
                link.style.display = 'none';
                link.parentElement.style.display = 'none';
            }
        });
    }

    // 视图切换
    setViewMode(mode) {
        this.currentView = mode;
        
        // 更新按钮状态
        document.querySelectorAll('.view-button').forEach(btn => {
            btn.classList.remove('active');
        });
        document.querySelector(`[onclick="setViewMode('${mode}')"]`).classList.add('active');

        // 保存用户偏好
        localStorage.setItem('viewMode', mode);
        
        // 切换视图
        const content = document.querySelector('.main-content');
        content.setAttribute('data-view', mode);
    }

    // 导出功能
    toggleExportMenu() {
        const menu = document.querySelector('.export-menu');
        const button = document.querySelector('.export-button');
        
        if (menu.classList.contains('show')) {
            menu.classList.remove('show');
            button.setAttribute('aria-expanded', 'false');
        } else {
            menu.classList.add('show');
            button.setAttribute('aria-expanded', 'true');
            
            // 点击外部关闭
            setTimeout(() => {
                document.addEventListener('click', this.closeExportMenu, { once: true });
            }, 0);
        }
    }

    closeExportMenu = (e) => {
        const menu = document.querySelector('.export-menu');
        const button = document.querySelector('.export-button');
        
        if (!button.contains(e.target)) {
            menu.classList.remove('show');
            button.setAttribute('aria-expanded', 'false');
        }
    }

    exportData(format) {
        const table = new URLSearchParams(window.location.search).get('table') || '{{.Table}}';
        const params = new URLSearchParams(window.location.search);
        params.set('format', format);
        
        window.location.href = `/api/export/${table}?${params.toString()}`;
        this.toggleExportMenu();
    }

    // 键盘快捷键
    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();
            }

            // Esc 关闭侧边栏/菜单
            if (e.key === 'Escape') {
                this.closeSidebar();
                this.closeExportMenu({ target: document });
            }

            // Ctrl/Cmd + , 打开设置
            if ((e.ctrlKey || e.metaKey) && e.key === ',') {
                e.preventDefault();
                this.openSettings();
            }
        });
    }

    // 无障碍支持
    setupAccessibility() {
        // 焦点管理
        document.addEventListener('focusin', (e) => {
            const sidebar = document.querySelector('.sidebar');
            const isInSidebar = sidebar.contains(e.target);
            const isMobile = window.innerWidth <= 768;
            
            if (isMobile && !isInSidebar && sidebar.classList.contains('open')) {
                this.closeSidebar();
            }
        });

        // 实时搜索反馈
        const searchInput = document.getElementById('searchInput');
        if (searchInput) {
            searchInput.addEventListener('input', (e) => {
                const clearButton = document.querySelector('.search-clear');
                if (clearButton) {
                    clearButton.style.display = e.target.value ? 'block' : 'none';
                }
            });
        }
    }

    // 设置面板
    openSettings() {
        // 可以扩展为模态框
        console.log('打开设置面板');
    }

    changePerPage(perPage) {
        const params = new URLSearchParams(window.location.search);
        params.set('per_page', perPage);
        params.set('page', '1');
        window.location.href = `/?${params.toString()}`;
    }
}

// 初始化应用
const app = new AppState();

// 全局函数
function toggleSidebar() { app.toggleSidebar(); }
function closeSidebar() { app.closeSidebar(); }
function debounceSearch(query) { app.debounceSearch(query); }
function performSearch() { app.performSearch(document.getElementById('searchInput').value); }
function clearSearch() { app.clearSearch(); }
function filterTables(query) { app.filterTables(query); }
function setViewMode(mode) { app.setViewMode(mode); }
function toggleExportMenu() { app.toggleExportMenu(); }
function exportData(format) { app.exportData(format); }
function changePerPage(perPage) { app.changePerPage(perPage); }

// 页面加载完成后恢复用户偏好
document.addEventListener('DOMContentLoaded', () => {
    const savedViewMode = localStorage.getItem('viewMode') || 'table';
    app.setViewMode(savedViewMode);
});

阶段5: 性能优化与无障碍设计

5.1 性能优化策略

/* 渐进式加载动画 */
@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;
}

/* 硬件加速 */
.sidebar,
.content-header,
.nav-link {
    will-change: transform, opacity;
    transform: translateZ(0);
}

/* 延迟加载 */
.lazy-image {
    opacity: 0;
    transition: opacity 0.3s ease;
}

.lazy-image.loaded {
    opacity: 1;
}

/* 减少重绘 */
.nav-link,
.view-button {
    backface-visibility: hidden;
}

5.2 无障碍设计增强

/* 焦点指示器 */
:focus-visible {
    outline: 2px solid var(--accent-600);
    outline-offset: 2px;
}

/* 减少动画偏好 */
@media (prefers-reduced-motion: reduce) {
    .sidebar,
    .nav-link,
    .content-header {
        transition: none;
    }
    
    .loading-shimmer {
        animation: none;
    }
}

/* 高对比度模式 */
@media (prefers-contrast: high) {
    :root {
        --border-light: #000;
        --border-medium: #000;
        --border-dark: #000;
    }
}

/* 语义化颜色 */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* 键盘导航支持 */
.nav-link:focus-visible,
.view-button:focus-visible,
.export-button:focus-visible {
    outline: 2px solid var(--accent-600);
    outline-offset: 2px;
}

/* 屏幕阅读器支持 */
.aria-live {
    position: absolute;
    left: -10000px;
    top: auto;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

5.3 用户体验增强

// 加载状态管理
class LoadingManager {
    constructor() {
        this.loadingStates = new Map();
    }

    showLoading(selector, message = '加载中...') {
        const element = document.querySelector(selector);
        if (element) {
            element.classList.add('loading-shimmer');
            element.setAttribute('aria-busy', 'true');
            element.setAttribute('aria-label', message);
        }
    }

    hideLoading(selector) {
        const element = document.querySelector(selector);
        if (element) {
            element.classList.remove('loading-shimmer');
            element.setAttribute('aria-busy', 'false');
            element.removeAttribute('aria-label');
        }
    }
}

// 错误处理
class ErrorHandler {
    static show(message, type = 'error') {
        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);
    }
}

// 响应式断点检测
class ResponsiveManager {
    constructor() {
        this.breakpoints = {
            mobile: 768,
            tablet: 1024,
            desktop: 1440
        };
        this.currentBreakpoint = this.getCurrentBreakpoint();
        this.setupListeners();
    }

    getCurrentBreakpoint() {
        const width = window.innerWidth;
        if (width <= this.breakpoints.mobile) return 'mobile';
        if (width <= this.breakpoints.tablet) return 'tablet';
        return 'desktop';
    }

    setupListeners() {
        window.addEventListener('resize', () => {
            const newBreakpoint = this.getCurrentBreakpoint();
            if (newBreakpoint !== this.currentBreakpoint) {
                this.currentBreakpoint = newBreakpoint;
                this.onBreakpointChange(newBreakpoint);
            }
        });
    }

    onBreakpointChange(breakpoint) {
        document.documentElement.setAttribute('data-breakpoint', breakpoint);
    }
}

// 初始化
const responsiveManager = new ResponsiveManager();

5.4 国际化支持

// 多语言支持
const i18n = {
    zh: {
        search: '搜索',
        clear: '清除',
        loading: '加载中...',
        noResults: '没有找到结果',
        export: '导出',
        settings: '设置'
    },
    en: {
        search: 'Search',
        clear: 'Clear',
        loading: 'Loading...',
        noResults: 'No results found',
        export: 'Export',
        settings: 'Settings'
    }
};

// 语言检测
function detectLanguage() {
    return navigator.language.startsWith('zh') ? 'zh' : 'en';
}

// 获取翻译
function t(key) {
    const lang = detectLanguage();
    return i18n[lang][key] || key;
}

实施优先级

高优先级 (立即实施)

  1. 专业配色系统设计
  2. 无障碍导航栏重构
  3. 智能搜索功能
  4. 响应式布局实现
  5. 暗色主题支持

中优先级 (1-2天内)

  1. 键盘快捷键支持
  2. 导出功能实现
  3. 视图模式切换
  4. 性能优化
  5. 无障碍设计增强

低优先级 (后续迭代)

  1. 国际化支持
  2. 高级筛选器
  3. 数据可视化
  4. 个性化设置面板
  5. 实时通知系统

用户体验指标

可访问性 (WCAG 2.1 标准)

  • 键盘导航支持
  • 屏幕阅读器兼容
  • 色彩对比度 4.5:1
  • 焦点指示器清晰
  • 语义化HTML结构

性能指标

  • 首屏加载 < 2秒
  • 交互响应 < 100ms
  • 动画流畅 60fps
  • 内存占用优化
  • 渐进式增强

交互体验

  • 智能搜索 (300ms防抖)
  • 一键清除搜索
  • 快捷键支持 (Ctrl+K)
  • 移动端手势支持
  • 实时状态反馈

设计验证

用户体验测试

  • 用户任务完成率 > 90%
  • 用户满意度评分 > 4.5/5
  • 平均操作时间减少 50%
  • 错误率降低 80%

技术验证

  • Lighthouse评分 > 95
  • 可访问性评分 > 100
  • 性能评分 > 90
  • SEO评分 > 100

总结

本次优化从配色和交互易用性角度出发,实现了:

🎨 专业配色系统: 基于中性色+强调色的现代配色方案,支持明暗主题 🎯 无障碍设计: 符合WCAG 2.1标准,支持键盘导航和屏幕阅读器 智能交互: 防抖搜索、快捷键、响应式断点检测 📱 多端适配: 桌面、平板、手机完美适配 🔧 扩展性强: 模块化设计,支持后续功能扩展

所有代码遵循KISS、YAGNI、DRY原则确保简洁高效且易于维护。

测试验证

功能测试

  • 表格切换功能正常
  • 搜索功能正常工作
  • 分页功能正常
  • 详情弹窗正常显示

响应式测试

  • 桌面端 (>1024px) 显示正常
  • 平板端 (768px-1024px) 显示正常
  • 移动端 (<768px) 侧边栏可正常开关

性能测试

  • 初始加载时间 < 2秒
  • 侧边栏切换流畅无卡顿
  • 响应式切换无闪烁

文件变更清单

需要修改的文件

  • web/templates/list.html - 主要布局重构
  • web/static/css/app.css - 新增样式文件
  • web/static/js/app.js - 新增交互逻辑

新增文件

  • web/static/css/components/sidebar.css - 侧边栏专用样式
  • web/static/css/components/content.css - 内容区样式
  • web/static/css/responsive.css - 响应式样式

回滚方案

保留原始文件备份,如出现问题可快速回滚到垂直布局版本。