diff --git a/examples/sites/demos/apis/tag-input.js b/examples/sites/demos/apis/tag-input.js
new file mode 100644
index 0000000000..1b2f7dc5b5
--- /dev/null
+++ b/examples/sites/demos/apis/tag-input.js
@@ -0,0 +1,182 @@
+export default {
+ mode: ['pc', 'mobile-first'],
+ apis: [
+ {
+ name: 'tag-input',
+ type: 'component',
+ props: [
+ {
+ name: 'model-value / v-model',
+ type: 'array',
+ defaultValue: '[]',
+ desc: {
+ 'zh-CN': '绑定值',
+ 'en-US': 'Binding Value'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'tag-group-size',
+ mfDemo: 'tag-group-size'
+ },
+ {
+ name: 'size',
+ type: "'medium' | 'small'",
+ defaultValue: "'medium'",
+ desc: {
+ 'zh-CN': '尺寸',
+ 'en-US': 'Size '
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'tag-group-size',
+ mfDemo: 'tag-group-size'
+ },
+ {
+ name: 'tag-type',
+ typeAnchorName: 'IType',
+ type: 'IType',
+ defaultValue: '',
+ desc: {
+ 'zh-CN': '显示类型',
+ 'en-US': 'Display type'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ },
+ {
+ name: 'tag-effect',
+ typeAnchorName: 'IEffect',
+ type: 'IEffect',
+ defaultValue: "'light'",
+ desc: {
+ 'zh-CN': '主题',
+ 'en-US': 'Theme Color'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'effect',
+ mfDemo: ''
+ },
+ {
+ name: 'clearable',
+ type: 'boolean',
+ defaultValue: 'false',
+ desc: {
+ 'zh-CN': '是否可清空',
+ 'en-US': 'Whether it can be cleared'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ },
+ {
+ name: 'disabled',
+ type: 'boolean',
+ defaultValue: 'false',
+ desc: {
+ 'zh-CN': '是否禁用标签输入框',
+ 'en-US': 'Whether the tag input box is disabled'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ },
+ {
+ name: 'max',
+ type: 'number',
+ defaultValue: 'Infinity',
+ desc: {
+ 'zh-CN': '最大允许输入的标签数量',
+ 'en-US': 'Maximum number of tags allowed to be input'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ },
+ {
+ name: 'placeholder',
+ type: 'string',
+ defaultValue: '',
+ desc: {
+ 'zh-CN': '占位符',
+ 'en-US': 'Placeholder'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ },
+ {
+ name: 'readonly',
+ type: 'boolean',
+ defaultValue: 'false',
+ desc: {
+ 'zh-CN': '是否只读',
+ 'en-US': 'Whether it is read-only'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ },
+ {
+ name: 'draggable',
+ type: 'boolean',
+ defaultValue: 'false',
+ desc: {
+ 'zh-CN': '是否可拖拽',
+ 'en-US': 'Whether it is draggable'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ },
+ {
+ name: 'minCollapsedNum',
+ type: 'number',
+ defaultValue: 'Infinity',
+ desc: {
+ 'zh-CN': '最小折叠数量',
+ 'en-US': 'Minimum collapsed number'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ },
+ {
+ name: 'separator',
+ type: 'string',
+ defaultValue: ',',
+ desc: {
+ 'zh-CN': '批量输入时标签分隔符',
+ 'en-US': 'Tag separator for batch input'
+ },
+ mode: ['pc', 'mobile-first'],
+ pcDemo: 'basic-usage',
+ mfDemo: ''
+ }
+ ],
+ events: [],
+ methods: [],
+ slots: [
+ {
+ name: 'prefix',
+ defaultValue: '',
+ desc: {
+ 'zh-CN': '输入框前缀内容的插槽',
+ 'en-US': 'Input prefix slot'
+ },
+ mode: ['pc'],
+ pcDemo: 'basic-usage'
+ },
+ {
+ name: 'suffix',
+ defaultValue: '',
+ desc: {
+ 'zh-CN': '输入框后缀内容的插槽',
+ 'en-US': 'Input suffix slot'
+ },
+ mode: ['pc'],
+ pcDemo: 'basic-usage'
+ }
+ ]
+ }
+ ],
+ types: []
+}
diff --git a/examples/sites/demos/pc/app/tag-input/basic-usage-composition-api.vue b/examples/sites/demos/pc/app/tag-input/basic-usage-composition-api.vue
new file mode 100644
index 0000000000..cdac046c83
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/basic-usage-composition-api.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/basic-usage.vue b/examples/sites/demos/pc/app/tag-input/basic-usage.vue
new file mode 100644
index 0000000000..292b6e6914
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/basic-usage.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/clearable-tag-composition-api.vue b/examples/sites/demos/pc/app/tag-input/clearable-tag-composition-api.vue
new file mode 100644
index 0000000000..9de862dcff
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/clearable-tag-composition-api.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/clearable-tag.vue b/examples/sites/demos/pc/app/tag-input/clearable-tag.vue
new file mode 100644
index 0000000000..6d62be65de
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/clearable-tag.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/collapsed-tag-composition-api.vue b/examples/sites/demos/pc/app/tag-input/collapsed-tag-composition-api.vue
new file mode 100644
index 0000000000..447cea8f59
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/collapsed-tag-composition-api.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/collapsed-tag.vue b/examples/sites/demos/pc/app/tag-input/collapsed-tag.vue
new file mode 100644
index 0000000000..d7a54710b0
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/collapsed-tag.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/disabled-readonly-composition-api.vue b/examples/sites/demos/pc/app/tag-input/disabled-readonly-composition-api.vue
new file mode 100644
index 0000000000..6ef936c1c8
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/disabled-readonly-composition-api.vue
@@ -0,0 +1,20 @@
+
+
+ disabled:
+ readonly:
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/disabled-readonly.vue b/examples/sites/demos/pc/app/tag-input/disabled-readonly.vue
new file mode 100644
index 0000000000..076c34f235
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/disabled-readonly.vue
@@ -0,0 +1,29 @@
+
+
+ disabled:
+ readonly:
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/draggable-tag-composition-api.vue b/examples/sites/demos/pc/app/tag-input/draggable-tag-composition-api.vue
new file mode 100644
index 0000000000..92c909a8c2
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/draggable-tag-composition-api.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/draggable-tag.spec.ts b/examples/sites/demos/pc/app/tag-input/draggable-tag.spec.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/sites/demos/pc/app/tag-input/draggable-tag.vue b/examples/sites/demos/pc/app/tag-input/draggable-tag.vue
new file mode 100644
index 0000000000..eeaa58b2b5
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/draggable-tag.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/max-composition-api.vue b/examples/sites/demos/pc/app/tag-input/max-composition-api.vue
new file mode 100644
index 0000000000..70d7409a9e
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/max-composition-api.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/max.vue b/examples/sites/demos/pc/app/tag-input/max.vue
new file mode 100644
index 0000000000..f1316aa4e5
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/max.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/prefix-suffix-composition-api.vue b/examples/sites/demos/pc/app/tag-input/prefix-suffix-composition-api.vue
new file mode 100644
index 0000000000..30511bcb73
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/prefix-suffix-composition-api.vue
@@ -0,0 +1,26 @@
+
+
+
+
+ prefix:
+
+
+ suffix
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/prefix-suffix.vue b/examples/sites/demos/pc/app/tag-input/prefix-suffix.vue
new file mode 100644
index 0000000000..589de51d1b
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/prefix-suffix.vue
@@ -0,0 +1,34 @@
+
+
+
+
+ prefix:
+
+
+ suffix
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/separator-tag-composition-api.vue b/examples/sites/demos/pc/app/tag-input/separator-tag-composition-api.vue
new file mode 100644
index 0000000000..887459f8c5
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/separator-tag-composition-api.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/separator-tag.vue b/examples/sites/demos/pc/app/tag-input/separator-tag.vue
new file mode 100644
index 0000000000..8c9492f427
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/separator-tag.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.cn.md b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.cn.md
new file mode 100644
index 0000000000..f9c21238c8
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.cn.md
@@ -0,0 +1,7 @@
+---
+title: TagInput 标签输入框
+---
+
+# TagInput 标签输入框
+
+
用于输入标签。
diff --git a/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.en.md b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.en.md
new file mode 100644
index 0000000000..a0b36160c2
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.en.md
@@ -0,0 +1,7 @@
+---
+title: TagInput
+---
+
+# TagInput
+
+Used to enter the label.
diff --git a/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.js b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.js
new file mode 100644
index 0000000000..7b6b8d00ec
--- /dev/null
+++ b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.js
@@ -0,0 +1,107 @@
+export default {
+ column: '2',
+ owner: '',
+ meta: {
+ experimental: '3.29.0'
+ },
+ show: true,
+ cloud: true,
+ demos: [
+ {
+ demoId: 'basic-usage',
+ name: {
+ 'zh-CN': '基本用法',
+ 'en-US': 'basic usage'
+ },
+ desc: {
+ 'zh-CN': `按enter回车键添加标签,按backspace删除最后一个标签。
`,
+ 'en-US': `Press Enter to add a tag, and press Backspace to delete the last one.
`
+ },
+ codeFiles: ['basic-usage.vue']
+ },
+ {
+ demoId: 'disabled-readonly',
+ name: {
+ 'zh-CN': '禁用与只读',
+ 'en-US': 'basic usage'
+ },
+ desc: {
+ 'zh-CN': `你可以设置TagInput被禁用或者只读。
`,
+ 'en-US': `You can set the TagInput to be disabled or readonly.
`
+ },
+ codeFiles: ['disabled-readonly.vue']
+ },
+ {
+ demoId: 'max-tag',
+ name: {
+ 'zh-CN': '最大标签数',
+ 'en-US': 'basic usage'
+ },
+ desc: {
+ 'zh-CN': `您可以设置添加标签的数量限制。
`,
+ 'en-US': `You can set a limit on the number of tags to add.
`
+ },
+ codeFiles: ['max.vue']
+ },
+ {
+ demoId: 'collapsed-tag',
+ name: {
+ 'zh-CN': '折叠标签',
+ 'en-US': 'basic usage'
+ },
+ desc: {
+ 'zh-CN': `通过设置minCollapsedTags属性,可以控制折叠标签的数量,超过部分将以+N的形式显示。
`,
+ 'en-US': `By setting the minCollapsedTags property, you can control the number of collapsed tags, with the excess displayed in a "+N" format.
`
+ },
+ codeFiles: ['collapsed-tag.vue']
+ },
+ {
+ demoId: 'clearable-tag',
+ name: {
+ 'zh-CN': '可清空标签',
+ 'en-US': 'basic usage'
+ },
+ desc: {
+ 'zh-CN': `通过设置clearable属性,可以控制标签是否可清空。
`,
+ 'en-US': `By setting the clearable attribute, you can control whether the tag is removable.
`
+ },
+ codeFiles: ['clearable-tag.vue']
+ },
+ {
+ demoId: 'separator-tag',
+ name: {
+ 'zh-CN': '分隔符输入标签',
+ 'en-US': 'basic usage'
+ },
+ desc: {
+ 'zh-CN': `可以通过设置分隔符separator来实现批量输入。
`,
+ 'en-US': `You can achieve batch input by setting the separator parameter.
`
+ },
+ codeFiles: ['separator-tag.vue']
+ },
+ {
+ demoId: 'prefix-suffix',
+ name: {
+ 'zh-CN': '自定义前后缀',
+ 'en-US': 'basic usage'
+ },
+ desc: {
+ 'zh-CN': `可以通过设置prefix和suffix属性来自定义前后缀。
`,
+ 'en-US': `You can customize the prefix and suffix by setting the prefix and suffix attributes.
`
+ },
+ codeFiles: ['prefix-suffix.vue']
+ },
+ {
+ demoId: 'draggable-tag',
+ name: {
+ 'zh-CN': '可拖拽标签',
+ 'en-US': 'basic usage'
+ },
+ desc: {
+ 'zh-CN': `可以通过设置drag属性来实现标签的拖拽功能。
`,
+ 'en-US': `You can enable the drag functionality of tags by setting the drag attribute.
`
+ },
+ codeFiles: ['draggable-tag.vue']
+ }
+ ]
+}
diff --git a/examples/sites/demos/pc/menus.js b/examples/sites/demos/pc/menus.js
index 99f233c8c7..8da0c95f13 100644
--- a/examples/sites/demos/pc/menus.js
+++ b/examples/sites/demos/pc/menus.js
@@ -175,6 +175,7 @@ export const cmpMenus = [
{ 'nameCn': '输入框', 'name': 'Input', 'key': 'input' },
{ 'nameCn': ' IP地址输入框', 'name': 'IpAddress', 'key': 'ip-address' },
{ 'nameCn': '数字输入框', 'name': 'Numeric', 'key': 'numeric' },
+ { 'nameCn': '标签输入框', 'name': 'TagInput', 'key': 'tag-input', 'meta': { 'experimental': '3.29.0' } },
{ 'nameCn': '弹出编辑', 'name': 'PopEditor', 'key': 'popeditor' },
{ 'nameCn': '弹出上传', 'name': 'PopUpload', 'key': 'pop-upload' },
{ 'nameCn': '单选框', 'name': 'Radio', 'key': 'radio' },
diff --git a/packages/modules.json b/packages/modules.json
index 34eeb4fb95..f417e31d12 100644
--- a/packages/modules.json
+++ b/packages/modules.json
@@ -2194,6 +2194,25 @@
"type": "template",
"exclude": false
},
+ "TagInput": {
+ "path": "vue/src/tag-input/index.ts",
+ "type": "component",
+ "exclude": false,
+ "mode": [
+ "mobile-first",
+ "pc"
+ ]
+ },
+ "TagInputPc": {
+ "path": "vue/src/tag-input/src/pc.vue",
+ "type": "template",
+ "exclude": false
+ },
+ "TagInputMobileFirst": {
+ "path": "vue/src/tag-input/src/mobile-first.vue",
+ "type": "template",
+ "exclude": false
+ },
"TextPopup": {
"path": "vue/src/text-popup/index.ts",
"type": "component",
diff --git a/packages/renderless/src/tag-input/index.ts b/packages/renderless/src/tag-input/index.ts
new file mode 100644
index 0000000000..295ade3c20
--- /dev/null
+++ b/packages/renderless/src/tag-input/index.ts
@@ -0,0 +1,111 @@
+import type { ITagInputRenderlessParams } from '@/types'
+
+export const addTag =
+ ({ emit, props, state }: Pick) =>
+ () => {
+ const value = state.currentValue.trim()
+ if (!value) {
+ return
+ }
+
+ if (state.tagList.length >= props.max) {
+ state.currentValue = ''
+ return
+ }
+
+ const tags = [...(state.tagList || [])]
+ let newTags = [value]
+ if (props.separator !== undefined) {
+ newTags = value.split(props.separator).filter((val) => val)
+ }
+
+ tags.push(...newTags)
+ state.tagList = tags
+ emit('update:modelValue', tags)
+ state.currentValue = ''
+ }
+
+export const removeTag =
+ ({ emit, props, state }: Pick) =>
+ (index: number) => {
+ state.tagList.splice(index, 1)
+ emit('update:modelValue', state.tagList)
+ }
+
+export const handleBackspace =
+ ({ emit, props, state }: Pick) =>
+ () => {
+ if (state.currentValue === '') {
+ state.tagList.pop()
+ emit('update:modelValue', state.tagList)
+ }
+ }
+
+export const handleClear =
+ ({ emit, props, state }: Pick) =>
+ () => {
+ state.tagList = []
+ emit('update:modelValue', state.tagList)
+ state.currentValue = ''
+ }
+
+export const handleMouseOver =
+ ({ state }: Pick) =>
+ (event: MouseEvent) => {
+ state.isHovering = true
+ }
+
+export const handleMouseLeave =
+ ({ state }: Pick) =>
+ () => {
+ state.isHovering = false
+ }
+
+export const handleInputFocus =
+ ({ state }: Pick) =>
+ () => {
+ state.isFocused = true
+ }
+
+export const handleInputBlur =
+ ({ state }: Pick) =>
+ () => {
+ state.isFocused = false
+ }
+
+export const handleDragStart =
+ ({ state }: Pick) =>
+ (index: number, event: DragEvent) => {
+ state.draggingIndex = index
+ if (event.target) {
+ event.dataTransfer?.setData('text/plain', event.target)
+ event.dataTransfer!.effectAllowed = 'move'
+ }
+ }
+
+export const handleDragOver = () => (index: number, event: DragEvent) => {
+ event.preventDefault()
+ event.dataTransfer!.dropEffect = 'move'
+}
+
+export const handleDragEnter =
+ ({ state, emit }: Pick) =>
+ (index: number, event: DragEvent) => {
+ event.preventDefault()
+ if (index === state.draggingIndex) return
+ state.dragTargetIndex = index
+ }
+
+export const handleDrop =
+ ({ emit, props, state }: Pick) =>
+ (index: number, event: DragEvent) => {
+ event.preventDefault()
+ const newTags = [...(state.tagList ?? [])]
+ const draggingTag = newTags[state.draggingIndex]
+ newTags.splice(state.draggingIndex, 1)
+ newTags.splice(state.dragTargetIndex, 0, draggingTag)
+ state.draggingIndex = null
+ state.dragTargetIndex = null
+ state.tagList = newTags
+ emit('update:modelValue', newTags)
+ }
diff --git a/packages/renderless/src/tag-input/vue.ts b/packages/renderless/src/tag-input/vue.ts
new file mode 100644
index 0000000000..6a15ee729e
--- /dev/null
+++ b/packages/renderless/src/tag-input/vue.ts
@@ -0,0 +1,82 @@
+import type { ITagInputApi, ITagInputState, ISharedRenderlessParamUtils, ISharedRenderlessParamHooks } from '@/types'
+import {
+ addTag,
+ removeTag,
+ handleBackspace,
+ handleClear,
+ handleMouseLeave,
+ handleMouseOver,
+ handleInputFocus,
+ handleInputBlur,
+ handleDragEnter,
+ handleDragStart,
+ handleDragOver,
+ handleDrop
+} from './index'
+
+export const api = [
+ 'addTag',
+ 'removeTag',
+ 'state',
+ 'handleBackspace',
+ 'handleClear',
+ 'handleMouseLeave',
+ 'handleMouseOver',
+ 'handleInputFocus',
+ 'handleInputBlur',
+ 'handleDragStart',
+ 'handleDragOver',
+ 'handleDragEnter',
+ 'handleDrop'
+]
+
+export const renderless = (
+ props,
+ { reactive, computed, ref }: ISharedRenderlessParamHooks,
+ { emit, parent }: ISharedRenderlessParamUtils
+): ITagInputApi => {
+ const state: ITagInputState = reactive({
+ currentValue: '',
+ tagList: props.modelValue || [],
+ disabled: computed(() => props.disabled),
+ closeable: computed(() => !props.readonly && !props.disabled),
+ showClearIcon: computed(() => {
+ return (
+ props.clearable &&
+ !props.readonly &&
+ !props.disabled &&
+ (state.isHovering || state.isFocused) &&
+ ((props.modelValue || []).length > 0 || state.currentValue)
+ )
+ }),
+ showTagList: computed(() => {
+ const limit = props.minCollapsedNum < props.max ? props.minCollapsedNum : props.max
+ return (state.tagList || []).slice(0, limit)
+ }),
+ collapsedTagList: computed(() => {
+ return props.minCollapsedNum < props.max ? (state.tagList || []).slice(props.minCollapsedNum) : []
+ }),
+ isHovering: false,
+ isFocused: false,
+ draggingIndex: null,
+ dragTargetIndex: null
+ })
+
+ const api: ITagInputApi = {
+ state,
+ addTag: addTag({ emit, props, state }),
+ removeTag: removeTag({ emit, props, state }),
+ handleBackspace: handleBackspace({ emit, props, state }),
+ handleClear: handleClear({ emit, props, state }),
+ handleMouseLeave: handleMouseLeave({ state }),
+ handleMouseOver: handleMouseOver({ state }),
+ handleInputBlur: handleInputBlur({ state }),
+ handleInputFocus: handleInputFocus({ state }),
+ handleDragStart: handleDragStart({ emit, props, state }),
+ handleDragOver: handleDragOver(),
+ handleDragEnter: handleDragEnter({ emit, state }),
+ handleDrop: handleDrop({ emit, props, state })
+ }
+
+ return api
+}
diff --git a/packages/renderless/types/index.ts b/packages/renderless/types/index.ts
index 5e1e172eec..afb1a80106 100644
--- a/packages/renderless/types/index.ts
+++ b/packages/renderless/types/index.ts
@@ -180,6 +180,7 @@ export * from './tabs.type'
export * from './tabs-mf.type'
export * from './tag.type'
export * from './tag-group.type'
+export * from './tag-input.type'
export * from './tall-storage.type'
export * from './text-popup.type'
export * from './time.type'
diff --git a/packages/renderless/types/tag-input.type.ts b/packages/renderless/types/tag-input.type.ts
new file mode 100644
index 0000000000..0409586c22
--- /dev/null
+++ b/packages/renderless/types/tag-input.type.ts
@@ -0,0 +1,55 @@
+import type { ExtractPropTypes } from 'vue'
+import type { tagInputProps } from '@/tag-input/src'
+import type {
+ addTag,
+ removeTag,
+ handleBackspace,
+ handleClear,
+ handleMouseLeave,
+ handleMouseOver,
+ handleInputBlur,
+ handleInputFocus,
+ handleDragStart,
+ handleDragOver,
+ handleDragEnter,
+ handleDrop
+} from '../src/tag-input'
+import type { ISharedRenderlessFunctionParams } from './shared.type'
+
+export interface ITagInputState {
+ currentValue: string
+ tagList: string[]
+ disabled: boolean
+ closeable: boolean
+ showClearIcon: boolean
+ showTagList: string[]
+ collapsedTagList: string[]
+ isHovering: boolean
+ isFocused: boolean
+ draggingIndex: null | number
+ dragTargetIndex: null | number
+}
+
+export interface ITagInputApi {
+ state: ITagInputState
+ addTag: ReturnType
+ removeTag: ReturnType
+ handleBackspace: ReturnType
+ handleClear: ReturnType
+ handleMouseLeave: ReturnType
+ handleMouseOver: ReturnType
+ handleInputBlur: ReturnType
+ handleInputFocus: ReturnType
+ handleDragStart: ReturnType
+ handleDragOver: ReturnType
+ handleDragEnter: ReturnType
+ handleDrop: ReturnType
+}
+
+export type ITagInputProps = ExtractPropTypes
+
+export type ITagInputRenderlessParams = ISharedRenderlessFunctionParams & {
+ state: ITagInputState
+ props: ITagInputProps
+ api: ITagInputApi
+}
diff --git a/packages/theme/src/index.less b/packages/theme/src/index.less
index b37b786fb7..c5eaf7c749 100644
--- a/packages/theme/src/index.less
+++ b/packages/theme/src/index.less
@@ -134,6 +134,7 @@
@import './tabs/index.less';
@import './tag/index.less';
@import './tag-group/index.less';
+@import './tag-input/index.less';
@import './tall-storage/index.less';
@import './text-popup/index.less';
@import './textarea/index.less';
diff --git a/packages/theme/src/tag-input/index.less b/packages/theme/src/tag-input/index.less
new file mode 100644
index 0000000000..f4ff1aaed5
--- /dev/null
+++ b/packages/theme/src/tag-input/index.less
@@ -0,0 +1,59 @@
+@import '../custom.less';
+@import './vars.less';
+
+@tag-prefix-cls: ~'@{css-prefix}tag';
+@tag-input-prefix-cls: ~'@{css-prefix}tag-input';
+
+.@{tag-input-prefix-cls} {
+ .inject-TagInput-vars();
+ border: 1px solid #ccc;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ border-radius: var(--tv-TagInput-border-radius);
+ justify-content: space-between;
+ gap: var(--tv-TagInput-item-gap);
+ padding: var(--tv-TagInput-padding);
+
+ &-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: var(--tv-TagInput-item-gap);
+ flex: 1;
+ }
+
+ &-suffix-wrapper {
+ display: flex;
+ align-items: center;
+ gap: var(--tv-TagInput-item-gap);
+ }
+
+ &-disabled {
+ background-color: var(--tv-TagInput-bg-color-disabled);
+ border-color: var(--tv-TagInput-border-color-disabled);
+ cursor: not-allowed;
+ }
+
+ .@{tag-input-prefix-cls}-inner {
+ border: none;
+ outline: none;
+ height: 32px;
+ line-height: 32px;
+ color: var(--tv-TagInput-text-color);
+ background: var(--tv-TagInput-bg-color);
+ padding: 0 var(--tv-TagInput-padding);
+ font-size: 14px;
+ width: auto;
+ }
+
+ .@{tag-input-prefix-cls}-inner:disabled {
+ background-color: var(--tv-TagInput-bg-color-disabled) !important;
+ }
+}
+
+.tiny-tag-input-collapsed-tags {
+ .inject-TagInput-vars();
+ display: flex;
+ gap: var(--tv-TagInput-collapsed-item-gap);
+}
diff --git a/packages/theme/src/tag-input/vars.less b/packages/theme/src/tag-input/vars.less
new file mode 100644
index 0000000000..6579b48f28
--- /dev/null
+++ b/packages/theme/src/tag-input/vars.less
@@ -0,0 +1,41 @@
+/**
+* Copyright (c) 2022 - present TinyVue Authors.
+* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
+*
+* Use of this source code is governed by an MIT-style license.
+*
+* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
+* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
+* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
+*
+*/
+
+.inject-TagInput-vars() {
+ // -----------------------------------------------排列布局场景
+ // 标签组的底部外间距
+ --tv-TagInput-margin-bottom: var(--tv-space-lg, 12px);
+ // 标签组的子项的间距
+ --tv-TagInput-item-gap: var(--tv-space-md, 8px);
+ // 标签组折叠的子项的间距
+ --tv-TagInput-collapsed-item-gap: var(--tv-space-sm, 4px);
+ // 标签输入框的内边距
+ --tv-TagInput-padding: var(--tv-space-sm, 4px);
+
+ // -----------------------------------------------更多的场景
+
+ // 标签组有省略图标时,标签子项到右边的安全距离
+ --tv-TagInput-more-icon-safe-space: 32px;
+
+ // 标签禁用时的文本色
+ --tv-TagInput-text-color-disabled: var(--tv-color-text-disabled, #c2c2c2);
+ // 标签禁用时的背景色
+ --tv-TagInput-bg-color-disabled: var(--tv-color-bg-disabled, #f0f0f0);
+ // 标签禁用时的边框色
+ --tv-TagInput-border-color-disabled: var(--tv-color-border-disabled, #e0e0e0);
+ //标签输入框圆角
+ --tv-TagInput-border-radius: var(--tv-border-radius-sm, 4px);
+ // 输入框文本颜色
+ --tv-TagInput-text-color: var(--tv-color-text, #191919);
+ // 输入框背景颜色
+ --tv-TagInput-bg-color: var(--tv-color-bg-secondary, #ffffff);
+}
diff --git a/packages/vue/index.ts b/packages/vue/index.ts
index 91ce12c1a5..e4a0fc190d 100644
--- a/packages/vue/index.ts
+++ b/packages/vue/index.ts
@@ -163,6 +163,7 @@ import Table from '@opentiny/vue-table'
import Tabs from '@opentiny/vue-tabs'
import Tag from '@opentiny/vue-tag'
import TagGroup from '@opentiny/vue-tag-group'
+import TagInput from '@opentiny/vue-tag-input'
import TextPopup from '@opentiny/vue-text-popup'
import Time from '@opentiny/vue-time'
import TimeLine from '@opentiny/vue-time-line'
@@ -364,6 +365,7 @@ const components = [
Tabs,
Tag,
TagGroup,
+ TagInput,
TextPopup,
Time,
TimeLine,
@@ -749,6 +751,8 @@ export {
Tag as TinyTag,
TagGroup,
TagGroup as TinyTagGroup,
+ TagInput,
+ TagInput as TinyTagInput,
TextPopup,
TextPopup as TinyTextPopup,
Time,
@@ -1148,6 +1152,8 @@ export default {
TinyTag: Tag,
TagGroup,
TinyTagGroup: TagGroup,
+ TagInput,
+ TinyTagInput: TagInput,
TextPopup,
TinyTextPopup: TextPopup,
Time,
diff --git a/packages/vue/package.json b/packages/vue/package.json
index fe41a3f6f1..ba193bcb9e 100644
--- a/packages/vue/package.json
+++ b/packages/vue/package.json
@@ -197,6 +197,7 @@
"@opentiny/vue-tabs": "workspace:~",
"@opentiny/vue-tag": "workspace:~",
"@opentiny/vue-tag-group": "workspace:~",
+ "@opentiny/vue-tag-input": "workspace:~",
"@opentiny/vue-text-popup": "workspace:~",
"@opentiny/vue-time": "workspace:~",
"@opentiny/vue-time-line": "workspace:~",
diff --git a/packages/vue/src/tag-input/index.ts b/packages/vue/src/tag-input/index.ts
new file mode 100644
index 0000000000..7a8770a9d2
--- /dev/null
+++ b/packages/vue/src/tag-input/index.ts
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
+ *
+ * Use of this source code is governed by an MIT-style license.
+ *
+ * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
+ * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
+ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
+ *
+ */
+import TagInput from './src/index'
+import '@opentiny/vue-theme/tag-input/index.less'
+import { version } from './package.json'
+
+/* istanbul ignore next */
+TagInput.install = function (Vue) {
+ Vue.component(TagInput.name, TagInput)
+}
+
+TagInput.version = version
+
+/* istanbul ignore next */
+if (process.env.BUILD_TARGET === 'runtime') {
+ if (typeof window !== 'undefined' && window.Vue) {
+ TagInput.install(window.Vue)
+ }
+}
+
+export default TagInput
diff --git a/packages/vue/src/tag-input/package.json b/packages/vue/src/tag-input/package.json
new file mode 100644
index 0000000000..e0272980ea
--- /dev/null
+++ b/packages/vue/src/tag-input/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@opentiny/vue-tag-input",
+ "version": "3.27.0",
+ "description": "",
+ "main": "lib/index.js",
+ "module": "index.ts",
+ "sideEffects": false,
+ "type": "module",
+ "dependencies": {
+ "@opentiny/vue-common": "workspace:~",
+ "@opentiny/vue-icon": "workspace:~",
+ "@opentiny/vue-popover": "workspace:~",
+ "@opentiny/vue-tag": "workspace:~",
+ "@opentiny/vue-renderless": "workspace:~",
+ "@opentiny/vue-theme": "workspace:~"
+ },
+ "license": "MIT",
+ "devDependencies": {
+ "@opentiny-internal/vue-test-utils": "workspace:*",
+ "vitest": "catalog:"
+ },
+ "scripts": {
+ "build": "pnpm -w build:ui $npm_package_name",
+ "//postversion": "pnpm build"
+ }
+}
\ No newline at end of file
diff --git a/packages/vue/src/tag-input/src/index.ts b/packages/vue/src/tag-input/src/index.ts
new file mode 100644
index 0000000000..5c3ce7259a
--- /dev/null
+++ b/packages/vue/src/tag-input/src/index.ts
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2022 - present TinyVue Authors.
+ * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd.
+ *
+ * Use of this source code is governed by an MIT-style license.
+ *
+ * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
+ * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
+ * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
+ *
+ */
+import { $props, $setup, defineComponent, $prefix } from '@opentiny/vue-common'
+import template from 'virtual-template?pc'
+
+export const tagInputProps = {
+ ...$props,
+ modelValue: {
+ type: Array,
+ default: () => []
+ },
+ size: {
+ type: String,
+ default: 'medium',
+ validator: (value: string) => ['medium', 'small'].includes(value)
+ },
+ tagType: {
+ type: String,
+ default: '',
+ validator: (value: string) => ['', 'info', 'success', 'warning', 'danger', 'primary'].includes(value)
+ },
+ tagEffect: {
+ type: String,
+ default: 'light',
+ validator: (value: string) => ['dark', 'light', 'plain'].includes(value)
+ },
+ clearable: {
+ type: Boolean,
+ default: false
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ placeholder: {
+ type: String,
+ default: ''
+ },
+ max: {
+ type: Number,
+ default: Infinity
+ },
+ readonly: {
+ type: Boolean,
+ default: false
+ },
+ separator: {
+ type: String,
+ default: undefined
+ },
+ minCollapsedNum: {
+ type: Number,
+ default: Infinity
+ },
+ draggable: {
+ type: Boolean,
+ default: false
+ }
+}
+
+export default defineComponent({
+ name: $prefix + 'TagInput',
+ props: tagInputProps,
+ setup(props, context): any {
+ return $setup({ props, context, template })
+ }
+})
diff --git a/packages/vue/src/tag-input/src/pc.vue b/packages/vue/src/tag-input/src/pc.vue
new file mode 100644
index 0000000000..19b32f0011
--- /dev/null
+++ b/packages/vue/src/tag-input/src/pc.vue
@@ -0,0 +1,97 @@
+
+
+
+
+