TearOffTabs 可撕离 Tab
Tab 拖拽超过阈值时触发 tear-off 事件;消费方决定是 spawn DetachedPanel 还是新窗口。
基础用法
普通 tabs + 拖拽手势。垂直拖动距离超过 tearThreshold(默认 60px)后触发 tear-off(id, item, x, y)。
组件本身不实现窗口/面板创建,由消费方决定行为。
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfTearOffTabs } from '@chufix-design/vue';
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable: true, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
{ id: 'readme', title: 'README.md', closable: true },
]);
const active = ref('orders');
</script>
<template>
<div style="height: 240px; border: 1px solid var(--line-1); border-radius: var(--r-6); overflow: hidden;">
<CfTearOffTabs
:tabs="tabs"
:model-value="active"
@update:model-value="(v) => active = v"
@tear-off="(id) => alert(`Torn off: ${id}\n(消费方在此 spawn DetachedPanel 或新窗口)`)"
@close="(id) => tabs = tabs.filter(t => t.id !== id)"
>
<template #content-orders>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;">// orders.ts</pre>
</template>
<template #content-login>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;">// login.ts</pre>
</template>
<template #content-readme>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;"># README</pre>
</template>
</CfTearOffTabs>
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfTearOffTabs } from '@chufix-design/vue';
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
{ id: 'readme', title: 'README.md', closable: true },
]);
const active = ref('orders');
</script>
<template>
<div style="height: 240px; border: 1px solid var(--line-1); border-radius: var(--r-6); overflow: hidden;">
<CfTearOffTabs
:tabs="tabs"
:model-value="active"
@update:model-value="(v) => active = v"
@tear-off="(id) => alert(`Torn off: ${id}\n(消费方在此 spawn DetachedPanel 或新窗口)`)"
@close="(id) => tabs = tabs.filter(t => t.id !== id)"
>
<template #content-orders>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;">// orders.ts</pre>
</template>
<template #content-login>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;">// login.ts</pre>
</template>
<template #content-readme>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;"># README</pre>
</template>
</CfTearOffTabs>
</div>
</template> <CfTearOffTabs
tabs={tabs}
value={active}
onChange={setActive}
onTearOff={(id) => spawnPanel(id)}
onClose={(id) => removeTab(id)}
slots={{ 'content-orders': <pre>orders.ts</pre> }}
/> <CfTearOffTabs
tabs={tabs}
value={active}
onChange={setActive}
onTearOff={(id) => spawnPanel(id)}
onClose={(id) => removeTab(id)}
slots={{ 'content-orders': <pre>orders.ts</pre> }}
/> Tear-off 接 DetachedPanel
把撕下的 tab 即刻 spawn 成 DetachedPanel;关闭面板时把 tab 还原回去。
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfTearOffTabs, CfDetachedPanel } from '@chufix-design/vue';
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable: true, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
]);
const active = ref('orders');
const detachedId = ref<string | null>(null);
const detachedX = ref(0);
const detachedY = ref(0);
function tearOff(id: string, _: any, x: number, y: number) {
detachedId.value = id;
detachedX.value = Math.max(40, x - 100);
detachedY.value = Math.max(40, y - 20);
tabs.value = tabs.value.filter((t) => t.id !== id);
}
function reattach() {
if (detachedId.value) {
tabs.value = [
...tabs.value,
{ id: detachedId.value, title: `${detachedId.value}.ts`, closable: true },
];
detachedId.value = null;
}
}
</script>
<template>
<div style="height: 200px; border: 1px solid var(--line-1); border-radius: var(--r-6); overflow: hidden;">
<CfTearOffTabs
:tabs="tabs"
:model-value="active"
@update:model-value="(v) => active = v"
@tear-off="tearOff"
@close="(id) => tabs = tabs.filter((t) => t.id !== id)"
>
<template #content-orders>
<pre style="margin: 0; padding: 12px; font-family: var(--font-mono); font-size: 12px;">// orders.ts
拖动这个 tab 向下可触发 tear-off →</pre>
</template>
<template #content-login>
<pre style="margin: 0; padding: 12px; font-family: var(--font-mono); font-size: 12px;">// login.ts</pre>
</template>
</CfTearOffTabs>
</div>
<CfDetachedPanel
:open="detachedId !== null"
:x="detachedX"
:y="detachedY"
:title="`${detachedId ?? ''}.ts`"
:width="280"
:height="160"
@update:open="(v) => { if (!v) reattach(); }"
>
<pre style="margin: 0; padding: 0; font-family: var(--font-mono); font-size: 12px;">// 已被分离的 {{ detachedId }}
关闭面板会重新挂回 tab 栏。</pre>
</CfDetachedPanel>
</template> <script setup>
import { ref } from 'vue';
import { CfTearOffTabs, CfDetachedPanel } from '@chufix-design/vue';
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
]);
const active = ref('orders');
const detachedId = ref<string | null>(null);
const detachedX = ref(0);
const detachedY = ref(0);
function tearOff(id, _, x, y) {
detachedId.value = id;
detachedX.value = Math.max(40, x - 100);
detachedY.value = Math.max(40, y - 20);
tabs.value = tabs.value.filter((t) => t.id !== id);
}
function reattach() {
if (detachedId.value) {
tabs.value = [
...tabs.value,
{ id: detachedId.value, title: `${detachedId.value}.ts`, closable: true },
];
detachedId.value = null;
}
}
</script>
<template>
<div style="height: 200px; border: 1px solid var(--line-1); border-radius: var(--r-6); overflow: hidden;">
<CfTearOffTabs
:tabs="tabs"
:model-value="active"
@update:model-value="(v) => active = v"
@tear-off="tearOff"
@close="(id) => tabs = tabs.filter((t) => t.id !== id)"
>
<template #content-orders>
<pre style="margin: 0; padding: 12px; font-family: var(--font-mono); font-size: 12px;">// orders.ts
拖动这个 tab 向下可触发 tear-off →</pre>
</template>
<template #content-login>
<pre style="margin: 0; padding: 12px; font-family: var(--font-mono); font-size: 12px;">// login.ts</pre>
</template>
</CfTearOffTabs>
</div>
<CfDetachedPanel
:open="detachedId !== null"
:x="detachedX"
:y="detachedY"
:title="`${detachedId ?? ''}.ts`"
:width="280"
:height="160"
@update:open="(v) => { if (!v) reattach(); }"
>
<pre style="margin: 0; padding: 0; font-family: var(--font-mono); font-size: 12px;">// 已被分离的 {{ detachedId }}
关闭面板会重新挂回 tab 栏。</pre>
</CfDetachedPanel>
</template> <CfTearOffTabs ... onTearOff={tearOff} />
<CfDetachedPanel ... onOpenChange={reattach} /> <CfTearOffTabs ... onTearOff={tearOff} />
<CfDetachedPanel ... onOpenChange={reattach} /> API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
tabs | TearOffTabItem[] | — | { id, title, contentKey?, modified?, closable? } |
modelValue / value | string | 第一个 tab | 活动 tab id |
tearThreshold | number | 60 | 触发 tear-off 的垂直拖动距离 |
事件:update:modelValue / tear-off(id, item, x, y) / close(id, item)。
反馈与讨论
TearOffTabs 可撕离 Tab 的讨论