diff --git a/components/components.ts b/components/components.ts
index 020c5add2a..0b3f182008 100644
--- a/components/components.ts
+++ b/components/components.ts
@@ -244,3 +244,6 @@ export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from
export { default as Upload, UploadDragger } from './upload';
export { default as LocaleProvider } from './locale-provider';
+
+export type { SegmentedProps } from './segmented';
+export { default as Segmented } from './segmented';
diff --git a/components/segmented/__tests__/__snapshots__/demo.test.js.snap b/components/segmented/__tests__/__snapshots__/demo.test.js.snap
new file mode 100644
index 0000000000..b62795b48f
--- /dev/null
+++ b/components/segmented/__tests__/__snapshots__/demo.test.js.snap
@@ -0,0 +1,204 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders ./components/segmented/demo/basic.vue correctly 1`] = `
+
+
+
+
+
+`;
+
+exports[`renders ./components/segmented/demo/block.vue correctly 1`] = `
+
+
+
+
+
+`;
+
+exports[`renders ./components/segmented/demo/controlled.vue correctly 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`renders ./components/segmented/demo/custom.vue correctly 1`] = `
+
+
+`;
+
+exports[`renders ./components/segmented/demo/disabled.vue correctly 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders ./components/segmented/demo/dynamic.vue correctly 1`] = `
+
+
+
+
+
+
+
+
+`;
+
+exports[`renders ./components/segmented/demo/icon.vue correctly 1`] = `
+
+`;
+
+exports[`renders ./components/segmented/demo/size.vue correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/components/segmented/__tests__/demo.test.js b/components/segmented/__tests__/demo.test.js
new file mode 100644
index 0000000000..6a3124c88b
--- /dev/null
+++ b/components/segmented/__tests__/demo.test.js
@@ -0,0 +1,3 @@
+import demoTest from '../../../tests/shared/demoTest';
+
+demoTest('segmented');
diff --git a/components/segmented/__tests__/index.test.js b/components/segmented/__tests__/index.test.js
new file mode 100644
index 0000000000..161bbefb2e
--- /dev/null
+++ b/components/segmented/__tests__/index.test.js
@@ -0,0 +1,11 @@
+import { mount } from '@vue/test-utils';
+import Segmented from '../index';
+describe('Segmented', () => {
+ const wrapper = mount({
+ render() {
+ return ;
+ },
+ });
+ const todo = wrapper.get('[options="[1,2,3,4,5]"]');
+ expect(todo.text()).toBe('segmented');
+});
diff --git a/components/segmented/demo/basic.vue b/components/segmented/demo/basic.vue
new file mode 100644
index 0000000000..2294600115
--- /dev/null
+++ b/components/segmented/demo/basic.vue
@@ -0,0 +1,32 @@
+
+---
+order: 0
+title:
+ zh-CN: 基本用法
+ en-US: Basic Usage
+---
+
+## zh-CN
+
+最简单的用法。
+
+## en-US
+The most basic usage.
+
+
+
+
+
+
+
diff --git a/components/segmented/demo/block.vue b/components/segmented/demo/block.vue
new file mode 100644
index 0000000000..4a94c9e9eb
--- /dev/null
+++ b/components/segmented/demo/block.vue
@@ -0,0 +1,31 @@
+
+---
+order: 1
+title:
+ zh-CN: Block分段控制器
+ en-US: Block Segmented
+---
+
+## zh-CN
+
+`block` 属性使其适合父元素宽度。
+
+## en-US
+`block` property will make the `Segmented` fit to its parent width.
+
+
+
+
+
+
diff --git a/components/segmented/demo/controlled.vue b/components/segmented/demo/controlled.vue
new file mode 100644
index 0000000000..6023150480
--- /dev/null
+++ b/components/segmented/demo/controlled.vue
@@ -0,0 +1,35 @@
+
+---
+order: 4
+title:
+ zh-CN: 受控模式
+ en-US: Controlled mode
+---
+
+## zh-CN
+
+受控的 Segmented
+
+## en-US
+Controlled Segmented.
+
+
+
+
+
+
diff --git a/components/segmented/demo/custom.vue b/components/segmented/demo/custom.vue
new file mode 100644
index 0000000000..09165d99d0
--- /dev/null
+++ b/components/segmented/demo/custom.vue
@@ -0,0 +1,94 @@
+
+---
+order: 5
+title:
+ zh-CN: 自定义渲染
+ en-US: Custom
+---
+
+## zh-CN
+
+自定义渲染每一个 Segmented Item。
+
+## en-US
+Custom each Segmented Item.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/segmented/demo/disabled.vue b/components/segmented/demo/disabled.vue
new file mode 100644
index 0000000000..ac7ed75ccc
--- /dev/null
+++ b/components/segmented/demo/disabled.vue
@@ -0,0 +1,44 @@
+
+---
+order: 3
+title:
+ zh-CN: 不可用
+ en-US: Disabled
+---
+
+## zh-CN
+
+Segmented 不可用。
+
+## en-US
+Disabled Segmented.
+
+
+
+
+
+
diff --git a/components/segmented/demo/dynamic.vue b/components/segmented/demo/dynamic.vue
new file mode 100644
index 0000000000..b888fa3d2b
--- /dev/null
+++ b/components/segmented/demo/dynamic.vue
@@ -0,0 +1,40 @@
+
+---
+order: 6
+title:
+ zh-CN: 动态数据
+ en-US: Dynamic
+---
+
+## zh-CN
+
+动态加载数据。
+
+## en-US
+Load dynamically.
+
+
+
+
+
+ Load More
+
+
+
diff --git a/components/segmented/demo/icon.vue b/components/segmented/demo/icon.vue
new file mode 100644
index 0000000000..b486851583
--- /dev/null
+++ b/components/segmented/demo/icon.vue
@@ -0,0 +1,49 @@
+
+---
+order: 7
+title:
+ zh-CN: 设置图标
+ en-US: With Icon
+---
+
+## zh-CN
+
+给 Segmented Item 设置 Icon。
+
+## en-US
+Set `icon` for Segmented Item.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/segmented/demo/index.vue b/components/segmented/demo/index.vue
new file mode 100644
index 0000000000..a43e7e60c7
--- /dev/null
+++ b/components/segmented/demo/index.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/segmented/demo/size.vue b/components/segmented/demo/size.vue
new file mode 100644
index 0000000000..b22197f1aa
--- /dev/null
+++ b/components/segmented/demo/size.vue
@@ -0,0 +1,37 @@
+
+---
+order: 6
+title:
+ zh-CN: 三种大小
+ en-US: Three sizes of Segmented
+---
+
+## zh-CN
+
+我们为 `` 组件定义了三种尺寸(大、默认、小),高度分别为 `40px`、`32px` 和 `24px`。
+
+## en-US
+There are three sizes of an a-segmented: `large` (40px), `default` (32px) and `small` (24px).
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/segmented/index.en-US.md b/components/segmented/index.en-US.md
new file mode 100644
index 0000000000..9a22d29f86
--- /dev/null
+++ b/components/segmented/index.en-US.md
@@ -0,0 +1,26 @@
+---
+category: Components
+type: Data Display
+title: Segmented
+---
+
+Segmented Controls.
+
+### When To Use
+
+- When displaying multiple options and user can select a single option;
+- When switching the selected option, the content of the associated area changes.
+
+## API
+
+### Segmented
+
+| Property | Description | Type | Default | Version |
+| --- | --- | --- | --- | --- |
+| block | Option to fit width to its parent\'s width | boolean | false | |
+| defaultValue | Default selected value | string \| number | | |
+| disabled | Disable all segments | boolean | false | |
+| change | The callback function that is triggered when the state changes | function(value: string \| number) | | |
+| options | Set children optional | string[] \| number[] \| Array<{ value?: string disabled?: boolean }> | [] | |
+| size | The size of the Segmented. | `large` \| `middle` \| `small` | - | |
+| value | Currently selected value | string \| number | | |
diff --git a/components/segmented/index.ts b/components/segmented/index.ts
new file mode 100644
index 0000000000..02d21bc86b
--- /dev/null
+++ b/components/segmented/index.ts
@@ -0,0 +1,10 @@
+import type { App } from 'vue';
+import Segmented from './src';
+import type { SegmentedProps } from './src';
+
+Segmented.install = function (app: App) {
+ app.component(Segmented.name, Segmented);
+ return app;
+};
+export default Segmented;
+export type { SegmentedProps };
diff --git a/components/segmented/index.zh-CN.md b/components/segmented/index.zh-CN.md
new file mode 100644
index 0000000000..70faa863a7
--- /dev/null
+++ b/components/segmented/index.zh-CN.md
@@ -0,0 +1,27 @@
+---
+category: Components
+subtitle: 分段控制器
+type: 数据展示
+title: Segmented
+---
+
+分段控制器。
+
+## 何时使用
+
+- 用于展示多个选项并允许用户选择其中单个选项;
+- 当切换选中选项时,关联区域的内容会发生变化。
+
+## API
+
+### Segmented
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+| --- | --- | --- | --- | --- |
+| block | 将宽度调整为父元素宽度的选项 | boolean | 无 | |
+| defaultValue | 默认选中的值 | string \| number | | |
+| disabled | 是否禁用 | boolean | false | |
+| change | 选项变化时的回调函数 | function(value: string \| number) | | |
+| options | 数据化配置选项内容 | string[] \| number[] \| Array<{ value?: string disabled?: boolean }> | [] | |
+| size | 控件尺寸 | `large` \| `middle` \| `small` | - | |
+| value | 当前选中的值 | string \| number | | |
diff --git a/components/segmented/src/index.ts b/components/segmented/src/index.ts
new file mode 100644
index 0000000000..4c8b46ef8f
--- /dev/null
+++ b/components/segmented/src/index.ts
@@ -0,0 +1,5 @@
+import Segmented from './segmented';
+import type { SegmentedProps } from './segmented';
+
+export type { SegmentedProps };
+export default Segmented;
diff --git a/components/segmented/src/segmented.tsx b/components/segmented/src/segmented.tsx
new file mode 100644
index 0000000000..1da88d0859
--- /dev/null
+++ b/components/segmented/src/segmented.tsx
@@ -0,0 +1,150 @@
+import { defineComponent, ref, toRefs, reactive, watch } from 'vue';
+import type { ExtractPropTypes, PropType } from 'vue';
+import classNames from '../../_util/classNames';
+import useConfigInject from '../../_util/hooks/useConfigInject';
+import { getPropsSlot, initDefaultProps } from '../../_util/props-util';
+export type segmentedSize = 'large' | 'small';
+export interface SegmentedOptions {
+ value?: string;
+ disabled?: boolean;
+}
+export const segmentedProps = () => {
+ return {
+ options: { type: Array as PropType> },
+ defaultValue: { type: [Number, String] },
+ block: Boolean,
+ disabled: Boolean,
+ size: { type: String as PropType },
+ };
+};
+export type SegmentedProps = Partial>>;
+export default defineComponent({
+ name: 'ASegmented',
+ inheritAttrs: false,
+ props: { ...initDefaultProps(segmentedProps(), {}) },
+ emits: ['change', 'value'],
+ slots: ['icon', 'title'],
+ setup(props, { emit, slots }) {
+ const { prefixCls } = useConfigInject('segmented', props);
+ const pre = prefixCls.value;
+ const { size } = toRefs(props);
+ const itemRef = ref([]);
+ const { options, disabled, defaultValue } = toRefs(props);
+ const segmentedItemInput = () => {
+ return ;
+ };
+ const isDisabled = item => {
+ if (disabled.value || (typeof item == 'object' && item.disabled)) {
+ return segmentedItemInput();
+ }
+ };
+ const currentItemKey = ref();
+ currentItemKey.value = defaultValue.value ? defaultValue.value : 0;
+ const toPX = (value: number) => (value !== undefined ? `${value}px` : undefined);
+ // 开始 or 停止
+ const thumbShow = ref(true);
+ const mergedStyle = reactive({
+ startLeft: '',
+ startWidth: '',
+ activeLeft: '',
+ activeWidth: '',
+ });
+ const handleSelectedChange = (item, index) => {
+ if (disabled.value || item.disabled) return;
+ currentItemKey.value = index;
+ emit('change', { value: item, key: index });
+ };
+ const icon = getPropsSlot(slots, props, 'icon');
+ const title = getPropsSlot(slots, props, 'title');
+ const iconNode = index => {
+ return icon ? (
+ {slots.icon?.(index)}
+ ) : (
+ ''
+ );
+ };
+ const itemNode = (item, index) => {
+ if (title) {
+ return {slots.title?.(index)}
;
+ }
+ return {item.value};
+ };
+ const calcThumbStyle = index => {
+ return {
+ left: itemRef.value[index].children[0].offsetParent.offsetLeft,
+ width: itemRef.value[index].children[0].clientWidth,
+ };
+ };
+ const thumbStyle = reactive({
+ transform: '',
+ width: '',
+ });
+ const isValueType = item => {
+ return item instanceof Object ? (item.disabled ? true : false) : false;
+ };
+ watch(
+ () => currentItemKey.value,
+ (newValue, oldValue) => {
+ const prev = oldValue ? oldValue : defaultValue.value ? defaultValue.value : 0;
+ const next = newValue;
+ const calcPrevStyle = calcThumbStyle(prev);
+ const calcNextStyle = calcThumbStyle(next);
+ mergedStyle.startLeft = toPX(calcPrevStyle.left);
+ mergedStyle.startWidth = toPX(calcPrevStyle.width);
+ mergedStyle.activeLeft = toPX(calcNextStyle.left);
+ mergedStyle.activeWidth = toPX(calcNextStyle.width);
+ if (prev !== next) {
+ thumbStyle.transform = `translateX(${mergedStyle.activeLeft})`;
+ thumbStyle.width = `${mergedStyle.activeWidth}`;
+ }
+ },
+ );
+ const thumbNode = () => {
+ return thumbShow.value ? (
+
+ ) : (
+ ''
+ );
+ };
+ return () => {
+ return (
+
+
+ {thumbNode()}
+ {options.value.map((item, index) => {
+ return (
+
+ );
+ })}
+
+
+ );
+ };
+ },
+});
diff --git a/components/segmented/style/index.less b/components/segmented/style/index.less
new file mode 100644
index 0000000000..6e88b3cc3b
--- /dev/null
+++ b/components/segmented/style/index.less
@@ -0,0 +1,148 @@
+@import '../../style/themes/index';
+@import '../../style/mixins/index';
+@segmented-prefix-cls: ~'@{ant-prefix}-segmented';
+.@{segmented-prefix-cls} {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 2px;
+ color: rgba(0, 0, 0, 0.65);
+ font-size: 14px;
+ line-height: 1.5714285714285714;
+ list-style: none;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
+ 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+ display: inline-block;
+ background-color: #f5f5f5;
+ border-radius: 6px;
+ transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+ &-group {
+ position: relative;
+ display: flex;
+ align-items: stretch;
+ justify-items: flex-start;
+ width: 100%;
+ }
+ &-rtl {
+ direction: rtl;
+ }
+ &-block {
+ display: flex;
+ width: 100%;
+ .@{segmented-prefix-cls}-item {
+ flex: 1;
+ min-width: 0;
+ }
+ }
+ &-item {
+ position: relative;
+ text-align: center;
+ cursor: pointer;
+ transition: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+ border-radius: 4px;
+ user-select: none;
+ &::after {
+ content: '';
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ inset-inline-start: 0;
+ border-radius: 4px;
+ transition: background-color 0.2s;
+ }
+ &:hover:not(.@{segmented-prefix-cls}-item-selected):not(
+ .@{segmented-prefix-cls}-item-disabled
+ ) {
+ color: rgba(0, 0, 0, 0.88);
+ }
+ &:hover:not(.@{segmented-prefix-cls}-item-selected):not(
+ .@{segmented-prefix-cls}-item-disabled
+ )::after {
+ background-color: rgba(0, 0, 0, 0.06);
+ }
+ }
+ &-item-selected {
+ background-color: #ffffff;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02),
+ 0 2px 4px 0 rgba(0, 0, 0, 0.02);
+ color: rgba(0, 0, 0, 0.88);
+ }
+ &-item-label {
+ min-height: 28px;
+ line-height: 28px;
+ padding: 0 11px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ &-item-icon {
+ margin-inline-end: 6px;
+ }
+ &-item-input {
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ pointer-events: none;
+ }
+ &-lg {
+ border-radius: 8px;
+ .@{segmented-prefix-cls}-item-label {
+ min-height: 36px;
+ line-height: 36px;
+ padding: 0 11px;
+ font-size: 16px;
+ }
+ .@{segmented-prefix-cls}-item-selected {
+ border-radius: 6px;
+ }
+ }
+ &-sm {
+ border-radius: 4px;
+ .@{segmented-prefix-cls}-item-label {
+ min-height: 20px;
+ line-height: 20px;
+ padding: 0 7px;
+ }
+ .@{segmented-prefix-cls}-item-selected {
+ border-radius: 2px;
+ }
+ }
+ &-disabled &-item,
+ &-disabled &-item:hover,
+ &-disabled &-item:focus {
+ color: rgba(0, 0, 0, 0.25);
+ cursor: not-allowed;
+ }
+ &-item-disabled,
+ &-item-disabled:hover,
+ &-item-disabled:focus {
+ color: rgba(0, 0, 0, 0.25);
+ cursor: not-allowed;
+ }
+ &-thumb {
+ background-color: #ffffff;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02),
+ 0 2px 4px 0 rgba(0, 0, 0, 0.02);
+ position: absolute;
+ inset-block-start: 0;
+ inset-inline-start: 0;
+ width: 0;
+ height: 100%;
+ padding: 4px 0;
+ border-radius: 4px;
+ .@{segmented-prefix-cls}-item:not(.@{segmented-prefix-cls}-item-selected):not(
+ .@{segmented-prefix-cls}-item-disabled
+ )::after {
+ background-color: #ffffff;
+ }
+ }
+ &-thumb-motion-appear-active {
+ transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
+ width 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+ will-change: transform, width;
+ }
+}
diff --git a/components/segmented/style/index.tsx b/components/segmented/style/index.tsx
new file mode 100644
index 0000000000..d74e52ee9f
--- /dev/null
+++ b/components/segmented/style/index.tsx
@@ -0,0 +1 @@
+import './index.less';
diff --git a/components/style.ts b/components/style.ts
index 88bd444b59..d4390302fa 100644
--- a/components/style.ts
+++ b/components/style.ts
@@ -62,3 +62,4 @@ import './space/style';
import './image/style';
import './typography/style';
// import './color-picker/style';
+import './segmented/style';
diff --git a/site/src/demo.js b/site/src/demo.js
index ff3209fa1f..b249c06118 100644
--- a/site/src/demo.js
+++ b/site/src/demo.js
@@ -395,4 +395,10 @@ export default {
// type: 'Data Entry',
// title: 'ColorPicker (Beta)',
// },
+ segmented: {
+ category: 'Components',
+ subtitle: '分段控制器',
+ type: 'Data Display',
+ title: 'Segmented',
+ },
};