feat: 添加 Tabs 和 TabPane 组件,支持动态标签页管理
This commit is contained in:
58
components.d.ts
vendored
58
components.d.ts
vendored
@@ -12,46 +12,40 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
Avatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
||||||
|
Collapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
||||||
|
Datetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
||||||
|
Default: typeof import('./src/components/layout/default.vue')['default']
|
||||||
|
Divider: typeof import('./src/components/ui/divider/index.vue')['default']
|
||||||
IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
|
IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
|
||||||
IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
|
IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
|
||||||
IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
||||||
|
Input: typeof import('./src/components/ui/input/index.vue')['default']
|
||||||
|
InputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
||||||
IonApp: typeof import('@ionic/vue')['IonApp']
|
IonApp: typeof import('@ionic/vue')['IonApp']
|
||||||
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||||
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
|
||||||
IonBreadcrumb: typeof import('@ionic/vue')['IonBreadcrumb']
|
|
||||||
IonBreadcrumbs: typeof import('@ionic/vue')['IonBreadcrumbs']
|
|
||||||
IonButton: typeof import('@ionic/vue')['IonButton']
|
IonButton: typeof import('@ionic/vue')['IonButton']
|
||||||
IonButtons: typeof import('@ionic/vue')['IonButtons']
|
|
||||||
IonContent: typeof import('@ionic/vue')['IonContent']
|
IonContent: typeof import('@ionic/vue')['IonContent']
|
||||||
IonDatetime: typeof import('@ionic/vue')['IonDatetime']
|
|
||||||
IonDatetimeButton: typeof import('@ionic/vue')['IonDatetimeButton']
|
|
||||||
IonHeader: typeof import('@ionic/vue')['IonHeader']
|
IonHeader: typeof import('@ionic/vue')['IonHeader']
|
||||||
IonIcon: typeof import('@ionic/vue')['IonIcon']
|
IonIcon: typeof import('@ionic/vue')['IonIcon']
|
||||||
IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
|
|
||||||
IonItem: typeof import('@ionic/vue')['IonItem']
|
IonItem: typeof import('@ionic/vue')['IonItem']
|
||||||
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
IonList: typeof import('@ionic/vue')['IonList']
|
IonList: typeof import('@ionic/vue')['IonList']
|
||||||
IonModal: typeof import('@ionic/vue')['IonModal']
|
IonModal: typeof import('@ionic/vue')['IonModal']
|
||||||
IonNote: typeof import('@ionic/vue')['IonNote']
|
|
||||||
IonPage: typeof import('@ionic/vue')['IonPage']
|
IonPage: typeof import('@ionic/vue')['IonPage']
|
||||||
IonRadio: typeof import('@ionic/vue')['IonRadio']
|
|
||||||
IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
|
|
||||||
IonRippleEffect: typeof import('@ionic/vue')['IonRippleEffect']
|
|
||||||
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
||||||
IonSearchbar: typeof import('@ionic/vue')['IonSearchbar']
|
IonSearchbar: typeof import('@ionic/vue')['IonSearchbar']
|
||||||
IonSegment: typeof import('@ionic/vue')['IonSegment']
|
|
||||||
IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
|
|
||||||
IonSelect: typeof import('@ionic/vue')['IonSelect']
|
|
||||||
IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
|
|
||||||
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||||
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||||
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
IonText: typeof import('@ionic/vue')['IonText']
|
|
||||||
IonTitle: typeof import('@ionic/vue')['IonTitle']
|
IonTitle: typeof import('@ionic/vue')['IonTitle']
|
||||||
IonToolbar: typeof import('@ionic/vue')['IonToolbar']
|
IonToolbar: typeof import('@ionic/vue')['IonToolbar']
|
||||||
LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
||||||
|
Result: typeof import('./src/components/ui/result/index.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
TabPane: typeof import('./src/components/ui/tabs/tab-pane.vue')['default']
|
||||||
|
Tabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
||||||
UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
||||||
UiCollapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
UiCollapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
||||||
UiDatetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
UiDatetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
||||||
@@ -59,51 +53,48 @@ declare module 'vue' {
|
|||||||
UiInput: typeof import('./src/components/ui/input/index.vue')['default']
|
UiInput: typeof import('./src/components/ui/input/index.vue')['default']
|
||||||
UiInputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
UiInputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
||||||
UiResult: typeof import('./src/components/ui/result/index.vue')['default']
|
UiResult: typeof import('./src/components/ui/result/index.vue')['default']
|
||||||
|
UiTabPane: typeof import('./src/components/ui/tab-pane/index.vue')['default']
|
||||||
|
UiTabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
||||||
|
UiTabsTabPane: typeof import('./src/components/ui/tabs/tab-pane.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For TSX support
|
// For TSX support
|
||||||
declare global {
|
declare global {
|
||||||
|
const Avatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
||||||
|
const Collapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
||||||
|
const Datetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
||||||
|
const Default: typeof import('./src/components/layout/default.vue')['default']
|
||||||
|
const Divider: typeof import('./src/components/ui/divider/index.vue')['default']
|
||||||
const IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
|
const IIcBaselineDataSaverOff: typeof import('~icons/ic/baseline-data-saver-off')['default']
|
||||||
const IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
|
const IIcBaselineDownloading: typeof import('~icons/ic/baseline-downloading')['default']
|
||||||
const IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
const IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
||||||
|
const Input: typeof import('./src/components/ui/input/index.vue')['default']
|
||||||
|
const InputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
||||||
const IonApp: typeof import('@ionic/vue')['IonApp']
|
const IonApp: typeof import('@ionic/vue')['IonApp']
|
||||||
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||||
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
|
||||||
const IonBreadcrumb: typeof import('@ionic/vue')['IonBreadcrumb']
|
|
||||||
const IonBreadcrumbs: typeof import('@ionic/vue')['IonBreadcrumbs']
|
|
||||||
const IonButton: typeof import('@ionic/vue')['IonButton']
|
const IonButton: typeof import('@ionic/vue')['IonButton']
|
||||||
const IonButtons: typeof import('@ionic/vue')['IonButtons']
|
|
||||||
const IonContent: typeof import('@ionic/vue')['IonContent']
|
const IonContent: typeof import('@ionic/vue')['IonContent']
|
||||||
const IonDatetime: typeof import('@ionic/vue')['IonDatetime']
|
|
||||||
const IonDatetimeButton: typeof import('@ionic/vue')['IonDatetimeButton']
|
|
||||||
const IonHeader: typeof import('@ionic/vue')['IonHeader']
|
const IonHeader: typeof import('@ionic/vue')['IonHeader']
|
||||||
const IonIcon: typeof import('@ionic/vue')['IonIcon']
|
const IonIcon: typeof import('@ionic/vue')['IonIcon']
|
||||||
const IonInputOtp: typeof import('@ionic/vue')['IonInputOtp']
|
|
||||||
const IonItem: typeof import('@ionic/vue')['IonItem']
|
const IonItem: typeof import('@ionic/vue')['IonItem']
|
||||||
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
const IonLabel: typeof import('@ionic/vue')['IonLabel']
|
||||||
const IonList: typeof import('@ionic/vue')['IonList']
|
const IonList: typeof import('@ionic/vue')['IonList']
|
||||||
const IonModal: typeof import('@ionic/vue')['IonModal']
|
const IonModal: typeof import('@ionic/vue')['IonModal']
|
||||||
const IonNote: typeof import('@ionic/vue')['IonNote']
|
|
||||||
const IonPage: typeof import('@ionic/vue')['IonPage']
|
const IonPage: typeof import('@ionic/vue')['IonPage']
|
||||||
const IonRadio: typeof import('@ionic/vue')['IonRadio']
|
|
||||||
const IonRadioGroup: typeof import('@ionic/vue')['IonRadioGroup']
|
|
||||||
const IonRippleEffect: typeof import('@ionic/vue')['IonRippleEffect']
|
|
||||||
const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
const IonRouterOutlet: typeof import('@ionic/vue')['IonRouterOutlet']
|
||||||
const IonSearchbar: typeof import('@ionic/vue')['IonSearchbar']
|
const IonSearchbar: typeof import('@ionic/vue')['IonSearchbar']
|
||||||
const IonSegment: typeof import('@ionic/vue')['IonSegment']
|
|
||||||
const IonSegmentButton: typeof import('@ionic/vue')['IonSegmentButton']
|
|
||||||
const IonSelect: typeof import('@ionic/vue')['IonSelect']
|
|
||||||
const IonSelectOption: typeof import('@ionic/vue')['IonSelectOption']
|
|
||||||
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
const IonTabBar: typeof import('@ionic/vue')['IonTabBar']
|
||||||
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
const IonTabButton: typeof import('@ionic/vue')['IonTabButton']
|
||||||
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
const IonTabs: typeof import('@ionic/vue')['IonTabs']
|
||||||
const IonText: typeof import('@ionic/vue')['IonText']
|
|
||||||
const IonTitle: typeof import('@ionic/vue')['IonTitle']
|
const IonTitle: typeof import('@ionic/vue')['IonTitle']
|
||||||
const IonToolbar: typeof import('@ionic/vue')['IonToolbar']
|
const IonToolbar: typeof import('@ionic/vue')['IonToolbar']
|
||||||
const LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
const LayoutDefault: typeof import('./src/components/layout/default.vue')['default']
|
||||||
|
const Result: typeof import('./src/components/ui/result/index.vue')['default']
|
||||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
const RouterView: typeof import('vue-router')['RouterView']
|
const RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
const TabPane: typeof import('./src/components/ui/tabs/tab-pane.vue')['default']
|
||||||
|
const Tabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
||||||
const UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
const UiAvatar: typeof import('./src/components/ui/avatar/index.vue')['default']
|
||||||
const UiCollapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
const UiCollapse: typeof import('./src/components/ui/collapse/index.vue')['default']
|
||||||
const UiDatetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
const UiDatetime: typeof import('./src/components/ui/datetime/index.vue')['default']
|
||||||
@@ -111,4 +102,7 @@ declare global {
|
|||||||
const UiInput: typeof import('./src/components/ui/input/index.vue')['default']
|
const UiInput: typeof import('./src/components/ui/input/index.vue')['default']
|
||||||
const UiInputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
const UiInputLabel: typeof import('./src/components/ui/input-label/index.vue')['default']
|
||||||
const UiResult: typeof import('./src/components/ui/result/index.vue')['default']
|
const UiResult: typeof import('./src/components/ui/result/index.vue')['default']
|
||||||
|
const UiTabPane: typeof import('./src/components/ui/tab-pane/index.vue')['default']
|
||||||
|
const UiTabs: typeof import('./src/components/ui/tabs/index.vue')['default']
|
||||||
|
const UiTabsTabPane: typeof import('./src/components/ui/tabs/tab-pane.vue')['default']
|
||||||
}
|
}
|
||||||
84
src/components/ui/tab-pane/index.vue
Normal file
84
src/components/ui/tab-pane/index.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang='ts' setup>
|
||||||
|
interface TabPaneProps {
|
||||||
|
name: string | number;
|
||||||
|
title: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
lazy?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<TabPaneProps>();
|
||||||
|
|
||||||
|
// 注入父组件上下文
|
||||||
|
const tabsContext = inject<any>("tabs-context");
|
||||||
|
if (!tabsContext) {
|
||||||
|
throw new Error("TabPane must be used inside Tabs");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前是否为活跃状态
|
||||||
|
const isActive = computed(() => tabsContext.currentTab.value === props.name);
|
||||||
|
|
||||||
|
// 是否已经渲染过(用于lazy加载)
|
||||||
|
const hasRendered = ref(false);
|
||||||
|
|
||||||
|
// 监听active状态变化
|
||||||
|
watch(isActive, (active) => {
|
||||||
|
if (active && !hasRendered.value) {
|
||||||
|
hasRendered.value = true;
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 注册到父组件
|
||||||
|
onMounted(() => {
|
||||||
|
tabsContext.registerTabPane({
|
||||||
|
name: props.name,
|
||||||
|
title: props.title,
|
||||||
|
disabled: props.disabled,
|
||||||
|
closable: props.closable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 卸载时注销
|
||||||
|
onUnmounted(() => {
|
||||||
|
tabsContext.unregisterTabPane({
|
||||||
|
name: props.name,
|
||||||
|
title: props.title,
|
||||||
|
disabled: props.disabled,
|
||||||
|
closable: props.closable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算pane样式类
|
||||||
|
const paneClasses = computed(() => [
|
||||||
|
"ui-tab-pane",
|
||||||
|
{
|
||||||
|
"ui-tab-pane--active": isActive.value,
|
||||||
|
"ui-tab-pane--animated": tabsContext.animated.value,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<div
|
||||||
|
v-if="!lazy || hasRendered"
|
||||||
|
v-show="isActive"
|
||||||
|
:class="paneClasses"
|
||||||
|
:style="tabsContext.paneStyle.value"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
|
|
||||||
|
/* Tab Panes */
|
||||||
|
.ui-tab-pane {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab-pane--animated {
|
||||||
|
@apply transition-opacity duration-200;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
602
src/components/ui/tabs/index.vue
Normal file
602
src/components/ui/tabs/index.vue
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
<script lang='ts' setup>
|
||||||
|
interface TabsProps {
|
||||||
|
modelValue?: string | number;
|
||||||
|
type?: "bar" | "line" | "segment";
|
||||||
|
size?: "small" | "medium" | "large";
|
||||||
|
animated?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
addable?: boolean;
|
||||||
|
placement?: "top" | "bottom" | "left" | "right";
|
||||||
|
tabStyle?: string | Record<string, any>;
|
||||||
|
tabClass?: string;
|
||||||
|
paneStyle?: string | Record<string, any>;
|
||||||
|
paneClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabsEmits {
|
||||||
|
(e: "update:modelValue", value: string | number): void;
|
||||||
|
(e: "tabClick", key: string | number, event: Event): void;
|
||||||
|
(e: "tabClose", key: string | number): void;
|
||||||
|
(e: "tabAdd"): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<TabsProps>(), {
|
||||||
|
type: "bar",
|
||||||
|
size: "medium",
|
||||||
|
animated: true,
|
||||||
|
closable: false,
|
||||||
|
addable: false,
|
||||||
|
placement: "top",
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<TabsEmits>();
|
||||||
|
|
||||||
|
// 内部状态
|
||||||
|
const currentTab = ref<string | number>();
|
||||||
|
const tabsRef = ref<HTMLElement>();
|
||||||
|
const activeBarRef = ref<HTMLElement>();
|
||||||
|
|
||||||
|
// 子组件列表
|
||||||
|
const tabPanes = ref<Array<{
|
||||||
|
name: string | number;
|
||||||
|
title: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
instance: any;
|
||||||
|
}>>([]);
|
||||||
|
|
||||||
|
// 使用provide/inject在父子组件间传递数据
|
||||||
|
const tabsContext = {
|
||||||
|
currentTab: readonly(currentTab),
|
||||||
|
type: toRef(props, "type"),
|
||||||
|
size: toRef(props, "size"),
|
||||||
|
closable: toRef(props, "closable"),
|
||||||
|
animated: toRef(props, "animated"),
|
||||||
|
placement: toRef(props, "placement"),
|
||||||
|
tabStyle: toRef(props, "tabStyle"),
|
||||||
|
tabClass: toRef(props, "tabClass"),
|
||||||
|
paneStyle: toRef(props, "paneStyle"),
|
||||||
|
paneClass: toRef(props, "paneClass"),
|
||||||
|
activateTab,
|
||||||
|
closeTab,
|
||||||
|
registerTabPane,
|
||||||
|
unregisterTabPane,
|
||||||
|
};
|
||||||
|
|
||||||
|
provide("tabs-context", tabsContext);
|
||||||
|
|
||||||
|
// 注册tab pane
|
||||||
|
function registerTabPane(pane: any) {
|
||||||
|
const existingIndex = tabPanes.value.findIndex(p => p.name === pane.name);
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
tabPanes.value.push(pane);
|
||||||
|
// 如果当前没有激活的tab且这是第一个可用的tab,则激活它
|
||||||
|
if (!currentTab.value && !pane.disabled) {
|
||||||
|
currentTab.value = pane.name;
|
||||||
|
emit("update:modelValue", pane.name);
|
||||||
|
// 延迟更新active bar,确保DOM完全渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
updateActiveBar();
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterTabPane(pane: any) {
|
||||||
|
const index = tabPanes.value.findIndex(p => p.name === pane.name);
|
||||||
|
if (index > -1) {
|
||||||
|
tabPanes.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激活tab
|
||||||
|
function activateTab(key: string | number, event?: Event) {
|
||||||
|
const pane = tabPanes.value.find(p => p.name === key);
|
||||||
|
if (!pane || pane.disabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (currentTab.value === key)
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentTab.value = key;
|
||||||
|
emit("update:modelValue", key);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
emit("tabClick", key, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新active bar位置
|
||||||
|
nextTick(updateActiveBar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭tab
|
||||||
|
function closeTab(key: string | number) {
|
||||||
|
emit("tabClose", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加tab
|
||||||
|
function addTab() {
|
||||||
|
emit("tabAdd");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新active bar位置(用于bar和line类型)
|
||||||
|
function updateActiveBar() {
|
||||||
|
if (props.type !== "bar" && props.type !== "line")
|
||||||
|
return;
|
||||||
|
if (!tabsRef.value || !activeBarRef.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const activeTabEl = tabsRef.value.querySelector(`[data-tab-key="${currentTab.value}"]`) as HTMLElement;
|
||||||
|
if (!activeTabEl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 获取tab label元素来计算更精确的宽度
|
||||||
|
const labelEl = activeTabEl.querySelector(".ui-tab__label") as HTMLElement;
|
||||||
|
|
||||||
|
if (labelEl) {
|
||||||
|
// 创建一个临时元素来测量文本的实际宽度
|
||||||
|
const tempEl = document.createElement("span");
|
||||||
|
tempEl.style.visibility = "hidden";
|
||||||
|
tempEl.style.position = "absolute";
|
||||||
|
tempEl.style.fontSize = window.getComputedStyle(labelEl).fontSize;
|
||||||
|
tempEl.style.fontFamily = window.getComputedStyle(labelEl).fontFamily;
|
||||||
|
tempEl.style.fontWeight = window.getComputedStyle(labelEl).fontWeight;
|
||||||
|
tempEl.textContent = labelEl.textContent;
|
||||||
|
document.body.appendChild(tempEl);
|
||||||
|
|
||||||
|
const textWidth = tempEl.offsetWidth;
|
||||||
|
document.body.removeChild(tempEl);
|
||||||
|
|
||||||
|
const tabsRect = tabsRef.value.getBoundingClientRect();
|
||||||
|
const labelRect = labelEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (props.placement === "top" || props.placement === "bottom") {
|
||||||
|
// 居中对齐文本宽度
|
||||||
|
const leftOffset = labelRect.left - tabsRect.left + (labelRect.width - textWidth) / 2;
|
||||||
|
activeBarRef.value.style.left = `${leftOffset}px`;
|
||||||
|
activeBarRef.value.style.width = `${textWidth}px`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
activeBarRef.value.style.top = `${labelRect.top - tabsRect.top}px`;
|
||||||
|
activeBarRef.value.style.height = `${textWidth}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 降级到原来的逻辑
|
||||||
|
const tabsRect = tabsRef.value.getBoundingClientRect();
|
||||||
|
const activeRect = activeTabEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (props.placement === "top" || props.placement === "bottom") {
|
||||||
|
activeBarRef.value.style.left = `${activeRect.left - tabsRect.left}px`;
|
||||||
|
activeBarRef.value.style.width = `${activeRect.width}px`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
activeBarRef.value.style.top = `${activeRect.top - tabsRect.top}px`;
|
||||||
|
activeBarRef.value.style.height = `${activeRect.height}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听modelValue变化
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (newValue !== undefined && newValue !== currentTab.value) {
|
||||||
|
currentTab.value = newValue;
|
||||||
|
nextTick(updateActiveBar);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// 监听tabPanes变化,自动设置第一个tab为活跃状态
|
||||||
|
watch(tabPanes, (newPanes) => {
|
||||||
|
if ((!currentTab.value || currentTab.value === "") && newPanes.length > 0) {
|
||||||
|
const firstAvailablePane = newPanes.find(pane => !pane.disabled);
|
||||||
|
if (firstAvailablePane) {
|
||||||
|
currentTab.value = firstAvailablePane.name;
|
||||||
|
emit("update:modelValue", firstAvailablePane.name);
|
||||||
|
nextTick(updateActiveBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { flush: "post" });
|
||||||
|
|
||||||
|
// 组件挂载后初始化
|
||||||
|
onMounted(() => {
|
||||||
|
// 如果没有当前激活的tab但有可用的tabs,选择第一个
|
||||||
|
if (!currentTab.value && tabPanes.value.length > 0) {
|
||||||
|
const firstAvailablePane = tabPanes.value.find(pane => !pane.disabled);
|
||||||
|
if (firstAvailablePane) {
|
||||||
|
currentTab.value = firstAvailablePane.name;
|
||||||
|
emit("update:modelValue", firstAvailablePane.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟更新active bar,确保DOM完全渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
updateActiveBar();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({
|
||||||
|
registerTabPane,
|
||||||
|
unregisterTabPane,
|
||||||
|
updateActiveBar,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算样式类
|
||||||
|
const tabsClasses = computed(() => [
|
||||||
|
"ui-tabs",
|
||||||
|
`ui-tabs--${props.type}`,
|
||||||
|
`ui-tabs--${props.size}`,
|
||||||
|
`ui-tabs--${props.placement}`,
|
||||||
|
{
|
||||||
|
"ui-tabs--animated": props.animated,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="tabsClasses">
|
||||||
|
<!-- 标签头部 -->
|
||||||
|
<div class="ui-tabs__nav-wrapper" :class="[`ui-tabs__nav-wrapper--${placement}`]">
|
||||||
|
<div
|
||||||
|
ref="tabsRef"
|
||||||
|
class="ui-tabs__nav"
|
||||||
|
:class="[`ui-tabs__nav--${type}`, `ui-tabs__nav--${size}`]"
|
||||||
|
>
|
||||||
|
<!-- 渲染标签列表 -->
|
||||||
|
<div
|
||||||
|
v-for="pane in tabPanes"
|
||||||
|
:key="pane.name"
|
||||||
|
class="ui-tab" :class="[
|
||||||
|
`ui-tab--${type}`,
|
||||||
|
`ui-tab--${size}`,
|
||||||
|
{
|
||||||
|
'ui-tab--active': currentTab === pane.name,
|
||||||
|
'ui-tab--disabled': pane.disabled,
|
||||||
|
'ui-tab--closable': pane.closable || closable,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:data-tab-key="pane.name"
|
||||||
|
:style="tabStyle"
|
||||||
|
@click="activateTab(pane.name, $event)"
|
||||||
|
>
|
||||||
|
<span class="ui-tab__label">
|
||||||
|
{{ pane.title }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Close Button -->
|
||||||
|
<button
|
||||||
|
v-if="(pane.closable || closable) && type !== 'segment'"
|
||||||
|
class="ui-tab__close"
|
||||||
|
@click.stop="closeTab(pane.name)"
|
||||||
|
>
|
||||||
|
<ion-icon name="close-outline" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Bar (用于bar和line类型) -->
|
||||||
|
<div
|
||||||
|
v-if="type === 'bar' || type === 'line'"
|
||||||
|
ref="activeBarRef"
|
||||||
|
class="ui-tabs__active-bar"
|
||||||
|
:class="`ui-tabs__active-bar--${type}`"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Add Button -->
|
||||||
|
<button
|
||||||
|
v-if="addable"
|
||||||
|
class="ui-tabs__add-btn"
|
||||||
|
@click="addTab"
|
||||||
|
>
|
||||||
|
<ion-icon name="add-outline" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签内容区域 -->
|
||||||
|
<div class="ui-tabs__content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "tailwindcss";
|
||||||
|
|
||||||
|
.ui-tabs {
|
||||||
|
@apply w-full;
|
||||||
|
--ui-tabs-primary: var(--ion-color-primary, #007aff);
|
||||||
|
--ui-tabs-primary-rgb: var(--ion-color-primary-rgb, 0, 122, 255);
|
||||||
|
--ui-tabs-background: var(--ion-background-color, #ffffff);
|
||||||
|
--ui-tabs-text: var(--ion-text-color, #000000);
|
||||||
|
--ui-tabs-text-rgb: var(--ion-text-color-rgb, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航包装器 */
|
||||||
|
.ui-tabs__nav-wrapper {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav-wrapper--top {
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav-wrapper--bottom {
|
||||||
|
border-top: 1px solid var(--ion-border-color, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav-wrapper--left {
|
||||||
|
border-right: 1px solid var(--ion-border-color, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav-wrapper--right {
|
||||||
|
border-left: 1px solid var(--ion-border-color, #e5e7eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航容器 */
|
||||||
|
.ui-tabs__nav {
|
||||||
|
@apply relative flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav--bar,
|
||||||
|
.ui-tabs__nav--line {
|
||||||
|
@apply gap-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav--segment {
|
||||||
|
@apply gap-1 p-1 rounded-lg;
|
||||||
|
background-color: var(--ion-color-light, #f8f9fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 尺寸 */
|
||||||
|
.ui-tabs__nav--small {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav--medium {
|
||||||
|
@apply text-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav--large {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Labels */
|
||||||
|
.ui-tab {
|
||||||
|
@apply relative flex items-center justify-between cursor-pointer transition-all duration-200;
|
||||||
|
color: var(--ion-color-medium, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab:hover {
|
||||||
|
color: var(--ion-color-dark, #374151);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--active {
|
||||||
|
@apply font-medium;
|
||||||
|
color: var(--ui-tabs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--disabled {
|
||||||
|
@apply cursor-not-allowed opacity-50;
|
||||||
|
color: var(--ion-color-light-shade, #d1d5db) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bar 类型 */
|
||||||
|
.ui-tab--bar {
|
||||||
|
@apply py-3 border-b-2 border-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--bar.ui-tab--active {
|
||||||
|
border-color: var(--ui-tabs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--bar:hover:not(.ui-tab--disabled):not(.ui-tab--active) {
|
||||||
|
background-color: rgba(var(--ui-tabs-primary-rgb), 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Line 类型 */
|
||||||
|
.ui-tab--line {
|
||||||
|
@apply py-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--line:hover:not(.ui-tab--disabled):not(.ui-tab--active) {
|
||||||
|
background-color: rgba(var(--ui-tabs-primary-rgb), 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Segment 类型 */
|
||||||
|
.ui-tab--segment {
|
||||||
|
@apply px-3 py-2 rounded-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--segment.ui-tab--active {
|
||||||
|
background-color: var(--ui-tabs-background);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||||
|
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--segment:hover:not(.ui-tab--disabled):not(.ui-tab--active) {
|
||||||
|
background-color: rgba(var(--ui-tabs-primary-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 尺寸变化 */
|
||||||
|
.ui-tab--small {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--small.ui-tab--bar,
|
||||||
|
.ui-tab--small.ui-tab--line {
|
||||||
|
@apply py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--small.ui-tab--segment {
|
||||||
|
@apply px-2 py-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--large {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--large.ui-tab--bar,
|
||||||
|
.ui-tab--large.ui-tab--line {
|
||||||
|
@apply py-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--large.ui-tab--segment {
|
||||||
|
@apply px-4 py-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Label */
|
||||||
|
.ui-tab__label {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close Button */
|
||||||
|
.ui-tab__close {
|
||||||
|
@apply ml-2 p-1 rounded-full opacity-60 hover:opacity-100 transition-all duration-200;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab__close:hover {
|
||||||
|
background-color: rgba(var(--ui-tabs-primary-rgb), 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab__close ion-icon {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Bar */
|
||||||
|
.ui-tabs__active-bar {
|
||||||
|
@apply absolute transition-all duration-300 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__active-bar--bar {
|
||||||
|
@apply bottom-0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--ui-tabs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__active-bar--line {
|
||||||
|
@apply bottom-0 h-px;
|
||||||
|
background-color: var(--ui-tabs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add Button */
|
||||||
|
.ui-tabs__add-btn {
|
||||||
|
@apply flex items-center justify-center px-3 py-2 transition-colors duration-200;
|
||||||
|
color: var(--ion-color-medium, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__add-btn:hover {
|
||||||
|
color: var(--ui-tabs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__add-btn ion-icon {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区域 */
|
||||||
|
.ui-tabs__content {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画支持 */
|
||||||
|
.ui-tabs--animated .ui-tabs__content {
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 垂直布局 */
|
||||||
|
.ui-tabs--left,
|
||||||
|
.ui-tabs--right {
|
||||||
|
@apply flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs--left .ui-tabs__nav-wrapper {
|
||||||
|
@apply min-w-48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs--right .ui-tabs__nav-wrapper {
|
||||||
|
@apply min-w-48 order-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs--left .ui-tabs__nav,
|
||||||
|
.ui-tabs--right .ui-tabs__nav {
|
||||||
|
@apply flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs--left .ui-tabs__content,
|
||||||
|
.ui-tabs--right .ui-tabs__content {
|
||||||
|
@apply flex-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 垂直布局tab调整 */
|
||||||
|
.ui-tabs--left .ui-tab,
|
||||||
|
.ui-tabs--right .ui-tab {
|
||||||
|
@apply justify-start w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs--left .ui-tab--bar,
|
||||||
|
.ui-tabs--right .ui-tab--bar {
|
||||||
|
@apply border-b-0 border-r-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs--left .ui-tab--bar.ui-tab--active,
|
||||||
|
.ui-tabs--right .ui-tab--bar.ui-tab--active {
|
||||||
|
border-color: var(--ui-tabs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 垂直布局active bar */
|
||||||
|
.ui-tabs--left .ui-tabs__active-bar--bar,
|
||||||
|
.ui-tabs--right .ui-tabs__active-bar--bar {
|
||||||
|
@apply right-0 w-0.5 h-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs--left .ui-tabs__active-bar--line,
|
||||||
|
.ui-tabs--right .ui-tabs__active-bar--line {
|
||||||
|
@apply right-0 w-px h-0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.ui-tabs__nav--small .ui-tab--bar,
|
||||||
|
.ui-tabs__nav--small .ui-tab--line {
|
||||||
|
@apply py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav--medium .ui-tab--bar,
|
||||||
|
.ui-tabs__nav--medium .ui-tab--line {
|
||||||
|
@apply py-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab__close {
|
||||||
|
@apply ml-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode 支持 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.ui-tabs {
|
||||||
|
--ui-tabs-background: var(--ion-background-color, #1a1a1a);
|
||||||
|
--ui-tabs-text: var(--ion-text-color, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tabs__nav--segment {
|
||||||
|
background-color: var(--ion-color-dark, #2d2d2d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab--segment.ui-tab--active {
|
||||||
|
background-color: var(--ion-color-dark-tint, #404040);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 触摸设备优化 */
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.ui-tab--bar,
|
||||||
|
.ui-tab--line,
|
||||||
|
.ui-tab--segment {
|
||||||
|
@apply py-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-tab__close {
|
||||||
|
@apply p-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
97
src/components/ui/tabs/types.ts
Normal file
97
src/components/ui/tabs/types.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
export interface TabItem {
|
||||||
|
name: string | number;
|
||||||
|
title: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
lazy?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsProps {
|
||||||
|
/** 当前激活的标签页名称 */
|
||||||
|
modelValue?: string | number;
|
||||||
|
/** 标签页类型 */
|
||||||
|
type?: "bar" | "line" | "segment";
|
||||||
|
/** 标签页尺寸 */
|
||||||
|
size?: "small" | "medium" | "large";
|
||||||
|
/** 是否开启切换动画 */
|
||||||
|
animated?: boolean;
|
||||||
|
/** 是否可关闭 */
|
||||||
|
closable?: boolean;
|
||||||
|
/** 是否可添加 */
|
||||||
|
addable?: boolean;
|
||||||
|
/** 标签页位置 */
|
||||||
|
placement?: "top" | "bottom" | "left" | "right";
|
||||||
|
/** 标签样式 */
|
||||||
|
tabStyle?: string | Record<string, any>;
|
||||||
|
/** 标签类名 */
|
||||||
|
tabClass?: string;
|
||||||
|
/** 面板样式 */
|
||||||
|
paneStyle?: string | Record<string, any>;
|
||||||
|
/** 面板类名 */
|
||||||
|
paneClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsEmits {
|
||||||
|
/** 当前激活标签页发生变化时触发 */
|
||||||
|
(e: "update:modelValue", value: string | number): void;
|
||||||
|
/** 点击标签时触发 */
|
||||||
|
(e: "tabClick", key: string | number, event: Event): void;
|
||||||
|
/** 关闭标签时触发 */
|
||||||
|
(e: "tabClose", key: string | number): void;
|
||||||
|
/** 添加标签时触发 */
|
||||||
|
(e: "tabAdd"): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabPaneProps {
|
||||||
|
/** 标签页标识,必须唯一 */
|
||||||
|
name: string | number;
|
||||||
|
/** 标签页标题 */
|
||||||
|
title: string;
|
||||||
|
/** 是否禁用 */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** 是否可关闭 */
|
||||||
|
closable?: boolean;
|
||||||
|
/** 是否懒加载 */
|
||||||
|
lazy?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsContext {
|
||||||
|
/** 当前激活的标签页 */
|
||||||
|
currentTab: Readonly<Ref<string | number | undefined>>;
|
||||||
|
/** 标签页类型 */
|
||||||
|
type: Readonly<Ref<"bar" | "line" | "segment">>;
|
||||||
|
/** 标签页尺寸 */
|
||||||
|
size: Readonly<Ref<"small" | "medium" | "large">>;
|
||||||
|
/** 是否可关闭 */
|
||||||
|
closable: Readonly<Ref<boolean>>;
|
||||||
|
/** 是否开启动画 */
|
||||||
|
animated: Readonly<Ref<boolean>>;
|
||||||
|
/** 标签页位置 */
|
||||||
|
placement: Readonly<Ref<"top" | "bottom" | "left" | "right">>;
|
||||||
|
/** 标签样式 */
|
||||||
|
tabStyle: Readonly<Ref<string | Record<string, any> | undefined>>;
|
||||||
|
/** 标签类名 */
|
||||||
|
tabClass: Readonly<Ref<string | undefined>>;
|
||||||
|
/** 面板样式 */
|
||||||
|
paneStyle: Readonly<Ref<string | Record<string, any> | undefined>>;
|
||||||
|
/** 面板类名 */
|
||||||
|
paneClass: Readonly<Ref<string | undefined>>;
|
||||||
|
/** 激活标签页方法 */
|
||||||
|
activateTab: (key: string | number, event?: Event) => void;
|
||||||
|
/** 关闭标签页方法 */
|
||||||
|
closeTab: (key: string | number) => void;
|
||||||
|
/** 注册标签页 */
|
||||||
|
registerTabPane: (pane: TabItem) => void;
|
||||||
|
/** 注销标签页 */
|
||||||
|
unregisterTabPane: (pane: TabItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tabs 组件实例类型 */
|
||||||
|
export interface TabsInstance {
|
||||||
|
/** 注册标签页 */
|
||||||
|
registerTabPane: (pane: TabItem) => void;
|
||||||
|
/** 注销标签页 */
|
||||||
|
unregisterTabPane: (pane: TabItem) => void;
|
||||||
|
/** 更新活跃指示条位置 */
|
||||||
|
updateActiveBar: () => void;
|
||||||
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"lib": ["ESNext", "DOM"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
"types": ["./components"]
|
"resolveJsonModule": true,
|
||||||
|
"types": ["./components"],
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "components.d.ts", "auto-imports.d.ts", "types/*.d.ts"],
|
"references": [{ "path": "./tsconfig.node.json" }],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "components.d.ts", "auto-imports.d.ts", "types/*.d.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user