feat: 优化
This commit is contained in:
@@ -13,7 +13,7 @@ const emit = defineEmits(['back'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="basic-layout">
|
||||
<div class="flex flex-col h-full overflow-hidden rounded-xl border bg-card">
|
||||
<LayoutHeader
|
||||
:title="title"
|
||||
:show-back="showBack"
|
||||
@@ -25,30 +25,12 @@ const emit = defineEmits(['back'])
|
||||
</LayoutHeader>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<div class="basic-layout__content">
|
||||
<div class="flex-1 flex flex-col overflow-x-hidden overflow-y-auto bg-card p-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.basic-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.basic-layout__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
background: var(--color-bg-card);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
/* 使用 Tailwind 类,已移除旧样式 */
|
||||
</style>
|
||||
|
||||
@@ -29,10 +29,10 @@ const handleBack = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card-layout">
|
||||
<LayoutHeader
|
||||
:title="title"
|
||||
:show-back="showBack"
|
||||
<div class="flex flex-col h-full overflow-hidden rounded-xl border bg-card">
|
||||
<LayoutHeader
|
||||
:title="title"
|
||||
:show-back="showBack"
|
||||
@back="handleBack"
|
||||
>
|
||||
<template #extra>
|
||||
@@ -41,13 +41,19 @@ const handleBack = () => {
|
||||
</LayoutHeader>
|
||||
|
||||
<!-- 卡片内容 -->
|
||||
<div class="card-layout__card" :class="{ 'no-padding': !showPadding }">
|
||||
<div v-if="!$slots.title && title" class="card-header">
|
||||
<div class="flex-1 flex flex-col overflow-hidden bg-card">
|
||||
<div
|
||||
v-if="!$slots.title && title"
|
||||
class="px-6 py-4 bg-muted/50 border-b text-sm font-medium text-muted-foreground uppercase tracking-wider"
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
<slot v-else name="title"></slot>
|
||||
|
||||
<div class="card-layout__content">
|
||||
<div
|
||||
class="flex-1 overflow-auto bg-card"
|
||||
:class="{ 'p-0': !showPadding, 'p-4': showPadding }"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,45 +61,5 @@ const handleBack = () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-layout__card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-card);
|
||||
|
||||
&.no-padding {
|
||||
.card-layout__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: var(--space-4) var(--space-6);
|
||||
background: var(--color-gray-50);
|
||||
border-bottom: 1px solid var(--color-gray-200);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-600);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.card-layout__content {
|
||||
flex: 1;
|
||||
padding: var(--space-4);
|
||||
overflow: auto;
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
/* 使用 Tailwind 类,已移除旧样式 */
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import LayoutHeader from './LayoutHeader.vue'
|
||||
|
||||
defineOptions({ name: 'FormLayout' })
|
||||
@@ -39,26 +40,14 @@ const props = defineProps({
|
||||
// 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">
|
||||
<LayoutHeader
|
||||
:title="title"
|
||||
:show-back="showBack"
|
||||
@back="handleBack"
|
||||
<div class="flex flex-col h-full overflow-hidden rounded-xl border bg-card">
|
||||
<LayoutHeader
|
||||
:title="title"
|
||||
:show-back="showBack"
|
||||
@back="emit('back')"
|
||||
>
|
||||
<template #extra>
|
||||
<slot name="extra"></slot>
|
||||
@@ -66,32 +55,31 @@ const handleBack = () => {
|
||||
</LayoutHeader>
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<div class="form-layout__content">
|
||||
<div class="form-container">
|
||||
<div class="flex-1 overflow-auto bg-muted/50 flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-[600px] rounded-xl border bg-card p-10">
|
||||
<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 v-if="showFooter" class="shrink-0 border-t px-6 py-4 bg-card">
|
||||
<div class="max-w-[600px] mx-auto flex justify-end gap-3">
|
||||
<Button v-if="showCancel" variant="outline" @click="emit('cancel')">
|
||||
{{ cancelText }}
|
||||
</Button>
|
||||
<Button :disabled="submitLoading" @click="emit('submit')">
|
||||
<Icon v-if="submitLoading" icon="lucide:loader-2" class="mr-2 h-4 w-4 animate-spin" />
|
||||
{{ submitText }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* 已移除旧样式,使用 Tailwind 类 */
|
||||
</style>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-layout {
|
||||
height: 100%;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
import LayoutHeader from './LayoutHeader.vue'
|
||||
|
||||
defineOptions({ name: 'FullWidthLayout' })
|
||||
@@ -34,27 +33,27 @@ const handleBack = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="full-width-layout">
|
||||
<div v-if="$slots.header || showBack" class="full-width-layout__header-wrapper">
|
||||
<LayoutHeader
|
||||
:show-back="showBack"
|
||||
<div class="flex flex-col h-full overflow-hidden bg-card">
|
||||
<div v-if="$slots.header || showBack" class="shrink-0">
|
||||
<LayoutHeader
|
||||
:show-back="showBack"
|
||||
:ghost="headerGhost"
|
||||
:padding="headerPadding"
|
||||
@back="handleBack"
|
||||
>
|
||||
<template #header v-if="$slots.header">
|
||||
<slot name="header"></slot>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<template #extra>
|
||||
<slot name="extra"></slot>
|
||||
</template>
|
||||
</LayoutHeader>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 全宽内容 -->
|
||||
<div
|
||||
class="full-width-layout__content"
|
||||
:class="{ 'no-padding': !showPadding }"
|
||||
class="flex-1 overflow-auto bg-background"
|
||||
:class="{ 'p-0': !showPadding, 'p-4': showPadding }"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
@@ -62,25 +61,5 @@ const handleBack = () => {
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.full-width-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.full-width-layout__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.full-width-layout__content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--color-bg);
|
||||
|
||||
&.no-padding {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
/* 使用 Tailwind 类,已移除旧样式 */
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
defineOptions({ name: 'LayoutHeader' })
|
||||
|
||||
@@ -30,98 +31,36 @@ const handleBack = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="layout-header"
|
||||
:class="{ 'layout-header--ghost': ghost }"
|
||||
<div
|
||||
class="flex justify-between items-center gap-4 border-b bg-card shrink-0 min-h-14 transition-all"
|
||||
:class="{
|
||||
'border-border': !ghost,
|
||||
'border-transparent bg-transparent': ghost,
|
||||
'px-4 py-3': !padding,
|
||||
'dark:border-border dark:bg-card dark:text-foreground': !ghost
|
||||
}"
|
||||
:style="padding ? { padding } : {}"
|
||||
>
|
||||
<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">
|
||||
<div class="flex items-center gap-4 flex-1 min-w-0">
|
||||
<Button
|
||||
v-if="showBack"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
@click="handleBack"
|
||||
>
|
||||
<Icon icon="lucide:arrow-left" class="w-4 h-4" />
|
||||
</Button>
|
||||
<div class="flex-1 min-w-0 flex items-center">
|
||||
<slot name="header">
|
||||
<h1 v-if="title" class="header-title">{{ title }}</h1>
|
||||
<h1 v-if="title" class="text-lg font-semibold text-foreground m-0 leading-snug tracking-tight">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.layout-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4) var(--space-6);
|
||||
border-bottom: 1px solid var(--color-gray-200);
|
||||
background: var(--color-bg-card);
|
||||
flex-shrink: 0;
|
||||
min-height: 56px;
|
||||
transition: all var(--duration-base);
|
||||
|
||||
&--ghost {
|
||||
background: transparent;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: var(--space-1);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-gray-600);
|
||||
border-radius: var(--radius-base);
|
||||
cursor: pointer;
|
||||
transition: all var(--duration-fast);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-900);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import LayoutHeader from './LayoutHeader.vue'
|
||||
|
||||
defineOptions({ name: 'TabLayout' })
|
||||
@@ -48,14 +49,14 @@ const handleBack = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tab-layout">
|
||||
<div v-if="$slots.header || showBack" class="tab-layout__header-wrapper">
|
||||
<LayoutHeader
|
||||
:show-back="showBack"
|
||||
<div class="flex flex-col h-full overflow-hidden rounded-xl border bg-card">
|
||||
<div v-if="$slots.header || showBack" class="shrink-0">
|
||||
<LayoutHeader
|
||||
:show-back="showBack"
|
||||
@back="handleBack"
|
||||
>
|
||||
<template #header v-if="$slots.header">
|
||||
<slot name="header"></slot>
|
||||
<slot name="header"></slot>
|
||||
</template>
|
||||
<template #extra>
|
||||
<slot name="extra"></slot>
|
||||
@@ -64,75 +65,26 @@ const handleBack = () => {
|
||||
</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>
|
||||
<Tabs v-model="activeTabKey" class="w-full" @update:modelValue="handleTabChange">
|
||||
<TabsList class="w-full justify-start border-b bg-muted/50 px-4">
|
||||
<TabsTrigger
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:value="tab.key"
|
||||
class="data-[state=active]:bg-card"
|
||||
>
|
||||
{{ tab.tab }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<div class="tab-content">
|
||||
<slot :name="activeTabKey"></slot>
|
||||
</div>
|
||||
<!-- 标签页内容 -->
|
||||
<TabsContent :value="activeTabKey" class="flex-1 overflow-auto p-4">
|
||||
<slot :name="activeTabKey"></slot>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.tab-layout {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-layout__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--color-gray-200);
|
||||
background: var(--color-gray-50);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: var(--space-2) var(--space-6);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-600);
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all var(--duration-fast);
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-gray-100);
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-primary-500);
|
||||
border-bottom-color: var(--color-primary-500);
|
||||
background: var(--color-bg-card);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: var(--color-bg-card);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
/* 使用 Tailwind + shadcn 组件,已移除旧样式 */
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user