代码演示
如何引入
<script setup lang="ts">
import { Transfer } from '@fibrous/semi-ui-vue';
</script>
基本使用
数据项需传入 value、label、key
代码
<script setup lang="ts">
import { Transfer, type TransferDataItem, type TransferValue } from '@fibrous/semi-ui-vue';
const data = Array.from({ length: 100 }, (_v, i) => ({
label: `选项名称 ${i}`,
value: i,
disabled: false,
key: `key-${i}`,
}));
const onChange = (values: TransferValue, items: TransferDataItem[]) => console.log(values, items);
</script>
<template>
<Transfer :dataSource="data" :style="{ width: '568px', height: '416px' }" @change="onChange" />
</template>分组
将 type 设为 groupList
分组的 dataSource,一级子元素必须拥有 title 以及 children 属性,结构参考 <GroupItem>
暂不支持多层嵌套
代码
<script setup lang="ts">
import { Transfer } from '@fibrous/semi-ui-vue';
const dataWithGroup = [
{
title: '类别A',
children: [
{ label: 'A-1', value: 1, disabled: false, key: 1 },
{ label: 'A-2', value: 2, disabled: false, key: 2 },
{ label: 'A-3', value: 3, disabled: false, key: 3 },
],
},
{
title: '类别B',
children: [
{ label: 'B-1', value: 4, disabled: false, key: 4 },
{ label: 'B-2', value: 5, disabled: false, key: 5 },
{ label: 'B-3(disabled)', value: 6, disabled: true, key: 6 },
],
},
{
title: '类别C',
children: [
{ label: 'C-1', value: 7, disabled: false, key: 7 },
{ label: 'C-2', value: 8, disabled: false, key: 8 },
{ label: 'C-3', value: 9, disabled: false, key: 9 },
{ label: 'C-4', value: 10, disabled: false, key: 10 },
{ label: 'C-5', value: 11, disabled: false, key: 11 },
{ label: 'C-6', value: 12, disabled: false, key: 12 },
{ label: 'C-7', value: 13, disabled: false, key: 13 },
],
},
];
</script>
<template>
<Transfer :dataSource="dataWithGroup" :defaultValue="[6]" style="width: 568px" type="groupList" @change="(values, items) => console.log(values, items)" />
</template>自定义筛选逻辑,自定义选项数据渲染
使用filter自定义搜索逻辑,返回 true 时表示当前项符合筛选规则,保留当前项在列表中的显示,返回 false 则表示不符合,当前项会被隐藏。
当 type 为 treeList时,如需要自定义搜索逻辑,需设置 filter 为 true,并通过 treeProps 的 filterTreeNode 设置自定义的搜索函数。
使用 #sourceItem 插槽,你可以自定义左侧每一条源数据的渲染结构。例如结合 Highlight 组件高亮搜索匹配文本
使用 #selectedItem 插槽你可以自定义右侧每一条已选项的渲染结构。
数据项自带的字段(如 demo 中的 abbr、color)都在插槽参数 item 上;只需要替换条目文本时,用 #label 插槽更简单,它同时作用于左侧条目、右侧已选条目和拖拽副本。
代码
<script setup lang="ts">
import { ref } from 'vue';
import { IconClose } from '@fibrous/semi-icons-vue';
import { Avatar, Button, Checkbox, Highlight, Transfer, type AvatarColor, type TransferDataItem, type TransferValue } from '@fibrous/semi-ui-vue';
interface Person extends TransferDataItem {
abbr: string;
color: AvatarColor;
}
const searchText = ref('');
const customFilter = (searchValue: string, item: TransferDataItem) => String(item.value).includes(searchValue) || String(item.label).includes(searchValue);
const data = [
{ label: '夏可漫', value: 'xiakeman@example.com', abbr: '夏', color: 'amber', area: 'US', key: 1 },
{ label: '申悦', value: 'shenyue@example.com', abbr: '申', color: 'indigo', area: 'UK', key: 2 },
{ label: '文嘉茂', value: 'wenjiamao@example.com', abbr: '文', color: 'cyan', area: 'HK', key: 3 },
{ label: '曲晨一', value: 'quchenyi@example.com', abbr: '曲', color: 'blue', area: 'India', key: 4 },
{ label: '曲晨二', value: 'quchener@example.com', abbr: '二', color: 'blue', area: 'India', key: 5 },
{ label: '曲晨三', value: 'quchensan@example.com', abbr: '三', color: 'blue', area: 'India', key: 6 },
] satisfies Person[];
const inputProps = { placeholder: '搜索姓名或邮箱' };
const onChange = (values: TransferValue, items: TransferDataItem[]) => console.log(values, items);
</script>
<template>
<Transfer :dataSource="data" :defaultValue="['xiakeman@example.com', 'shenyue@example.com']" :filter="customFilter" :inputProps="inputProps" :style="{ width: '568px' }" @change="onChange" @search="searchText = $event">
<template #sourceItem="{ item, checked, onChange: onItemChange }">
<div class="components-transfer-demo-source-item">
<Checkbox :modelValue="checked" :style="{ height: '52px', alignItems: 'center' }" @change="onItemChange">
<Avatar :color="(item as Person).color" size="small">{{ (item as Person).abbr }}</Avatar>
<div class="info">
<div class="name">
<Highlight :searchWords="[searchText]" :sourceString="String(item.label)" />
</div>
<div class="email">
<Highlight :searchWords="[searchText]" :sourceString="String(item.value)" />
</div>
</div>
</Checkbox>
</div>
</template>
<template #selectedItem="{ item, onRemove }">
<div class="components-transfer-demo-selected-item">
<Avatar :color="(item as Person).color" size="small">{{ (item as Person).abbr }}</Avatar>
<div class="info">
<div class="name">{{ item.label }}</div>
<div class="email">{{ item.value }}</div>
</div>
<Button :aria-label="`移除 ${item.label}`" class="remove" size="small" theme="borderless" type="tertiary" @click="onRemove">
<template #icon>
<IconClose />
</template>
</Button>
</div>
</template>
</Transfer>
</template>.components-transfer-demo-selected-item {
.remove {
opacity: 0;
}
&:hover,
&:focus-within {
.remove {
opacity: 1;
}
}
}
.components-transfer-demo-selected-item,
.components-transfer-demo-source-item {
height: 52px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
&:hover {
background-color: var(--semi-color-fill-0);
}
.info {
margin-left: 8px;
flex-grow: 1;
}
.name {
font-size: 14px;
line-height: 20px;
}
.email {
font-size: 12px;
line-height: 16px;
color: var(--semi-color-text-2);
}
}
禁用
代码
<script setup lang="ts">
import { Transfer, type TransferDataItem, type TransferValue } from '@fibrous/semi-ui-vue';
const data = Array.from({ length: 20 }, (_v, i) => ({
label: `选项名称 ${i}`,
value: i,
disabled: false,
key: `key-${i}`,
}));
const onChange = (values: TransferValue, items: TransferDataItem[]) => console.log(values, items);
</script>
<template>
<Transfer :dataSource="data" :defaultValue="[2, 4]" disabled :style="{ width: '568px', height: '416px' }" @change="onChange" />
</template>拖拽排序
将 draggable设为 true,开启拖拽排序功能。
代码
<script setup lang="ts">
import { Transfer } from '@fibrous/semi-ui-vue';
const data = Array.from({ length: 30 }, (_, i) => ({
label: `选项名称 ${i}`,
value: i,
disabled: false,
key: `key-${i}`,
}));
</script>
<template>
<Transfer :dataSource="data" :defaultValue="[2, 4]" draggable style="width: 568px; height: 416px" @change="(values, items) => console.log(values, items)" />
</template>左侧分页
当左侧选项较多时,可以通过 pagination 属性开启分页功能。
pagination 接收一个对象,包含以下属性:
pageSize: 每页显示的条数,默认为 10currentPage: 当前页码(受控模式)defaultCurrentPage: 默认当前页码(非受控模式)onPageChange: 页码变化时的回调
代码
<script setup lang="ts">
import { Transfer } from '@fibrous/semi-ui-vue';
const data = Array.from({ length: 100 }, (_, i) => ({
label: `选项名称 ${i}`,
value: i,
disabled: false,
key: `key-${i}`,
}));
</script>
<template>
<Transfer :dataSource="data" :pagination="{ pageSize: 10 }" style="width: 568px; height: 416px" @change="(values, items) => console.log(values, items)" />
</template>左侧分页 + 受控页码
通过 pagination.currentPage 可以受控地设置当前页码。
代码
<script setup lang="ts">
import { ref } from 'vue';
import { Transfer, ButtonGroup, Button } from '@fibrous/semi-ui-vue';
const currentPage = ref(1);
const data = Array.from({ length: 100 }, (_, i) => ({
label: `选项名称 ${i}`,
value: i,
disabled: false,
key: `key-${i}`,
}));
</script>
<template>
<div>
<ButtonGroup style="margin-bottom: 12px">
<Button @click="currentPage = 1">第1页</Button>
<Button @click="currentPage = 2">第2页</Button>
<Button @click="currentPage = 5">第5页</Button>
<Button @click="currentPage = 10">第10页</Button>
</ButtonGroup>
<div>当前页码: {{ currentPage }}</div>
<Transfer :dataSource="data" :pagination="{ pageSize: 10, currentPage, onPageChange: (page) => (currentPage = page) }" style="width: 568px; height: 416px" @change="(values, items) => console.log(values, items)" />
</div>
</template>拖拽 + 自定义已选项渲染
将 draggable设为 true,开启拖拽排序功能;使用 #selectedItem 插槽自定义右侧已选项渲染;
你可以将触发器定义为任意你想要的内容,并且添加样式。draggable 时插槽参数 handle 是拖拽手柄组件,用 <component :is="handle"> 包住触发器即可。
代码
<script setup lang="ts">
import { IconClose, IconHandle } from '@fibrous/semi-icons-vue';
import { Avatar, Button, Checkbox, Transfer, type AvatarColor, type TransferDataItem, type TransferValue } from '@fibrous/semi-ui-vue';
interface Person extends TransferDataItem {
abbr: string;
color: AvatarColor;
}
const customFilter = (searchValue: string, item: TransferDataItem) => String(item.value).includes(searchValue) || String(item.label).includes(searchValue);
const data = [
{ label: '夏可漫', value: 'xiakeman@example.com', abbr: '夏', color: 'amber', area: 'US', key: 1 },
{ label: '申悦', value: 'shenyue@example.com', abbr: '申', color: 'indigo', area: 'UK', key: 2 },
{ label: '文嘉茂', value: 'wenjiamao@example.com', abbr: '文', color: 'cyan', area: 'HK', key: 3 },
{ label: '曲晨一', value: 'quchenyi@example.com', abbr: '曲', color: 'blue', area: 'India', key: 4 },
{ label: '曲晨二', value: 'quchener@example.com', abbr: '二', color: 'blue', area: 'India', key: 5 },
{ label: '曲晨三', value: 'quchensan@example.com', abbr: '三', color: 'blue', area: 'India', key: 6 },
] satisfies Person[];
const inputProps = { placeholder: '搜索姓名或邮箱' };
const onChange = (values: TransferValue, items: TransferDataItem[]) => console.log(values, items);
</script>
<template>
<Transfer :dataSource="data" :defaultValue="['xiakeman@example.com', 'shenyue@example.com']" draggable :filter="customFilter" :inputProps="inputProps" :style="{ width: '568px' }" @change="onChange">
<template #sourceItem="{ item, checked, onChange: onItemChange }">
<div class="components-transfer-demo-source-item">
<Checkbox :modelValue="checked" :style="{ height: '52px', alignItems: 'center' }" @change="onItemChange">
<Avatar :color="(item as Person).color" size="small">{{ (item as Person).abbr }}</Avatar>
<div class="info">
<div class="name">{{ item.label }}</div>
<div class="email">{{ item.value }}</div>
</div>
</Checkbox>
</div>
</template>
<template #selectedItem="{ item, handle, onRemove }">
<div v-if="handle" class="components-transfer-demo-selected-item">
<component :is="handle">
<IconHandle class="semi-right-item-drag-handler" />
</component>
<Avatar :color="(item as Person).color" size="small">{{ (item as Person).abbr }}</Avatar>
<div class="info">
<div class="name">{{ item.label }}</div>
<div class="email">{{ item.value }}</div>
</div>
<Button :aria-label="`移除 ${item.label}`" class="remove" size="small" theme="borderless" type="tertiary" @click="onRemove">
<template #icon>
<IconClose />
</template>
</Button>
</div>
</template>
</Transfer>
</template>自定义渲染面板头部信息
#sourceHeader、#selectedHeader 插槽允许用户自定义渲染左右两个面板的头部信息,插槽参数类型分别为 TransferSourceHeaderSlotProps、TransferSelectedHeaderSlotProps:
type TransferSourceHeaderSlotProps = {
num: number; // 数据总数或筛选结果数目
showButton: boolean; // 是否展示全选/取消全选按钮
allChecked: boolean; // 当前数据是否已全选
onAllClick: () => void; // 点击全选/取消全选按钮后应调用的函数
leafOnlyNum?: number; // 仅在 type 为 treeList 时提供,表示叶子节点数量
};
type TransferSelectedHeaderSlotProps = {
num: number; // 已选中数据总数
showButton: boolean; // 是否展示清空按钮
onClear: () => void; // 点击清空按钮后应调用的函数
};
使用示例如下
代码
<script setup lang="ts">
import { Button, Transfer } from '@fibrous/semi-ui-vue';
const data = Array.from({ length: 30 }, (_v, i) => ({
label: `选项名称 ${i}`,
value: i,
disabled: false,
key: `key-${i}`,
}));
const headerStyle = { margin: '10px 0 0 10px', height: '24px', display: 'flex', alignItems: 'center' };
</script>
<template>
<Transfer :dataSource="data" :style="{ width: '568px', height: '416px' }">
<template #sourceHeader="{ num, showButton, allChecked, onAllClick }">
<div :style="headerStyle">
<span>共 {{ num }} 项</span>
<Button v-if="showButton" size="small" theme="borderless" type="tertiary" @click="onAllClick">
{{ allChecked ? '取消全选' : '全选' }}
</Button>
</div>
</template>
<template #selectedHeader="{ num, showButton, onClear }">
<div :style="headerStyle">
<span>{{ num }} 项已选</span>
<Button v-if="showButton" size="small" theme="borderless" type="tertiary" @click="onClear"> 清空 </Button>
</div>
</template>
</Transfer>
</template>完全自定义渲染
Semi 提供了 #sourcePanel、#selectedPanel 插槽,允许你完全自定义左右侧两个面板的渲染结构
通过该功能,你可以直接复用 Transfer 内部的逻辑能力,实现高度自定义样式结构的Transfer组件
#sourcePanel 的参数类型为 TransferSourcePanelSlotProps,你可以从中获取数据来渲染出你的 Panel 结构
interface TransferSourcePanelSlotProps {
value: Array<string | number>; // 所有选中项的 value
loading: boolean | undefined; // 是否加载中
noMatch: boolean; // 是否没有符合当前搜索值匹配的项
filterData: Array<Item>; // 匹配当前搜索值的项
sourceData: Array<Item>; // 所有项
propsDataSource: Array<Item> | Array<GroupItem> | Array<TreeItem>; // Transfer 的 dataSource
allChecked: boolean; // 是否全部选中
showNumber: number; // 筛选结果数量
inputValue: string; // input搜索框的值
selectedItems: Map<string | number, Item>; // 所有已选项的集合,Map 的键是 item.key
onSearch: (searchString: string) => void; // 搜索框变化时应调用的函数,入参为搜索值
onAllClick: () => void; // 左侧全部按钮点击时应调用的函数
onSelectOrRemove: (item: Item) => void; // 选择、删除单个选项时应调用的函数,入参应为当前操作item
onSelect: (values: Array<string | number>) => void; // 受控批量选中,入参为选中项的 value
}
#selectedPanel 的参数类型为 TransferSelectedPanelSlotProps
interface TransferSelectedPanelSlotProps {
length: number; // 已选项的数量
selectedData: Array<Item>; // 所有已选项集合
onClear: () => void; // 点击清空时应调用的回调函数
onRemove: (item: Item) => void; // 删除单个选项时应调用的函数
onSortEnd: (payload: { oldIndex: number; newIndex: number }) => void; // 对结果重新排序时应调用的函数
}
海底捞门店 0
海底捞门店 1
海底捞门店 2
海底捞门店 3
海底捞门店 4
海底捞门店 5
海底捞门店 6
海底捞门店 7
海底捞门店 8
海底捞门店 9
海底捞门店 10
海底捞门店 11
海底捞门店 12
海底捞门店 13
海底捞门店 14
海底捞门店 15
海底捞门店 16
海底捞门店 17
海底捞门店 18
海底捞门店 19
海底捞门店 20
海底捞门店 21
海底捞门店 22
海底捞门店 23
海底捞门店 24
海底捞门店 25
海底捞门店 26
海底捞门店 27
海底捞门店 28
海底捞门店 29
海底捞门店 30
海底捞门店 31
海底捞门店 32
海底捞门店 33
海底捞门店 34
海底捞门店 35
海底捞门店 36
海底捞门店 37
海底捞门店 38
海底捞门店 39
海底捞门店 40
海底捞门店 41
海底捞门店 42
海底捞门店 43
海底捞门店 44
海底捞门店 45
海底捞门店 46
海底捞门店 47
海底捞门店 48
海底捞门店 49
海底捞门店 50
海底捞门店 51
海底捞门店 52
海底捞门店 53
海底捞门店 54
海底捞门店 55
海底捞门店 56
海底捞门店 57
海底捞门店 58
海底捞门店 59
海底捞门店 60
海底捞门店 61
海底捞门店 62
海底捞门店 63
海底捞门店 64
海底捞门店 65
海底捞门店 66
海底捞门店 67
海底捞门店 68
海底捞门店 69
海底捞门店 70
海底捞门店 71
海底捞门店 72
海底捞门店 73
海底捞门店 74
海底捞门店 75
海底捞门店 76
海底捞门店 77
海底捞门店 78
海底捞门店 79
海底捞门店 80
海底捞门店 81
海底捞门店 82
海底捞门店 83
海底捞门店 84
海底捞门店 85
海底捞门店 86
海底捞门店 87
海底捞门店 88
海底捞门店 89
海底捞门店 90
海底捞门店 91
海底捞门店 92
海底捞门店 93
海底捞门店 94
海底捞门店 95
海底捞门店 96
海底捞门店 97
海底捞门店 98
海底捞门店 99
代码
<script setup lang="ts">
import { IconSearch } from '@fibrous/semi-icons-vue';
import { Button, Input, Spin, Transfer, type TransferValue } from '@fibrous/semi-ui-vue';
const dataSource = Array.from({ length: 100 }, (_v, i) => ({
label: `海底捞门店 ${i}`,
value: i,
disabled: false,
key: `key-${i}`,
}));
const onChange = (values: TransferValue) => console.log(values);
</script>
<template>
<Transfer class="component-transfer-demo-custom-panel" :dataSource="dataSource" @change="onChange">
<template #sourcePanel="{ loading, noMatch, filterData, selectedItems, allChecked, onAllClick, inputValue, onSearch, onSelectOrRemove }">
<section class="source-panel">
<div class="panel-header sp-font">门店列表</div>
<div class="panel-main">
<Input aria-label="搜索门店" showClear :style="{ width: '454px', margin: '12px 14px' }" @change="onSearch">
<template #prefix>
<IconSearch />
</template>
</Input>
<div class="panel-controls sp-font">
<span>待选门店: {{ filterData.length }}</span>
<Button size="small" theme="borderless" @click="onAllClick">
{{ allChecked ? '取消全选' : '全选' }}
</Button>
</div>
<div class="panel-list">
<Spin v-if="loading" />
<div v-else-if="noMatch" class="empty sp-font">
{{ inputValue ? '无搜索结果' : '暂无内容' }}
</div>
<template v-else>
<div v-for="item in filterData" :key="item.key" class="semi-transfer-item panel-item">
<p>{{ item.label }}</p>
<Button class="panel-item-remove" size="small" theme="borderless" type="primary" @click="onSelectOrRemove(item)">
{{ selectedItems.has(item.key) ? '删除' : '添加' }}
</Button>
</div>
</template>
</div>
</div>
</section>
</template>
<template #selectedPanel="{ selectedData, onClear, onRemove }">
<section class="selected-panel">
<div class="panel-header sp-font">
<div>已选同步门店: {{ selectedData.length }}</div>
<Button size="small" theme="borderless" type="primary" @click="onClear">清空</Button>
</div>
<div class="panel-main">
<div v-if="!selectedData.length" class="empty sp-font">暂无数据,请从左侧筛选</div>
<template v-else>
<div v-for="item in selectedData" :key="item.key" class="semi-transfer-item panel-item">
<p>{{ item.label }}</p>
<Button class="panel-item-remove" size="small" theme="borderless" type="primary" @click="onRemove(item)"> 删除 </Button>
</div>
</template>
</div>
</section>
</template>
</Transfer>
</template>.component-transfer-demo-custom-panel {
.sp-font {
color: var(--semi-grey-9);
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
.empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.panel-item {
flex-shrink: 0;
height: 56px;
border-radius: 4px;
padding: 8px 12px;
flex-wrap: wrap;
background-color: rgba(22, 24, 35, 0.03);
&-main {
flex-grow: 1;
}
p {
margin: 0 12px;
flex-basis: 100%;
}
.panel-item-remove {
cursor: pointer;
color: var(--semi-color-primary);
}
}
.panel-header {
padding: 10px 12px;
border: 1px solid rgba(22, 24, 35, 0.16);
border-radius: 4px 4px 0 0;
height: 38px;
box-sizing: border-box;
background-color: var(--semi-color-tertiary-light-default);
display: flex;
align-items: center;
justify-content: space-between;
.clear {
cursor: pointer;
color: var(--semi-color-primary);
}
}
.source-panel {
display: flex;
flex-direction: column;
width: 482px;
height: 353px;
.panel-main {
border: 1px solid var(--semi-color-border);
border-top: none;
.panel-list {
display: flex;
flex-wrap: wrap;
row-gap: 8px;
column-gap: 8px;
overflow-y: auto;
height: 214px;
margin-left: 12px;
margin-right: 12px;
padding-bottom: 8px;
}
}
.panel-controls {
margin: 10px 12px;
font-size: 12px;
line-height: 20px;
.semi-button {
margin-left: 8px;
font-size: 12px;
}
}
.panel-item {
width: 176px;
}
margin-right: 16px;
}
.selected-panel {
width: 200px;
height: 353px;
.panel-main {
display: flex;
flex-direction: column;
overflow-y: auto;
padding: 12px;
border: 1px solid var(--semi-color-border);
border-top: none;
height: 323px;
box-sizing: border-box;
row-gap: 8px;
}
}
}
完全自定义渲染 、 拖拽排序
在完全自定义渲染的场景下,由于拖拽区的渲染也已由你完全接管,因此你不声明 draggable 亦可。但你需要自行实现拖拽逻辑,你可以借助社区中的拖拽工具库 dnd-kit 快速实现功能。
另外,要支持拖拽排序,你需要在拖拽排序结束后,将 oldIndex、newIndex 作为入参,调用 onSortEnd
使用 dnd-kit 的示例如下,需要用到的核心依赖有 @dnd-kit/dom:DragDropManager 提供拖拽上下文,每个已选项对应一个 @dnd-kit/dom/sortable 导出的 Sortable,参数如下(项目使用 @dnd-kit/vue 时,它的 useSortable 接收同样的 id、index、element、handle)
1. 作用:通过唯一标志 id 与当前位置 index 获取拖拽过程中必要信息
2. 核心输入参数:
- id: 唯一标识,数字或者字符串
- index: 该项在列表中的当前位置,列表变化后需同步更新
- element: 可拖拽的 dom 节点
- handle: 可选,拖拽手柄的元素,不传时整个节点都可拖拽
3. 第二个参数是 DragDropManager 实例,卸载组件时需要调用各 Sortable 与 manager 的 destroy
拖拽结束后,在 manager.monitor 的 dragend 事件中,用 event.operation.source 的 initialIndex 与 index 得到拖拽前后的位置;event.canceled 为 true 表示拖拽已取消。
示例用 v-model 保存已选值,拖拽结束后直接把调整后的顺序写回 v-model;不自己保存值时,也可以在 #selectedPanel 的参数里取 onSortEnd 调用。
海底捞门店 0
海底捞门店 1
海底捞门店 2
海底捞门店 3
海底捞门店 4
海底捞门店 5
海底捞门店 6
海底捞门店 7
海底捞门店 8
海底捞门店 9
海底捞门店 10
海底捞门店 11
海底捞门店 12
海底捞门店 13
海底捞门店 14
海底捞门店 15
海底捞门店 16
海底捞门店 17
海底捞门店 18
海底捞门店 19
海底捞门店 20
海底捞门店 21
海底捞门店 22
海底捞门店 23
海底捞门店 24
海底捞门店 25
海底捞门店 26
海底捞门店 27
海底捞门店 28
海底捞门店 29
海底捞门店 30
海底捞门店 31
海底捞门店 32
海底捞门店 33
海底捞门店 34
海底捞门店 35
海底捞门店 36
海底捞门店 37
海底捞门店 38
海底捞门店 39
海底捞门店 40
海底捞门店 41
海底捞门店 42
海底捞门店 43
海底捞门店 44
海底捞门店 45
海底捞门店 46
海底捞门店 47
海底捞门店 48
海底捞门店 49
海底捞门店 50
海底捞门店 51
海底捞门店 52
海底捞门店 53
海底捞门店 54
海底捞门店 55
海底捞门店 56
海底捞门店 57
海底捞门店 58
海底捞门店 59
海底捞门店 60
海底捞门店 61
海底捞门店 62
海底捞门店 63
海底捞门店 64
海底捞门店 65
海底捞门店 66
海底捞门店 67
海底捞门店 68
海底捞门店 69
海底捞门店 70
海底捞门店 71
海底捞门店 72
海底捞门店 73
海底捞门店 74
海底捞门店 75
海底捞门店 76
海底捞门店 77
海底捞门店 78
海底捞门店 79
海底捞门店 80
海底捞门店 81
海底捞门店 82
海底捞门店 83
海底捞门店 84
海底捞门店 85
海底捞门店 86
海底捞门店 87
海底捞门店 88
海底捞门店 89
海底捞门店 90
海底捞门店 91
海底捞门店 92
海底捞门店 93
海底捞门店 94
海底捞门店 95
海底捞门店 96
海底捞门店 97
海底捞门店 98
海底捞门店 99
海底捞门店 2
海底捞门店 4
代码
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { DragDropManager } from '@dnd-kit/dom';
import { Sortable, isSortable } from '@dnd-kit/dom/sortable';
import { IconHandle, IconSearch } from '@fibrous/semi-icons-vue';
import { Button, Input, Spin, Transfer, type TransferDataItem, type TransferValue } from '@fibrous/semi-ui-vue';
type SortableId = string | number;
const dataSource = Array.from({ length: 100 }, (_v, i) => ({
label: `海底捞门店 ${i}`,
value: i,
disabled: false,
key: `key-${i}`,
}));
const values = ref<TransferValue>([2, 4]);
// Items drag by value, falling back to key
const sortableId = (item: TransferDataItem): SortableId => item.value ?? item.key;
const manager = new DragDropManager();
const sortables = new Map<SortableId, Sortable>();
const itemElements = new Map<SortableId, HTMLElement>();
const handleElements = new Map<SortableId, HTMLElement>();
const bindElement = (elements: Map<SortableId, HTMLElement>, id: SortableId, element: unknown) => {
if (element instanceof HTMLElement) {
elements.set(id, element);
} else {
elements.delete(id);
}
};
// One Sortable per selected item, synced after the DOM updates when items change
const syncSortables = () => {
values.value.forEach((id, index) => {
const element = itemElements.get(id);
if (!element) {
return;
}
const sortable = sortables.get(id);
if (sortable) {
sortable.index = index;
} else {
sortables.set(id, new Sortable({ id, index, element, handle: handleElements.get(id) }, manager));
}
});
for (const [id, sortable] of sortables) {
if (!values.value.includes(id)) {
sortable.destroy();
sortables.delete(id);
}
}
};
// The selected order is the value array order, so write it back to v-model after a drop
const stopListening = manager.monitor.addEventListener('dragend', (event) => {
const { source } = event.operation;
if (event.canceled || !isSortable(source) || source.initialIndex === source.index) {
return;
}
const next = [...values.value];
const [moved] = next.splice(source.initialIndex, 1);
next.splice(source.index, 0, moved);
values.value = next;
});
watch(values, syncSortables, { flush: 'post' });
onMounted(syncSortables);
onBeforeUnmount(() => {
stopListening();
sortables.forEach((sortable) => sortable.destroy());
manager.destroy();
});
const onChange = (next: TransferValue) => console.log(next);
</script>
<template>
<Transfer v-model="values" class="component-transfer-demo-custom-panel" :dataSource="dataSource" @change="onChange">
<template #sourcePanel="{ loading, noMatch, filterData, selectedItems, allChecked, onAllClick, inputValue, onSearch, onSelectOrRemove }">
<section class="source-panel">
<div class="panel-header sp-font">门店列表</div>
<div class="panel-main">
<Input aria-label="搜索门店" showClear :style="{ width: '454px', margin: '12px 14px' }" @change="onSearch">
<template #prefix>
<IconSearch />
</template>
</Input>
<div class="panel-controls sp-font">
<span>待选门店: {{ filterData.length }}</span>
<Button size="small" theme="borderless" @click="onAllClick">
{{ allChecked ? '取消全选' : '全选' }}
</Button>
</div>
<div class="panel-list">
<Spin v-if="loading" />
<div v-else-if="noMatch" class="empty sp-font">
{{ inputValue ? '无搜索结果' : '暂无内容' }}
</div>
<template v-else>
<div v-for="item in filterData" :key="item.key" class="semi-transfer-item panel-item">
<div class="panel-item-main" :style="{ flexGrow: 1 }">
<p :style="{ margin: '0 12px' }">{{ item.label }}</p>
<Button class="panel-item-remove" size="small" theme="borderless" type="primary" @click="onSelectOrRemove(item)">
{{ selectedItems.has(item.key) ? '删除' : '添加' }}
</Button>
</div>
</div>
</template>
</div>
</div>
</section>
</template>
<template #selectedPanel="{ selectedData, onClear, onRemove }">
<section class="selected-panel">
<div class="panel-header sp-font">
<div>已选同步门店: {{ selectedData.length }}</div>
<Button size="small" theme="borderless" type="primary" @click="onClear">清空</Button>
</div>
<div class="panel-main" :style="{ display: 'block' }">
<div :style="{ overflow: 'auto', display: 'flex', flexDirection: 'column', rowGap: '8px' }">
<div v-for="item in selectedData" :key="item.key" :ref="(element) => bindElement(itemElements, sortableId(item), element)">
<div class="semi-transfer-item panel-item">
<!-- Only the handle drags; the Accessibility plugin adds a button role and Tab stop -->
<span :ref="(element) => bindElement(handleElements, sortableId(item), element)" :aria-label="String(item.label)" :style="{ lineHeight: 0 }">
<IconHandle class="pane-item-drag-handler" />
</span>
<div class="panel-item-main" :style="{ flexGrow: 1 }">
<p :style="{ margin: '0 12px' }">{{ item.label }}</p>
<Button class="panel-item-remove" size="small" theme="borderless" type="primary" @click="onRemove(item)"> 删除 </Button>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
</Transfer>
</template>树穿梭框
传入 type 为treeList,使用Tree组件作为自定义渲染列表。
可通过treeProps(TreeProps)来覆盖默认树的属性,左侧树默认属性为
{
multiple:true,
disableStrictly:true,
leafOnly:true,
filterTreeNode:true,
defaultExpandAll:true,
}
Tree 自带的搜索框已用空的 #search 插槽隐藏,由 Transfer 自己的搜索框驱动过滤。Tree 的插槽中只有 #label 经 Transfer 的 #label 插槽转发,同时用于左侧树行和右侧已选项;Tree 的 #icon、#expandIcon、#fullLabel 不可用。
- Asia
- China
- Beijing
- Shanghai
- Chengdu
- Japan
- Osaka
- North America
- United States
- Canada
- Mexico
- Cuba
代码
<script setup lang="ts">
import { ref } from 'vue';
import { Transfer } from '@fibrous/semi-ui-vue';
const treeData = [
{
label: 'Asia',
value: 'Asia',
key: '0',
children: [
{
label: 'China',
value: 'China',
key: '0-0',
children: [
{ label: 'Beijing', value: 'Beijing', key: '0-0-0' },
{ label: 'Shanghai', value: 'Shanghai', key: '0-0-1' },
{ label: 'Chengdu', value: 'Chengdu', key: '0-0-2' },
],
},
{
label: 'Japan',
value: 'Japan',
key: '0-1',
children: [{ label: 'Osaka', value: 'Osaka', key: '0-1-0' }],
},
],
},
{
label: 'North America',
value: 'North America',
key: '1',
children: [
{ label: 'United States', value: 'United States', key: '1-0' },
{ label: 'Canada', value: 'Canada', key: '1-1' },
{ label: 'Mexico', value: 'Mexico', disabled: true, key: '1-2' },
{ label: 'Cuba', value: 'Cuba', key: '1-3' },
],
},
];
const value = ref<Array<string | number>>(['Shanghai']);
</script>
<template>
<div style="margin: 10px; padding: 10px; width: 600px">
<Transfer v-model="value" :dataSource="treeData" type="treeList" />
</div>
</template>树穿梭框自定义头部显示叶子节点数量
当 type 为 treeList 时,#sourceHeader 的 TransferSourceHeaderSlotProps 参数中会额外提供 leafOnlyNum 字段,表示叶子节点的数量。这在文件选择等场景中非常有用,可以在头部只显示文件数量而不是包含文件夹的总数。
- 文件夹 1
- 文件 1-1
- 文件 1-2
- 子文件夹 1
- 文件 1-1-1
- 文件 1-1-2
- 文件夹 2
- 文件 2-1
- 文件 2-2
- 独立文件
代码
<script setup lang="ts">
import { ref } from 'vue';
import { Transfer, Button } from '@fibrous/semi-ui-vue';
const treeData = [
{
label: '文件夹 1',
value: 'folder1',
key: 'folder1',
children: [
{ label: '文件 1-1', value: 'file1-1', key: 'file1-1' },
{ label: '文件 1-2', value: 'file1-2', key: 'file1-2' },
{
label: '子文件夹 1',
value: 'subfolder1',
key: 'subfolder1',
children: [
{ label: '文件 1-1-1', value: 'file1-1-1', key: 'file1-1-1' },
{ label: '文件 1-1-2', value: 'file1-1-2', key: 'file1-1-2' },
],
},
],
},
{
label: '文件夹 2',
value: 'folder2',
key: 'folder2',
children: [
{ label: '文件 2-1', value: 'file2-1', key: 'file2-1' },
{ label: '文件 2-2', value: 'file2-2', key: 'file2-2' },
],
},
{ label: '独立文件', value: 'file3', key: 'file3' },
];
const value = ref<Array<string | number>>([]);
</script>
<template>
<Transfer v-model="value" :dataSource="treeData" style="width: 600px" type="treeList">
<template #sourceHeader="{ num, leafOnlyNum, showButton, allChecked, onAllClick }">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--semi-color-border)">
<span>
<strong>文件总数:</strong>
{{ leafOnlyNum !== undefined ? leafOnlyNum : num }} 个文件
<span v-if="leafOnlyNum !== undefined" style="color: var(--semi-color-text-2); margin-inline-start: 8px; font-size: 12px"> (总节点数:{{ num }}) </span>
</span>
<Button v-if="showButton" size="small" theme="borderless" type="tertiary" @click="onAllClick">
{{ allChecked ? '取消全选' : '全选' }}
</Button>
</div>
</template>
</Transfer>
</template>Accessibility
ARIA
- 搜索框所在的容器是原生
search元素(search 地标),名称取自语言包 - 根元素带
aria-label或aria-labelledby且未指定role时,自动加rolegroup - 左侧选项列表添加
rolelist,选项为listitem;groupList的每个分组是外层列表的一个listitem,其中分组标题命名嵌套的list,选项在嵌套列表里 - 右侧选中列表添加
rolelist,选中项添加rolelistitem;自定义渲染的左右条目外会包一层rolelistitem的元素
键盘交互
- 默认过滤不区分大小写
- 禁用时左右两个列表是 Tab 停靠点,用来用键盘滚动其中内容
draggable的拖拽句柄是带名称的原生button:Tab 聚焦句柄,空格拿起,方向键移动,空格放下;排序过程会通过读屏播报。鼠标拖拽不变- 拖拽排序所需的 dnd-kit 加载完成前,句柄是带
aria-disabled="true"的占位
API 参考
Transfer
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| modelValue | 已选中项的 value 数组,用 v-model 绑定。传入后即为受控组件,数组顺序就是右侧的显示顺序;传入 undefined 回到非受控。 | TransferValue | undefined |
| defaultValue | 默认已选中的 value 数组,只在挂载时读取。 | TransferValue | semiDefault("Transfer", "defaultValue", []) |
| dataSource | 数据源。type 为 list 时是数据项数组,groupList 时是分组数组,treeList 时是带 children 的树形数据。treeList 的左侧树只在传入新数组时更新,原地修改不会生效。 | TransferDataSource | semiDefault("Transfer", "dataSource", []) |
| type | 穿梭框类型,可选 list、groupList、treeList。 | TransferType | semiDefault("Transfer", "type", "list") |
| disabled | 是否禁用。 | boolean | semiGlobalDefault("Transfer", "disabled") |
| draggable | 是否开启右侧已选项的拖拽排序。排序后 update:modelValue 与 change 的数组顺序随之改变。 | boolean | semiGlobalDefault("Transfer", "draggable") |
| loading | 左侧选项是否正在加载。 | boolean | semiGlobalDefault("Transfer", "loading") |
| showPath | 仅 treeList 有效,控制右侧已选项是否显示从根节点到自身的选择路径。 | boolean | semiDefault("Transfer", "showPath", false) |
| filter | 筛选逻辑。为 false 时不显示搜索框;传入函数时自定义匹配,返回 true 表示保留该项;默认按 label 不区分大小写匹配。treeList 想自定义搜索时需设为 true,并通过 treeProps 的 filterTreeNode 传入匹配函数。 | Typeboolean | ((searchValue: string, item: TransferDataItem) => boolean) | undefined |
| inputProps | 传给搜索框 Input 的属性,可配置项参考 Input;值与 change 由 Transfer 占用,需要外部驱动搜索时调用 search 方法。 | TransferInputProps | semiGlobalDefault("Transfer", "inputProps") |
| treeProps | 仅 treeList 有效,传给左侧 Tree 的属性,会覆盖默认的 multiple、disableStrictly、leafOnly、filterTreeNode、defaultExpandAll。选中值、树数据与 Tree 的引用由 Transfer 决定,modelValue、defaultValue、keyMaps、treeData、treeDataSimpleJson、changeWithObject、change 监听器与 ref 会被忽略;Tree 的插槽只转发 label。 | TransferTreeProps | semiGlobalDefault("Transfer", "treeProps") |
| pagination | 左侧面板的分页配置,仅对 list 与 groupList 生效。onPageChange 在对象内。 | TransferPagination | semiGlobalDefault("Transfer", "pagination") |
| virtualize | 右侧已选列表虚拟化,仅在使用默认右侧面板且 draggable 为 false 时生效。height 为数字时直接生效,为字符串(如 100%)时按面板剩余高度测量。 | TransferVirtualize | semiGlobalDefault("Transfer", "virtualize") |
Events
| 事件 | 说明 | 回调参数 |
|---|---|---|
| update:modelValue | 已选中的值变化时触发,v-model 使用。载荷是 value 数组。 | [values: TransferValue] |
| change | 已选中的值变化时触发,拖拽排序后也会触发。载荷依次为 value 数组与对应的数据项数组,值与 update:modelValue 相同。会遮住根元素上同名的原生事件。 | [values: TransferValue, items: TransferDataItem[]] |
| select | 勾选一项时触发,载荷为该数据项。会遮住根元素上同名的原生事件。 | [item: TransferDataItem] |
| deselect | 取消勾选一项时触发,载荷为该数据项。 | [item: TransferDataItem] |
| search | 搜索框输入内容变化时触发,载荷为搜索值。search 方法驱动的搜索不触发。 | [searchValue: string] |
Slots
| 插槽 | 说明 | 作用域参数 |
|---|---|---|
| label | 数据项的内容,同时用于左侧条目(treeList 为树行)、右侧已选条目与拖拽副本;默认显示数据项的 label。 | TransferLabelSlotProps |
| sourceItem | 完全自定义左侧单个候选项。index 是过滤与分页后可见条目中的下标,不是在 dataSource 中的位置。 | TransferSourceItemSlotProps |
| selectedItem | 完全自定义右侧单个已选项。draggable 时 handle 是拖拽手柄组件,用它包住拖拽图标。treeList 且 showPath 时 fullPath 为完整路径。 | TransferSelectedItemSlotProps |
| sourceHeader | 自定义左侧面板头部。leafOnlyNum 仅 treeList 提供。 | TransferSourceHeaderSlotProps |
| selectedHeader | 自定义右侧面板头部。 | TransferSelectedHeaderSlotProps |
| sourcePanel | 完全自定义左侧候选面板,可复用 Transfer 的筛选与选择逻辑。 | TransferSourcePanelSlotProps |
| selectedPanel | 完全自定义右侧已选面板。onSortEnd 在自行实现拖拽排序结束后调用。 | TransferSelectedPanelSlotProps |
| emptyLeft | 左侧没有数据时的提示。未提供时显示语言包文案,提供空插槽则隐藏该空状态。 | any |
| emptyRight | 右侧没有已选项时的提示。未提供时显示语言包文案,提供空插槽则隐藏该空状态。 | any |
| emptySearch | 搜索没有结果时的提示。未提供时显示语言包文案,提供空插槽则隐藏该空状态。 | any |
Exposes
| 名称 | 说明 | 类型 |
|---|---|---|
| search | 按输入了该值来搜索,该值会被置给搜索框;不触发 search 事件。 | Type(value: string) => void |
class、style、aria-* 等 attrs 落在 Transfer 根元素上;根元素带 aria-label 或 aria-labelledby 且未指定 role 时为 role="group"。搜索框的属性用 inputProps 传入,treeList 左侧 Tree 的属性用 treeProps 传入。
下文各接口在 @fibrous/semi-ui-vue 中导出的类型名为:Item 对应 TransferDataItem,GroupItem 对应 TransferGroupItem,TreeItem 对应 TransferTreeItem,VirtualizeProps 对应 TransferVirtualize,PaginationProps 对应 TransferPagination。
Item Interface
数据项还可以带任意自定义字段,它们会原样出现在插槽参数与事件载荷的 item 上。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| disabled | 是否禁用 | boolean | false |
| key | 必填,每个选项的唯一标识,不允许重复 | string | number | |
| label | 选项展示文本,默认过滤按它匹配;需要其他内容时用 #label 插槽 | string | |
| fullPath | 当 type="treeList" 且 showPath 为 true 时,返回当前节点从根节点到自身的完整路径节点数组 | Array<Item> | |
| value | 选项代表的值 | string | number |
GroupItem Interface
分组本身不是数据项,只有以下属性
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | 该分组的元素 | Array<Item> | |
| title | 分组名称 | string |
TreeItem Interface
TreeItem 继承 Item 的所有属性
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | 子元素 | Array<TreeItem> |
VirtualizeProps Interface
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| height | 虚拟列表高度,传入 number 则直接生效,传入 string(如 100%)则根据剩余高度自适应 | number | string | |
| width | 虚拟列表宽度 | number | string | |
| itemSize | 每行高度(固定) | number |
PaginationProps Interface
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| currentPage | 当前页码(受控模式) | number | |
| defaultCurrentPage | 默认当前页码(非受控模式) | number | 1 |
| pageSize | 每页显示条数 | number | 10 |
| onPageChange | 页码变化时的回调 | (currentPage: number) => void |
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $transition_duration_transfer_item-bg | var(--semi-transition_duration-none) | 穿梭框条目-背景色-动画持续时间 |
| $transition_function_transfer_item-bg | var(--semi-transition_function-easeIn) | 穿梭框条目-背景色-过渡曲线 |
| $transition_delay_transfer_item-bg | var(--semi-transition_delay-none) | 穿梭框条目-背景色-延迟时间 |
| $color-transfer-bg | var(--semi-color-bg-1) | 穿梭框背景颜色 - 默认 |
| $color-transfer-border | var(--semi-color-border) | 穿梭框描边颜色 - 默认 |
| $color-transfer_header-text | var(--semi-color-text-2) | 穿梭框 header 文字颜色 |
| $color-transfer_empty-text | var(--semi-color-text-2) | 穿梭框空状态文字颜色 |
| $color-transfer_group_title-text | var(--semi-color-text-2) | 穿梭框分组标题文字颜色 |
| $color-transfer_disabled-text | var(--semi-color-disabled-text) | 穿梭框禁用文本文字颜色 |
| $color-transfer_close_icon-icon | var(--semi-color-text-2) | 穿梭框关闭按钮颜色 |
| $color-transfer_item-text | var(--semi-color-text-1) | 穿梭框常规文字颜色 |
| $color-transfer_selected_item-text | var(--semi-color-text-0) | 穿梭框选中条目文字颜色 |
| $color-transfer_item-bg-hover | var(--semi-color-fill-0) | 穿梭框条目背景色 - 悬浮 |
| $color-transfer_item-bg-active | var(--semi-color-fill-1) | 穿梭框条目背景色 - 按下 |
| $width-transfer-border | 1px | 穿梭框描边宽度 |
| $width-transfer-minWidth | 402px | 穿梭框整体最小宽度 |
| $height-transfer | 400px | 穿梭框高度 |
| $height-transfer_header | 24px | 穿梭框 header 高度 |
| $height-transfer_item-minHeight | 36px | 穿梭框条目最小高度 |
| $width-transfer_item_close-icon | $width-icon-small | 穿梭框选中条目删除按钮宽度 |
| $width-transfer_left | 50% | 穿梭框左侧面板宽度 |
| $width-transfer_right | 50% | 穿梭框左侧面板宽度 |
| $height-transfer_left_empty | 36px | 穿梭框空状态高度 |
| $width-transfer_left-border | $border-thickness-control | 穿梭框双侧面板分割线宽度 |
| $height-transfer_right_header | 32px | 穿梭框 header 高度 |
| $height-transfer_right_empty | 100% | 穿梭框右侧空状态宽度 |
| $width-transfer_empty | 100% | 穿梭框整体空状态宽度 |
| $height-transfer_group_title | 28px | 穿梭框分组标题高度 |
| $spacing-transfer_header-marginTop | 12px | 穿梭框 header 顶部外边距 |
| $spacing-transfer_header-marginRight | 12px | 穿梭框 header 右侧外边距 |
| $spacing-transfer_header-marginBottom | 8px | 穿梭框 header 底部外边距 |
| $spacing-transfer_header-marginLeft | 12px | 穿梭框 header 左侧外边距 |
| $spacing-transfer_header_all-marginLeft | $spacing-base | 穿梭框全选按钮左侧外边距 |
| $spacing-transfer_item-paddingTop | $spacing-tight | 穿梭框条目顶部内边距 |
| $spacing-transfer_item-paddingRight | 9px | 穿梭框条目右侧内边距 |
| $spacing-transfer_item-paddingBottom | $spacing-tight | 穿梭框条目底部内边距 |
| $spacing-transfer_item-paddingLeft | $spacing-base-tight | 穿梭框条目左侧内边距 |
| $spacing-transfer_right_header-marginTop | $spacing-base-tight | 穿梭框右侧面板 header 顶部外边距 |
| $spacing-transfer_right_header-marginBottom | 0 | 穿梭框右侧面板 header 底部外边距 |
| $spacing-transfer_right_item_drag_handler-marginRight | 4px | 穿梭框拖动图标右侧外边距 |
| $spacing-transfer_filter-marginTop | 12px | 穿梭框搜索顶部外边距 |
| $spacing-transfer_filter-marginRight | 12px | 穿梭框搜索右侧外边距 |
| $spacing-transfer_filter-marginBottom | 0 | 穿梭框搜索底部外边距 |
| $spacing-transfer_filter-marginLeft | 12px | 穿梭框搜索左侧外边距 |
| $spacing-transfer_group_title-paddingLeft | $spacing-base-tight | 穿梭框分组标题左侧内边距 |
| $radius-transfer | var(--semi-border-radius-medium) | 穿梭框圆角 |
| $font-transfer_header_all-fontWeight | 600 | 穿梭框字重 |