feat: 优化
This commit is contained in:
@@ -1,16 +1,6 @@
|
||||
---
|
||||
name: antd-to-shadcn
|
||||
description: |
|
||||
Vue 3 项目从 Ant Design Vue 迁移到 shadcn-vue 的专业技能。
|
||||
|
||||
当用户请求以下操作时使用此技能:
|
||||
- 将 Ant Design 组件替换为 shadcn 组件
|
||||
- 迁移表单、表格、弹窗等复杂组件
|
||||
- 保持业务功能不变的前提下现代化 UI
|
||||
- 颜色解耦和主题配置
|
||||
- 组件间距和布局优化
|
||||
|
||||
触发词:迁移、shadcn、antd 替换、组件升级、UI 现代化
|
||||
description: Vue 3 项目从 Ant Design Vue 迁移到 shadcn-vue 的专业技能。支持组件替换、表单表格弹窗迁移、UI 现代化、颜色解耦和主题配置。触发词:迁移、shadcn、antd 替换、组件升级、UI 现代化
|
||||
---
|
||||
|
||||
# Ant Design Vue → shadcn-vue 迁移技能
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
import SvgSprite from '@/components/icons/SvgSprite.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import tokenManager from '@gold/utils/token-manager'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
|
||||
// ========================================
|
||||
// Ant Design Vue 主题配置
|
||||
// 主题配置 - 使用 CSS 变量,无需 Ant Design
|
||||
// ========================================
|
||||
const isDark = ref(false)
|
||||
|
||||
@@ -19,129 +18,49 @@ const initTheme = () => {
|
||||
} else {
|
||||
isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
document.documentElement.classList.toggle('dark', isDark.value)
|
||||
document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
// 主题切换
|
||||
const toggleTheme = () => {
|
||||
isDark.value = !isDark.value
|
||||
document.documentElement.classList.toggle('dark', isDark.value)
|
||||
document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : 'light')
|
||||
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
// Ant Design 主题 Token
|
||||
const themeToken = computed(() => {
|
||||
const lightToken = {
|
||||
// 品牌色
|
||||
colorPrimary: '#3B82F6',
|
||||
colorSuccess: '#22C55E',
|
||||
colorWarning: '#F59E0B',
|
||||
colorError: '#EF4444',
|
||||
colorInfo: '#3B82F6',
|
||||
// 背景色
|
||||
colorBgContainer: '#FFFFFF',
|
||||
colorBgElevated: '#FFFFFF',
|
||||
colorBgLayout: '#F9FAFB',
|
||||
// 边框
|
||||
colorBorder: '#E5E7EB',
|
||||
colorBorderSecondary: '#F3F4F6',
|
||||
// 文字
|
||||
colorText: '#111827',
|
||||
colorTextSecondary: '#4B5563',
|
||||
colorTextTertiary: '#6B7280',
|
||||
colorTextQuaternary: '#9CA3AF',
|
||||
// 填充
|
||||
colorFill: '#F3F4F6',
|
||||
colorFillSecondary: '#F9FAFB',
|
||||
// 字体
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif',
|
||||
fontSize: 14,
|
||||
// 圆角
|
||||
borderRadius: 6,
|
||||
borderRadiusSM: 4,
|
||||
borderRadiusLG: 8,
|
||||
// 控件尺寸
|
||||
controlHeight: 32,
|
||||
controlHeightSM: 28,
|
||||
controlHeightLG: 40,
|
||||
// 动画
|
||||
motionDurationFast: '0.15s',
|
||||
motionDurationMid: '0.2s',
|
||||
motionDurationSlow: '0.3s',
|
||||
// 阴影
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
|
||||
boxShadowSecondary: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
||||
}
|
||||
|
||||
const darkToken = {
|
||||
colorPrimary: '#60A5FA',
|
||||
colorBgContainer: '#1E293B',
|
||||
colorBgElevated: '#334155',
|
||||
colorBgLayout: '#0F172A',
|
||||
colorBorder: '#334155',
|
||||
colorBorderSecondary: '#1E293B',
|
||||
colorText: '#F1F5F9',
|
||||
colorTextSecondary: '#94A3B8',
|
||||
colorTextTertiary: '#64748B',
|
||||
colorTextQuaternary: '#475569',
|
||||
colorFill: '#334155',
|
||||
colorFillSecondary: '#1E293B',
|
||||
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px -1px rgba(0, 0, 0, 0.4)',
|
||||
boxShadowSecondary: '0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.4)',
|
||||
}
|
||||
|
||||
return {
|
||||
token: isDark.value ? { ...lightToken, ...darkToken } : lightToken,
|
||||
components: {
|
||||
Button: {
|
||||
primaryShadow: 'none',
|
||||
defaultShadow: 'none',
|
||||
fontWeight: 500,
|
||||
},
|
||||
Input: {
|
||||
paddingBlock: 6,
|
||||
paddingInline: 12,
|
||||
activeShadow: '0 0 0 3px rgba(59, 130, 246, 0.15)',
|
||||
},
|
||||
Select: {
|
||||
optionSelectedBg: isDark.value ? 'rgba(96, 165, 250, 0.15)' : '#EFF6FF',
|
||||
optionActiveBg: isDark.value ? '#334155' : '#F3F4F6',
|
||||
},
|
||||
Table: {
|
||||
headerBg: isDark.value ? '#1E293B' : '#F9FAFB',
|
||||
rowHoverBg: isDark.value ? '#334155' : '#F9FAFB',
|
||||
borderColor: isDark.value ? '#334155' : '#E5E7EB',
|
||||
headerColor: '#4B5563',
|
||||
},
|
||||
Card: {
|
||||
paddingLG: 20,
|
||||
borderRadiusLG: 12,
|
||||
},
|
||||
Modal: {
|
||||
borderRadiusLG: 12,
|
||||
},
|
||||
Menu: {
|
||||
itemHoverBg: isDark.value ? '#334155' : '#F3F4F6',
|
||||
itemSelectedBg: isDark.value ? 'rgba(96, 165, 250, 0.15)' : '#EFF6FF',
|
||||
itemSelectedColor: isDark.value ? '#60A5FA' : '#3B82F6',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
initTheme()
|
||||
|
||||
// 监听系统主题变化
|
||||
watchEffect(() => {
|
||||
watchEffect((onCleanup) => {
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const handler = (e) => {
|
||||
if (!localStorage.getItem('theme')) {
|
||||
isDark.value = e.matches
|
||||
document.documentElement.classList.toggle('dark', e.matches)
|
||||
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light')
|
||||
}
|
||||
}
|
||||
media.addEventListener('change', handler)
|
||||
onCleanup(() => media.removeEventListener('change', handler))
|
||||
})
|
||||
|
||||
// 用户状态
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 自动刷新 token
|
||||
watchEffect((onCleanup) => {
|
||||
let interval
|
||||
if (userStore.isLoggedIn) {
|
||||
interval = setInterval(() => {
|
||||
tokenManager.refreshToken()
|
||||
}, 5 * 60 * 1000)
|
||||
}
|
||||
onCleanup(() => {
|
||||
if (interval) clearInterval(interval)
|
||||
})
|
||||
})
|
||||
|
||||
// 暴露给模板使用
|
||||
@@ -149,54 +68,14 @@ defineExpose({ toggleTheme, isDark })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-config-provider :theme="themeToken" :locale="zhCN">
|
||||
<div class="app-container min-h-screen bg-background text-foreground antialiased">
|
||||
<SvgSprite />
|
||||
<keep-alive>
|
||||
<RouterView />
|
||||
<RouterView />
|
||||
</keep-alive>
|
||||
</a-config-provider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 全局样式保持不变 */
|
||||
.ant-btn{
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.ant-select-selection-item,
|
||||
.ant-select-selection-placeholder {
|
||||
line-height: 30px !important;
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
.ant-modal-confirm-btns {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
.ant-btn {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
.ant-select-focused .ant-select-selector {
|
||||
border-color: var(--color-primary) !important;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.ant-select:hover .ant-select-selector {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--color-text-secondary) !important;
|
||||
}
|
||||
.ant-modal .ant-modal-footer {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
.ant-tooltip{
|
||||
z-index: 100
|
||||
}
|
||||
/* 全局基础样式已由 Tailwind + theme.css 处理 */
|
||||
</style>
|
||||
|
||||
@@ -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