Files
ProjectAGiPrompt/1-Vue3项目/user-dashboard-style-guide.md
2026-01-21 16:15:49 +08:00

17 KiB
Raw Blame History

description
description
UserDashboard 页面样式与布局设计指南 - 供后续页面设计参考

UserDashboard 页面样式与布局设计指南

概述

此文档描述了 UserDashboard.vue 页面的样式和布局设计模式,可作为后续页面设计的参考标准。该页面采用 Vue 3 + Vuetify 3 + TypeScript 技术栈,实现了一个分层钻取式(Drill-Down)导航的 Dashboard 界面。


1. 整体页面结构

1.1 布局架构

┌─────────────────────────────────────────────────────────────┐
│  顶部工具栏 (Top Bar)                                        │
│  - 面包屑导航 (Breadcrumbs)                                  │
│  - 全局搜索框 (Search Autocomplete)                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  主内容区域 (Main Content Area)                              │
│  - 可滚动区域                                                │
│  - 根据导航层级动态切换内容组件                               │
│                                                             │
│                                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 根容器设置

<v-container 
  fluid 
  class="d-flex flex-column h-100 pa-0" 
  style="width: 95%; height: 100vh; min-height: 0;"
>

设计要点:

  • 使用 fluid 流式布局
  • d-flex flex-column 实现垂直弹性布局
  • h-100 占满高度
  • pa-0 移除内边距
  • 宽度 95% 留出两侧适当边距

2. 顶部工具栏设计

2.1 结构

<div class="d-flex align-center px-4 py-2 bg-white border-b flex-shrink-0">
  <!-- 面包屑导航 -->
  <DashboardBreadcrumbs class="flex-grow-1" ... />
  
  <!-- 搜索框 -->
  <div style="width: 350px" class="ml-4">
    <v-autocomplete ... />
  </div>
</div>

2.2 设计要点

  • 固定高度:使用 flex-shrink-0 防止压缩
  • 背景色bg-white 白色背景
  • 底部边框border-b 分隔线
  • 内边距px-4 py-2 水平16px垂直8px
  • 内容对齐d-flex align-center 水平排列垂直居中

2.3 面包屑导航设计

<div class="px-4 py-2 d-flex align-center">
  <v-btn variant="text" @click="..." :disabled="...">
    层级名称
  </v-btn>
  <v-icon v-if="..." size="small" class="mx-2">mdi-chevron-right</v-icon>
  <!-- 更多层级... -->
</div>

特点:

  • 使用 v-btn variant="text" 作为可点击导航项
  • 使用 mdi-chevron-right 图标作为分隔符
  • 图标尺寸 size="small",间距 mx-2
  • 当前层级设置 :disabled="true" 表示不可点击

2.4 搜索框设计

<v-autocomplete 
  v-model="treeSearchSelect" 
  v-model:search="treeSearchQuery" 
  :items="treeSearchResults"
  :loading="treeSearchLoading" 
  item-title="name" 
  item-value="id" 
  placeholder="Search Org / Repo / Branch..."
  variant="outlined" 
  density="compact" 
  hide-details 
  prepend-inner-icon="mdi-magnify" 
  return-object 
  hide-no-data
  bg-color="grey-lighten-5" 
  clearable
>

特点:

  • variant="outlined" 外框样式
  • density="compact" 紧凑型高度
  • bg-color="grey-lighten-5" 浅灰色背景
  • prepend-inner-icon="mdi-magnify" 搜索图标
  • hide-details 隐藏辅助文本空间
  • clearable 可清除按钮

3. 主内容区域设计

3.1 滚动容器

<div class="flex-grow-1 overflow-y-auto custom-scrollbar"
  style="height: 80vh; min-height: 0; overflow-y: auto; overflow-x: hidden;">

设计要点:

  • flex-grow-1 填充剩余空间
  • overflow-y-auto 垂直滚动
  • overflow-x-hidden 隐藏水平滚动
  • 自定义滚动条样式

3.2 自定义滚动条样式

.custom-scrollbar {
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}

.custom-scrollbar::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

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

.custom-scrollbar::-webkit-scrollbar-thumb {
  background-color: rgba(0, 0, 0, 0.2);
  border-radius: 3px;
}

特点:

  • 细滚动条 width: 6px
  • 透明轨道
  • 半透明滚动条滑块
  • 圆角处理

4. 列表组件设计模式

4.1 通用列表结构

<v-list lines="two" class="pa-2">
  <v-list-subheader class="text-h6 font-weight-medium">
    <v-icon class="mr-2">图标</v-icon>
    列表标题
  </v-list-subheader>
  
  <!-- 空状态 -->
  <v-empty-state
    v-if="items.length === 0"
    icon="mdi-database-off-outline"
    title="暂无数据"
    text="描述文本"
  />
  
  <!-- 列表项 -->
  <v-list-item
    v-for="item in items"
    :key="item.id"
    @click="..."
    class="mb-2"
    rounded
    hover
  >
    <!-- 前置图标 -->
    <template #prepend>
      <v-avatar color="primary" size="40">
        <v-icon>图标</v-icon>
      </v-avatar>
    </template>
    
    <!-- 标题 -->
    <v-list-item-title class="font-weight-medium">
      {{ item.name }}
    </v-list-item-title>
    
    <!-- 副标题 -->
    <v-list-item-subtitle>...</v-list-item-subtitle>
    
    <!-- 后置图标 -->
    <template #append>
      <v-icon>mdi-chevron-right</v-icon>
    </template>
  </v-list-item>
</v-list>

4.2 列表项悬停效果

.v-list-item {
  transition: all 0.2s ease;
}

.v-list-item:hover {
  background-color: rgba(var(--v-theme-primary), 0.08);
}

5. 数据表格设计模式

5.1 表格工具栏

<v-toolbar color="transparent" density="compact" class="px-4">
  <v-icon class="mr-2">mdi-hammer-wrench</v-icon>
  <v-toolbar-title class="text-h6">表格标题</v-toolbar-title>
  <v-spacer />
  <v-btn color="primary" prepend-icon="mdi-plus" class="mr-2">
    操作按钮
  </v-btn>
  <v-btn icon="mdi-refresh" variant="text" @click="..." :loading="loading" />
</v-toolbar>

5.2 数据表格

<v-data-table
  :headers="headers"
  :items="items"
  :items-per-page="50"
  :items-per-page-options="[25, 50, 100]"
  fixed-header
  height="calc(100vh - 280px)"
  class="elevation-1"
  hover
  @click:row="handleRowClick"
>
  <!-- 自定义列模板 -->
</v-data-table>

5.3 状态标签设计

<v-chip :color="getStatusColor(status)" size="small" variant="flat">
  <v-icon start size="x-small">{{ getStatusIcon(status) }}</v-icon>
  {{ status }}
</v-chip>

状态颜色映射:

const statusColors = {
  'SUCCESS': 'success',
  'FAILURE': 'error',
  'UNSTABLE': 'warning',
  'ABORTED': 'grey',
  'RUNNING': 'info'
}

6. 详情页面设计模式

6.1 详情页头部

<div class="d-flex align-center justify-space-between pa-4 bg-surface w-100">
  <!-- 左侧组 -->
  <div class="d-flex align-center ga-3">
    <v-btn icon="mdi-arrow-left" variant="text" @click="goBack" />
    <span class="text-h6 font-weight-bold">标题</span>
    <v-chip :color="statusColor" size="small" variant="flat">状态</v-chip>
  </div>
  
  <!-- 右侧组 -->
  <div class="d-flex align-center ga-2">
    <v-btn color="info" variant="tonal" prepend-icon="mdi-eye">操作1</v-btn>
    <v-btn color="primary" variant="flat" prepend-icon="mdi-open-in-new">操作2</v-btn>
  </div>
</div>

6.2 详情卡片分组

<v-row>
  <!-- 左侧列主要信息 -->
  <v-col cols="12" md="7">
    <v-card class="mb-4" variant="outlined">
      <v-card-title class="text-subtitle-1 font-weight-bold">
        <v-icon start size="small" color="primary">mdi-icon</v-icon>
        卡片标题
      </v-card-title>
      <v-divider />
      <v-card-text class="pa-4">
        <!-- 详情内容 -->
      </v-card-text>
    </v-card>
  </v-col>
  
  <!-- 右侧列元信息 -->
  <v-col cols="12" md="5">
    <!-- 更多卡片... -->
  </v-col>
</v-row>

6.3 详情项组件 (DetailItem)

<div class="d-flex align-center mb-5 detail-item">
  <!-- 标签区域固定宽度 -->
  <div class="d-flex align-center text-medium-emphasis flex-shrink-0" style="width: 140px;">
    <v-icon size="small" class="mr-2" color="primary">{{ icon }}</v-icon>
    <span class="text-body-2 font-weight-medium">{{ label }}</span>
  </div>
  
  <!-- 值区域胶囊样式 -->
  <div class="flex-grow-1 d-flex align-center justify-space-between px-4 py-2 rounded-pill bg-grey-lighten-5 value-capsule">
    <div class="text-body-2 font-weight-medium text-truncate flex-grow-1 mr-2">
      <slot name="value">{{ value }}</slot>
    </div>
    
    <v-btn v-if="copyable" icon="mdi-content-copy" variant="text" size="x-small" 
      color="medium-emphasis" class="copy-btn" @click="copy" />
  </div>
</div>

胶囊样式 CSS

.value-capsule {
  transition: all 0.2s ease;
  min-height: 44px;
  border: 1px solid rgba(0, 0, 0, 0.05);
}

.value-capsule:hover {
  background-color: rgba(var(--v-theme-on-surface), 0.08) !important;
  border-color: rgba(0, 0, 0, 0.1);
}

.copy-btn {
  opacity: 0;
  transition: opacity 0.2s ease;
}

.value-capsule:hover .copy-btn {
  opacity: 1;
}

7. 弹窗设计模式

7.1 标准弹窗

<v-dialog v-model="showDialog" max-width="60vh" max-height="80vh">
  <v-card>
    <v-card-title>弹窗标题</v-card-title>
    <v-divider />
    <v-card-text>内容</v-card-text>
    <v-card-actions>
      <v-spacer />
      <v-btn variant="text" @click="cancel">取消</v-btn>
      <v-btn color="primary" @click="confirm">确认</v-btn>
    </v-card-actions>
  </v-card>
</v-dialog>

7.2 全屏弹窗(日志查看器)

<v-dialog v-model="showDialog" fullscreen transition="dialog-bottom-transition">
  <v-card>
    <v-toolbar color="primary" density="compact">
      <v-btn icon="mdi-close" @click="close"></v-btn>
      <v-toolbar-title>全屏弹窗标题</v-toolbar-title>
    </v-toolbar>
    <v-card-text class="pa-0 h-100">
      <!-- 内容 -->
    </v-card-text>
  </v-card>
</v-dialog>

8. 状态处理模式

8.1 加载状态

<div v-if="loading" class="pa-4">
  <v-skeleton-loader type="list-item@5" />
</div>

8.2 错误状态

<v-alert 
  v-else-if="error" 
  type="error" 
  variant="tonal" 
  class="ma-4" 
  closable 
  @click:close="error = null"
>
  {{ error }}
</v-alert>

8.3 空状态

<v-empty-state
  icon="mdi-database-off-outline"
  title="暂无数据"
  text="描述性文本说明"
/>

9. 颜色规范

9.1 主题颜色使用

用途 颜色 Vuetify类/变量
主要操作 蓝色 primary
次要操作 灰色 secondary
成功状态 绿色 success
错误状态 红色 error
警告状态 橙色 warning
提示信息 蓝色 info
禁用/中性 灰色 grey

9.2 背景颜色

  • 页面背景:默认(白色/浅灰)
  • 卡片背景:bg-surface
  • 输入框背景:bg-grey-lighten-5
  • 胶囊值背景:bg-grey-lighten-5

10. 间距规范

10.1 Vuetify 间距系统

像素 使用场景
1 4px 极小间距
2 8px 小间距
3 12px 中等间距
4 16px 标准间距
5 24px 大间距
6 32px 超大间距

10.2 常用间距组合

  • 卡片内边距:pa-416px
  • 列表项底部间距:mb-28px
  • 详情项底部间距:mb-524px
  • 按钮组间距:ga-2ga-3

11. 图标使用规范

11.1 常用图标

用途 图标
组织 mdi-domain / mdi-folder-network
仓库 mdi-source-repository
分支 mdi-source-branch
构建 mdi-hammer-wrench
搜索 mdi-magnify
导航 mdi-chevron-right / mdi-arrow-left
刷新 mdi-refresh
关闭 mdi-close
复制 mdi-content-copy
外部链接 mdi-open-in-new
日志 mdi-text-box-outline

11.2 图标尺寸

  • 列表项图标:size="small" 或默认
  • 状态标签图标:size="x-small"
  • 头像图标:默认(在 v-avatar 内)
  • 面包屑分隔符:size="small"

12. 组件化设计原则

12.1 组件拆分原则

  1. 单一职责:每个组件只负责一个功能
  2. 可复用性:将通用 UI 模式抽取为独立组件
  3. Props 驱动:通过 Props 传递数据,通过 Events 传递交互

12.2 本项目组件结构

components/
├── projects/
│   ├── DashboardBreadcrumbs.vue  # 面包屑导航
│   ├── OrganizationList.vue      # 组织列表
│   ├── RepositoryList.vue        # 仓库列表
│   ├── BranchList.vue            # 分支列表
│   ├── BuildList.vue             # 构建列表
│   └── BuildDetails.vue          # 构建详情
├── common/
│   └── DetailItem.vue            # 详情项组件
└── jenkins/
    ├── BuildTrigger.vue          # 构建触发器
    └── LogViewer.vue             # 日志查看器

13. 交互模式

13.1 导航模式

  • 钻取式导航:点击列表项进入下一层级
  • 面包屑回溯:点击面包屑返回上层
  • 全局搜索:快速跳转到任意层级

13.2 动画效果

  • 过渡动画:transition: all 0.2s ease
  • 弹窗动画:transition="dialog-bottom-transition"
  • 悬停效果:背景色变化、元素显现

13.3 自动刷新

  • 列表数据刷新10秒间隔有运行中任务时3秒
  • 详情数据刷新3秒间隔仅运行中状态

使用示例

创建新的钻取式页面

<template>
  <v-container fluid class="d-flex flex-column h-100 pa-0" style="width: 95%; height: 100vh; min-height: 0;">
    <!-- 顶部栏 -->
    <div class="d-flex align-center px-4 py-2 bg-white border-b flex-shrink-0">
      <YourBreadcrumbs ... />
      <div style="width: 350px" class="ml-4">
        <v-autocomplete ... />
      </div>
    </div>
    
    <v-divider />
    
    <!-- 主内容区 -->
    <div class="flex-grow-1 overflow-y-auto custom-scrollbar" style="height: 80vh; min-height: 0;">
      <div v-if="loading" class="pa-4">
        <v-skeleton-loader type="list-item@5" />
      </div>
      
      <v-alert v-else-if="error" type="error" variant="tonal" class="ma-4" closable>
        {{ error }}
      </v-alert>
      
      <YourList1 v-else-if="currentLevel === 'level1'" ... />
      <YourList2 v-else-if="currentLevel === 'level2'" ... />
      <YourDetails v-else-if="currentLevel === 'details'" ... />
    </div>
  </v-container>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'

// Navigation State
type NavigationLevel = 'level1' | 'level2' | 'details'
const currentLevel = ref<NavigationLevel>('level1')
const selectedItem1 = ref<string>('')
const selectedItem2 = ref<string>('')

// Data State
const items1 = ref<Item1[]>([])
const items2 = ref<Item2[]>([])
const details = ref<Details | null>(null)

// UI State
const loading = ref(false)
const error = ref<string | null>(null)

// Auto-refresh timer
const refreshTimer = ref<number | null>(null)

// Navigation Functions
function selectItem1(name: string) {
  selectedItem1.value = name
  currentLevel.value = 'level2'
  loadItems2(name)
}

function selectItem2(name: string) {
  selectedItem2.value = name
  currentLevel.value = 'details'
  loadDetails(selectedItem1.value, name)
}

function navigateToLevel1() {
  clearTimer()
  currentLevel.value = 'level1'
  selectedItem1.value = ''
  selectedItem2.value = ''
}

function navigateToLevel2() {
  clearTimer()
  currentLevel.value = 'level2'
  selectedItem2.value = ''
}

// Timer Management
function startRefresh() {
  stopRefresh()
  refreshTimer.value = window.setInterval(() => {
    // Refresh logic
  }, 10000)
}

function stopRefresh() {
  if (refreshTimer.value) {
    clearInterval(refreshTimer.value)
    refreshTimer.value = null
  }
}

function clearTimer() {
  stopRefresh()
}

// Lifecycle
onMounted(() => {
  loadItems1()
})

onBeforeUnmount(() => {
  clearTimer()
})
</script>

<style scoped>
.custom-scrollbar {
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
}

.custom-scrollbar::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

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

.custom-scrollbar::-webkit-scrollbar-thumb {
  background-color: rgba(0, 0, 0, 0.2);
  border-radius: 3px;
}
</style>