feat: 功能优化
This commit is contained in:
237
frontend/app/web-gold/src/layouts/LayoutExamples.vue
Normal file
237
frontend/app/web-gold/src/layouts/LayoutExamples.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
BasicLayout,
|
||||
CardLayout,
|
||||
TabLayout,
|
||||
FullWidthLayout,
|
||||
FormLayout
|
||||
} from './components'
|
||||
|
||||
// Tab Layout 示例数据
|
||||
const tabs = ref([
|
||||
{
|
||||
key: 'tab1',
|
||||
tab: '标签页1',
|
||||
forceRender: true
|
||||
},
|
||||
{
|
||||
key: 'tab2',
|
||||
tab: '标签页2',
|
||||
forceRender: true
|
||||
},
|
||||
{
|
||||
key: 'tab3',
|
||||
tab: '标签页3',
|
||||
forceRender: false
|
||||
}
|
||||
])
|
||||
|
||||
const activeTabKey = ref('tab1')
|
||||
|
||||
// Form Layout 示例
|
||||
const formData = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
submitLoading.value = true
|
||||
setTimeout(() => {
|
||||
submitLoading.value = false
|
||||
console.log('表单提交:', formData.value)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const handleFormCancel = () => {
|
||||
console.log('表单取消')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-examples">
|
||||
<h2>布局组件使用示例</h2>
|
||||
|
||||
<!-- 1. BasicLayout 示例 -->
|
||||
<h3>1. BasicLayout - 基础布局</h3>
|
||||
<BasicLayout
|
||||
title="基础布局页面"
|
||||
subtitle="这是一个基础布局的示例,适用于大部分页面"
|
||||
:show-back="true"
|
||||
@back="console.log('返回')"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary">额外操作</a-button>
|
||||
</template>
|
||||
|
||||
<div class="example-content">
|
||||
<p>这里放置页面内容...</p>
|
||||
<a-table :columns="[]" :data-source="[]" />
|
||||
</div>
|
||||
</BasicLayout>
|
||||
|
||||
<!-- 2. CardLayout 示例 -->
|
||||
<h3>2. CardLayout - 卡片布局</h3>
|
||||
<CardLayout
|
||||
title="卡片布局页面"
|
||||
subtitle="这是一个卡片布局的示例"
|
||||
:show-back="true"
|
||||
:show-padding="true"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary">操作</a-button>
|
||||
</template>
|
||||
|
||||
<div class="example-content">
|
||||
<p>卡片内容...</p>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="名称">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</CardLayout>
|
||||
|
||||
<!-- 3. TabLayout 示例 -->
|
||||
<h3>3. TabLayout - 标签页布局</h3>
|
||||
<TabLayout
|
||||
:tabs="tabs"
|
||||
v-model:active-key="activeTabKey"
|
||||
:show-back="true"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary">标签页操作</a-button>
|
||||
</template>
|
||||
|
||||
<template #tab1>
|
||||
<div class="tab-content">
|
||||
<h4>标签页1的内容</h4>
|
||||
<p>这是第一个标签页的内容...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #tab2>
|
||||
<div class="tab-content">
|
||||
<h4>标签页2的内容</h4>
|
||||
<p>这是第二个标签页的内容...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #tab3>
|
||||
<div class="tab-content">
|
||||
<h4>标签页3的内容</h4>
|
||||
<p>这是第三个标签页的内容...</p>
|
||||
</div>
|
||||
</template>
|
||||
</TabLayout>
|
||||
|
||||
<!-- 4. FullWidthLayout 示例 -->
|
||||
<h3>4. FullWidthLayout - 全宽布局</h3>
|
||||
<FullWidthLayout
|
||||
:show-back="true"
|
||||
:show-padding="true"
|
||||
>
|
||||
<template #header>
|
||||
<div class="custom-header">
|
||||
<h2>自定义头部</h2>
|
||||
<p>这是全宽布局的自定义头部</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-button type="primary">全宽操作</a-button>
|
||||
</template>
|
||||
|
||||
<div class="example-content">
|
||||
<p>全宽内容...</p>
|
||||
<a-table :columns="[]" :data-source="[]" />
|
||||
</div>
|
||||
</FullWidthLayout>
|
||||
|
||||
<!-- 5. FormLayout 示例 -->
|
||||
<h3>5. FormLayout - 表单布局</h3>
|
||||
<FormLayout
|
||||
title="表单布局页面"
|
||||
subtitle="这是一个表单布局的示例"
|
||||
:show-back="true"
|
||||
:submit-loading="submitLoading"
|
||||
@submit="handleFormSubmit"
|
||||
@cancel="handleFormCancel"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="名称">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="邮箱">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="描述">
|
||||
<a-textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入描述"
|
||||
:rows="4"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</FormLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.layout-examples {
|
||||
padding: 24px;
|
||||
background: var(--color-bg);
|
||||
min-height: 100vh;
|
||||
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 32px 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.example-content {
|
||||
padding: 24px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-card);
|
||||
border: 1px solid var(--color-border);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 24px;
|
||||
background: var(--color-bg);
|
||||
border-radius: var(--radius-card);
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -42,6 +42,6 @@ import SidebarNav from '@/components/SidebarNav.vue'
|
||||
.content-scroll {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto; /* 右侧内容区域滚动 */
|
||||
padding: 0 16px 0 16px;
|
||||
padding: 16px 0px 16px 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
297
frontend/app/web-gold/src/layouts/README.md
Normal file
297
frontend/app/web-gold/src/layouts/README.md
Normal file
@@ -0,0 +1,297 @@
|
||||
# 统一布局组件
|
||||
|
||||
为项目提供5种不同类型的布局组件,保持设计一致性和代码复用性。
|
||||
|
||||
## 布局组件类型
|
||||
|
||||
### 1. BasicLayout - 基础布局
|
||||
最通用的布局组件,适用于大多数页面。
|
||||
|
||||
**特点:**
|
||||
- 标准页面头部(标题 + 副标题)
|
||||
- 灵活的内容区域
|
||||
- 支持返回按钮和额外操作
|
||||
|
||||
**使用场景:**
|
||||
- 列表页面
|
||||
- 详情页面
|
||||
- 数据统计页面
|
||||
|
||||
**示例:**
|
||||
```vue
|
||||
<template>
|
||||
<BasicLayout
|
||||
title="用户列表"
|
||||
subtitle="管理系统中的所有用户"
|
||||
:show-back="true"
|
||||
@back="handleBack"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary">新增用户</a-button>
|
||||
</template>
|
||||
|
||||
<a-table :columns="columns" :data-source="data" />
|
||||
</BasicLayout>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. CardLayout - 卡片布局
|
||||
内容包装在卡片中,提供更好的视觉层次。
|
||||
|
||||
**特点:**
|
||||
- Ant Design Card 容器
|
||||
- 可配置的边距
|
||||
- 灵活的标题栏
|
||||
|
||||
**使用场景:**
|
||||
- 表单页面
|
||||
- 配置页面
|
||||
- 详情展示页面
|
||||
|
||||
**示例:**
|
||||
```vue
|
||||
<template>
|
||||
<CardLayout
|
||||
title="用户信息"
|
||||
:show-back="true"
|
||||
:show-padding="true"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button>编辑</a-button>
|
||||
</template>
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="姓名">
|
||||
<a-input v-model:value="form.name" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</CardLayout>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 3. TabLayout - 标签页布局
|
||||
多标签页内容组织。
|
||||
|
||||
**特点:**
|
||||
- 动态标签页
|
||||
- 支持懒加载
|
||||
- 自定义标签页内容
|
||||
|
||||
**使用场景:**
|
||||
- 设置页面
|
||||
- 数据对比
|
||||
- 多模块内容
|
||||
|
||||
**示例:**
|
||||
```vue
|
||||
<template>
|
||||
<TabLayout
|
||||
:tabs="tabs"
|
||||
v-model:active-key="activeKey"
|
||||
@change="handleTabChange"
|
||||
>
|
||||
<template #tab1>
|
||||
<div>标签页1内容</div>
|
||||
</template>
|
||||
|
||||
<template #tab2>
|
||||
<div>标签页2内容</div>
|
||||
</template>
|
||||
</TabLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const tabs = ref([
|
||||
{ key: 'tab1', tab: '基础信息', forceRender: true },
|
||||
{ key: 'tab2', tab: '高级设置', forceRender: false }
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
### 4. FullWidthLayout - 全宽布局
|
||||
充分利用屏幕宽度的布局。
|
||||
|
||||
**特点:**
|
||||
- 全宽度内容
|
||||
- 自定义头部
|
||||
- 无卡片包装
|
||||
|
||||
**使用场景:**
|
||||
- 数据可视化
|
||||
- 宽屏内容展示
|
||||
- 全屏应用
|
||||
|
||||
**示例:**
|
||||
```vue
|
||||
<template>
|
||||
<FullWidthLayout :show-back="true">
|
||||
<template #header>
|
||||
<h2>数据分析仪表板</h2>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<a-button-group>
|
||||
<a-button>导出</a-button>
|
||||
<a-button>刷新</a-button>
|
||||
</a-button-group>
|
||||
</template>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<!-- 图表组件 -->
|
||||
</div>
|
||||
</FullWidthLayout>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 5. FormLayout - 表单布局
|
||||
专门用于表单页面。
|
||||
|
||||
**特点:**
|
||||
- 居中的表单容器
|
||||
- 内置提交/取消按钮
|
||||
- 加载状态管理
|
||||
|
||||
**使用场景:**
|
||||
- 创建/编辑页面
|
||||
- 复杂表单
|
||||
- 向导式表单
|
||||
|
||||
**示例:**
|
||||
```vue
|
||||
<template>
|
||||
<FormLayout
|
||||
title="创建用户"
|
||||
:submit-loading="submitting"
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="姓名" required>
|
||||
<a-input v-model:value="form.name" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="邮箱" required>
|
||||
<a-input v-model:value="form.email" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</FormLayout>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 设计特色
|
||||
|
||||
### 1. 统一的设计语言
|
||||
- 使用项目的设计令牌(Design Tokens)
|
||||
- 一致的颜色、字体、间距
|
||||
- Notion 风格的标签页设计
|
||||
|
||||
### 2. 响应式设计
|
||||
- 移动端友好
|
||||
- 自适应布局
|
||||
- 弹性内容区域
|
||||
|
||||
### 3. 可访问性
|
||||
- 语义化 HTML
|
||||
- 键盘导航支持
|
||||
- 屏幕阅读器友好
|
||||
|
||||
### 4. 性能优化
|
||||
- 懒加载支持
|
||||
- keep-alive 兼容
|
||||
- 轻量级实现
|
||||
|
||||
## 导入方式
|
||||
|
||||
```javascript
|
||||
// 单独导入
|
||||
import BasicLayout from '@/layouts/components/BasicLayout.vue'
|
||||
import CardLayout from '@/layouts/components/CardLayout.vue'
|
||||
import TabLayout from '@/layouts/components/TabLayout.vue'
|
||||
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
|
||||
import FormLayout from '@/layouts/components/FormLayout.vue'
|
||||
|
||||
// 批量导入
|
||||
import {
|
||||
BasicLayout,
|
||||
CardLayout,
|
||||
TabLayout,
|
||||
FullWidthLayout,
|
||||
FormLayout
|
||||
} from '@/layouts/components'
|
||||
```
|
||||
|
||||
## Props 说明
|
||||
|
||||
### 通用 Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| showBack | Boolean | false | 是否显示返回按钮 |
|
||||
| title | String | '' | 页面标题 |
|
||||
| subtitle | String | '' | 页面副标题 |
|
||||
|
||||
### TabLayout 特有 Props
|
||||
|
||||
| 属性 | 类型 | 必需 | 说明 |
|
||||
|------|------|------|------|
|
||||
| tabs | Array | 是 | 标签页配置数组 |
|
||||
| activeKey | String | 否 | 当前激活的标签页 |
|
||||
|
||||
### FormLayout 特有 Props
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| submitText | String | '提交' | 提交按钮文字 |
|
||||
| cancelText | String | '取消' | 取消按钮文字 |
|
||||
| showCancel | Boolean | true | 是否显示取消按钮 |
|
||||
| submitLoading | Boolean | false | 提交按钮加载状态 |
|
||||
|
||||
## 插槽(Slots)
|
||||
|
||||
### 通用插槽
|
||||
|
||||
- `#extra` - 头部右侧额外操作区域
|
||||
- `#header` - 自定义头部内容(某些布局)
|
||||
|
||||
### TabLayout 插槽
|
||||
|
||||
- `#tab1`, `#tab2`, ... - 标签页内容,名称为 tabs 中的 key
|
||||
|
||||
## 事件(Events)
|
||||
|
||||
### 通用事件
|
||||
|
||||
- `@back` - 点击返回按钮时触发
|
||||
|
||||
### TabLayout 事件
|
||||
|
||||
- `@change(key)` - 标签页切换时触发
|
||||
|
||||
### FormLayout 事件
|
||||
|
||||
- `@submit` - 点击提交按钮时触发
|
||||
- `@cancel` - 点击取消按钮时触发
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **选择合适的布局**
|
||||
- 简单内容 → BasicLayout
|
||||
- 需要卡片容器 → CardLayout
|
||||
- 多标签内容 → TabLayout
|
||||
- 宽屏内容 → FullWidthLayout
|
||||
- 表单页面 → FormLayout
|
||||
|
||||
2. **保持一致性**
|
||||
- 使用统一的设计令牌
|
||||
- 遵循项目的设计规范
|
||||
- 保持交互行为一致
|
||||
|
||||
3. **性能优化**
|
||||
- 设置合适的 `forceRender` 属性
|
||||
- 使用 `keep-alive` 缓存组件
|
||||
- 避免不必要的重渲染
|
||||
|
||||
4. **可访问性**
|
||||
- 为所有交互元素添加适当的标签
|
||||
- 确保键盘导航正常工作
|
||||
- 提供适当的焦点管理
|
||||
125
frontend/app/web-gold/src/layouts/components/BasicLayout.vue
Normal file
125
frontend/app/web-gold/src/layouts/components/BasicLayout.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup>
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
defineOptions({ name: 'BasicLayout' })
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
extra: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['back'])
|
||||
|
||||
// Methods
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="basic-layout">
|
||||
<!-- 页面头部 -->
|
||||
<div class="basic-layout__header">
|
||||
<div class="header-left">
|
||||
<a-button v-if="showBack" type="text" @click="handleBack" class="back-btn">
|
||||
<template #icon>
|
||||
<ArrowLeftOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<h1 v-if="title" class="header-title">{{ title }}</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<div class="basic-layout__content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.basic-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.basic-layout__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: var(--space-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.basic-layout__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
background: var(--bg-primary);
|
||||
padding: var(--space-md);
|
||||
}
|
||||
</style>
|
||||
155
frontend/app/web-gold/src/layouts/components/CardLayout.vue
Normal file
155
frontend/app/web-gold/src/layouts/components/CardLayout.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup>
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
defineOptions({ name: 'CardLayout' })
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showPadding: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['back'])
|
||||
|
||||
// Methods
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card-layout">
|
||||
<!-- 页面头部 -->
|
||||
<div class="card-layout__header">
|
||||
<div class="header-left">
|
||||
<a-button v-if="showBack" type="text" @click="handleBack" class="back-btn">
|
||||
<template #icon>
|
||||
<ArrowLeftOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<h1 v-if="title" class="header-title">{{ title }}</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<div class="card-layout__card" :class="{ 'no-padding': !showPadding }">
|
||||
<div v-if="!$slots.title && title" class="card-header">
|
||||
{{ title }}
|
||||
</div>
|
||||
<slot v-else name="title"></slot>
|
||||
|
||||
<div class="card-layout__content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-layout__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: var(--space-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-layout__card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
|
||||
&.no-padding {
|
||||
.card-layout__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.card-layout__content {
|
||||
flex: 1;
|
||||
padding: var(--space-md);
|
||||
overflow: auto;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
</style>
|
||||
194
frontend/app/web-gold/src/layouts/components/FormLayout.vue
Normal file
194
frontend/app/web-gold/src/layouts/components/FormLayout.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<script setup>
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
defineOptions({ name: 'FormLayout' })
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
submitText: {
|
||||
type: String,
|
||||
default: '提交'
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
},
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
submitLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showFooter: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['submit', 'cancel', 'back'])
|
||||
|
||||
// Methods
|
||||
const handleSubmit = () => {
|
||||
emit('submit')
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-layout">
|
||||
<!-- 页面头部 -->
|
||||
<div class="form-layout__header">
|
||||
<div class="header-left">
|
||||
<a-button v-if="showBack" type="text" @click="handleBack" class="back-btn">
|
||||
<template #icon>
|
||||
<ArrowLeftOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<h1 v-if="title" class="header-title">{{ title }}</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<div class="form-layout__content">
|
||||
<div class="form-container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div v-if="showFooter" class="form-layout__footer">
|
||||
<div class="footer-content">
|
||||
<a-space :size="12">
|
||||
<a-button v-if="showCancel" @click="handleCancel">
|
||||
{{ cancelText }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ submitText }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.form-layout__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: var(--space-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-layout__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: var(--space-md);
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-2xl);
|
||||
}
|
||||
|
||||
.form-layout__footer {
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-top: 1px solid var(--border-light);
|
||||
background: var(--bg-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
122
frontend/app/web-gold/src/layouts/components/FullWidthLayout.vue
Normal file
122
frontend/app/web-gold/src/layouts/components/FullWidthLayout.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
defineOptions({ name: 'FullWidthLayout' })
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showPadding: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['back'])
|
||||
|
||||
// Methods
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="full-width-layout">
|
||||
<!-- 页面头部 -->
|
||||
<div v-if="$slots.header || showBack" class="full-width-layout__header">
|
||||
<div class="header-left">
|
||||
<a-button v-if="showBack" type="text" @click="handleBack" class="back-btn">
|
||||
<template #icon>
|
||||
<ArrowLeftOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<div class="header-content">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全宽内容 -->
|
||||
<div
|
||||
class="full-width-layout__content"
|
||||
:class="{ 'no-padding': !showPadding }"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.full-width-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.full-width-layout__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: var(--space-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.full-width-layout__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--bg-secondary);
|
||||
|
||||
&.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
182
frontend/app/web-gold/src/layouts/components/TabLayout.vue
Normal file
182
frontend/app/web-gold/src/layouts/components/TabLayout.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
defineOptions({ name: 'TabLayout' })
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
tabs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
activeKey: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showBack: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:activeKey', 'change', 'back'])
|
||||
|
||||
// State
|
||||
const activeTabKey = ref(props.activeKey)
|
||||
|
||||
// Watch for external changes
|
||||
watch(() => props.activeKey, (newKey) => {
|
||||
if (newKey !== activeTabKey.value) {
|
||||
activeTabKey.value = newKey
|
||||
}
|
||||
})
|
||||
|
||||
// Methods
|
||||
const handleTabChange = (key) => {
|
||||
activeTabKey.value = key
|
||||
emit('update:activeKey', key)
|
||||
emit('change', key)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tab-layout">
|
||||
<!-- 页面头部 -->
|
||||
<div v-if="$slots.header || showBack" class="tab-layout__header">
|
||||
<div class="header-left">
|
||||
<a-button v-if="showBack" type="text" @click="handleBack" class="back-btn">
|
||||
<template #icon>
|
||||
<ArrowLeftOutlined />
|
||||
</template>
|
||||
</a-button>
|
||||
<div class="header-content">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签页导航 -->
|
||||
<div class="tab-nav">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ 'active': activeTabKey === tab.key }"
|
||||
@click="handleTabChange(tab.key)"
|
||||
>
|
||||
{{ tab.tab }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<div class="tab-content">
|
||||
<slot :name="activeTabKey"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tab-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-layout__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md) var(--space-lg);
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: var(--space-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: var(--space-sm) var(--space-lg);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--text-primary);
|
||||
border-bottom-color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--bg-primary);
|
||||
padding: var(--space-md);
|
||||
}
|
||||
</style>
|
||||
6
frontend/app/web-gold/src/layouts/components/index.js
Normal file
6
frontend/app/web-gold/src/layouts/components/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// 布局组件统一导出
|
||||
export { default as BasicLayout } from './BasicLayout.vue'
|
||||
export { default as CardLayout } from './CardLayout.vue'
|
||||
export { default as TabLayout } from './TabLayout.vue'
|
||||
export { default as FullWidthLayout } from './FullWidthLayout.vue'
|
||||
export { default as FormLayout } from './FormLayout.vue'
|
||||
Reference in New Issue
Block a user