(component): [Tag}新增 Tag 组件

This commit is contained in:
sight 2022-08-06 09:06:37 +08:00
parent 48cacb8d91
commit 24b2cbd143
11 changed files with 788 additions and 0 deletions

View File

@ -0,0 +1,88 @@
@tag-size-default: 24px;
@tag-size-default-font-size: 12px;
@tag-size-lg: @tag-size-default + 2px;
@tag-size-md: @tag-size-default;
@tag-size-sm: @tag-size-default - 2px;
@tag-size-xs: @tag-size-default - 2px * 2;
@tag-size-lg-font-size: @tag-size-default-font-size + 2px;
@tag-size-md-font-size: @tag-size-default-font-size;
@tag-size-sm-font-size: @tag-size-default-font-size - 2px;
@tag-size-xs-font-size: @tag-size-default-font-size - 2px * 2;
@tag-border-width: 1px;
.layui-tag {
display: inline-flex;
align-items: baseline;
vertical-align: middle;
box-sizing: border-box;
height: @tag-size-md;
line-height: @tag-size-md - @tag-border-width * 2;
padding: 0 6px;
font-size: @tag-size-md-font-size;
font-weight: 500;
color: currentColor;
border-width: @tag-border-width;
border-style: solid;
border-color: transparent;
border-radius: var(--global-border-radius);
&-icon {
margin-right: 4px;
}
&-bordered {
border-color: var(--global-neutral-color-5);
}
&-size-lg {
height: @tag-size-lg;
font-size: @tag-size-lg-font-size;
line-height: @tag-size-lg - @tag-border-width * 2;
.layui-icon {
font-size: @tag-size-lg-font-size - 2px;
}
}
&-size-md {
height: @tag-size-md;
font-size: @tag-size-md-font-size;
line-height: @tag-size-md - @tag-border-width * 2;
.layui-icon {
font-size: @tag-size-md-font-size - 2px;
}
}
&-size-sm {
height: @tag-size-sm;
font-size: @tag-size-sm-font-size;
line-height: @tag-size-sm - @tag-border-width * 2;
.layui-icon {
font-size: @tag-size-sm-font-size - 2px;
}
}
&-size-xs {
height: @tag-size-xs;
font-size: @tag-size-xs-font-size;
line-height: @tag-size-xs - @tag-border-width * 2;
.layui-icon {
font-size: @tag-size-xs-font-size - 2px;
}
}
& &-close-icon {
margin-left: 4px;
font-size: @tag-size-default-font-size - 2px;
.layui-icon {
&:hover {
cursor: pointer;
}
}
}
}

View File

@ -0,0 +1,5 @@
import { withInstall, WithInstallType } from "../../utils";
import Component from "./index.vue";
const component: WithInstallType<typeof Component> = withInstall(Component);
export default component;

View File

@ -0,0 +1,71 @@
<script lang="ts">
export default {
name: "LayTag",
};
</script>
<script lang="ts" setup>
import "./index.less";
import { LayIcon } from "@layui/icons-vue";
import { computed, ref } from "vue";
import { TAG_COLORS, TagColor } from "./interface";
export interface LayTagProps {
color?: TagColor | string;
closable?: boolean;
size?: string;
bordered?: boolean;
}
const props = withDefaults(defineProps<LayTagProps>(), {
color: "green",
size: "md",
closable: false,
bordered: false,
});
const emit = defineEmits(["close"]);
const isBuiltInColor = computed(
() => props.color && TAG_COLORS.includes(props.color as any)
);
const isCustomColor = computed(
() => props.color && !TAG_COLORS.includes(props.color as any)
);
const visible = ref(true);
const handleClose = (e: MouseEvent) => {
//visible.value = false;
emit("close", e);
};
const classTag = computed(() => [
"layui-tag",
`layui-tag-size-${props.size}`,
{
[`layui-bg-${props.color}`]: isBuiltInColor,
"layui-tag-bordered": props.bordered,
},
]);
const styleTag = computed(() => {
return isCustomColor.value ? { backgroundColor: props.color } : undefined;
});
</script>
<template>
<span v-if="visible" :class="classTag" :style="styleTag">
<span v-if="$slots.icon" class="layui-tag-icon">
<slot name="icon" />
</span>
<slot />
<span
v-if="closable"
class="layui-tag-close-icon"
@click.stop="handleClose"
>
<slot name="close-icon">
<lay-icon type="layui-icon-close"></lay-icon>
</slot>
</span>
</span>
</template>

View File

@ -0,0 +1,11 @@
export const TAG_COLORS = [
"red",
"orange",
"green",
"cyan",
"blue",
"black",
"gray",
] as const;
export type TagColor = typeof TAG_COLORS[number];

View File

@ -0,0 +1,64 @@
<script lang="ts">
export default {
name: "Tag",
};
</script>
<script lang="ts" setup>
import { ref } from "vue";
export interface LayTagProps {
theme?: string;
closable?: boolean;
size?: string;
}
const props = withDefaults(defineProps<LayTagProps>(), {
closable: true,
});
const emit = defineEmits(["close"]);
const visible = ref(true);
const handleClose = (e: MouseEvent) => {
visible.value = false;
emit("close", e);
};
</script>
<template>
<lay-badge v-if="visible" theme="green">
<template v-if="$slots.default" #default>
<slot name="default"></slot>
<lay-icon
v-if="closable"
type="layui-icon-close"
@click.stop="handleClose"
></lay-icon>
</template>
</lay-badge>
</template>
<!-- <template v-for="(item, index) in selectItem.label" :key="index">
<lay-badge theme="green">
<span>{{ item }}</span>
<i
:class="['layui-icon', { 'layui-icon-close': true }]"
v-if="
!disabled &&
!(
Array.isArray(selectItem.value) &&
selectItem.value.length > 0 &&
disabledItemMap[selectItem.value[index]]
)
"
@click="
removeItemHandle($event, {
label: item,
value: Array.isArray(selectItem.value)
? selectItem.value[index]
: null,
})
"
></i>
</lay-badge>
</template> -->

View File

@ -0,0 +1,65 @@
.layui-input-tag {
position: relative;
display: block;
padding: 0 5px;
height: auto;
overflow: hidden;
.layui-input-prefix {
display: inline;
text-align: left;
height: 100%;
flex: unset;
}
.layui-input-suffix{
position: absolute;
right: 3px;
bottom: 0;
height: 100%;
}
.layui-input {
display: inline-block;
padding-left: 0;
width: auto;
flex: 1;
max-width: 100%;
min-width: 12px;
}
}
.layui-input-tag-collapsed-panel,
.layui-input-tag {
.layui-badge {
margin-right: 5px;
height: 28px;
line-height: 28px;
user-select: none;
white-space: pre-wrap;
.layui-icon {
font-size: 12px;
padding-left: 3px;
&:hover {
cursor: pointer;
color: #ff5722;
}
}
}
}
.layui-input-tag-collapsed-panel {
white-space: normal;
display: flex;
align-items: center;
flex-wrap: wrap;
width: fit-content;
max-width: 200px;
height: auto;
overflow: hidden;
.layui-badge{
margin-bottom: 4px;
}
}

View File

@ -0,0 +1,5 @@
import { withInstall, WithInstallType } from "../../utils";
import Component from "./index.vue";
const component: WithInstallType<typeof Component> = withInstall(Component);
export default component;

View File

@ -0,0 +1,158 @@
<script lang="ts">
export default {
name: "LayInputTag",
};
</script>
<script lang="ts" setup>
import "./index.less";
import Tag from "./Tag.vue";
import LayToopTip from "../tooltip/index.vue";
import { onMounted, shallowRef, ref, watch, computed } from "vue";
export interface TagData {
value?: string | number;
label?: string;
closable?: boolean;
[other: string]: any;
}
export interface LayInputTagProps {
modelValue?: TagData[];
inputValue?: string;
disabled?: boolean;
placeholder?: string;
readonly?: boolean;
allowClear?: boolean;
max?: number;
minCollapsedNum?: number;
collapseTagsTooltip?: boolean;
size?: "md" | "sm" | "xs";
}
const props = withDefaults(defineProps<LayInputTagProps>(), {
disabled: false,
placeholder: "",
readonly: false,
allowClear: true,
minCollapsedNum: 10,
//max:3
});
const emit = defineEmits(["update:modelValue", "update:inputValue"]);
const mirrorRef = shallowRef<HTMLElement | undefined>(undefined);
const inputRef = shallowRef<HTMLElement | undefined>(undefined);
const tagData = ref<TagData[]>(props.modelValue ?? []);
const inputValue = ref<string>();
const oldInputValue = ref<string>();
const computedTagData = computed(() => {
return props.minCollapsedNum
? tagData.value?.slice(0, props.minCollapsedNum)
: tagData.value;
});
const collapsedTagData = computed(() => {
return props.minCollapsedNum && tagData.value?.length > props.minCollapsedNum
? tagData.value?.slice(props.minCollapsedNum)
: [];
});
const handleInputEnter = (e: KeyboardEvent) => {
e.preventDefault();
const valueStr = inputValue.value ? String(inputValue.value).trim() : "";
if (!valueStr || !tagData.value) return;
const isLimit = props.max && tagData.value?.length >= props.max;
if (!isLimit) {
tagData.value =
tagData.value instanceof Array
? tagData.value.concat(String(valueStr))
: [valueStr];
inputValue.value = "";
}
};
const handlerInputBackspaceKeyUp = (e: KeyboardEvent) => {
if (!tagData.value || !tagData.value.length) return;
if (!oldInputValue.value && ["Backspace", "Delete"].includes(e.code)) {
tagData.value = tagData.value.slice(0, -1);
}
oldInputValue.value = inputValue.value;
};
const handlerClearClick = (e: MouseEvent) => {
tagData.value = [];
};
const handlerClose = (index: number) => {
if (!tagData.value) return;
const arr = [...tagData.value];
arr.splice(index, 1);
tagData.value = arr;
};
const handlerFocus = (e: MouseEvent) => {
(
(e.target as HTMLElement).querySelector(".layui-input") as HTMLInputElement
)?.focus();
};
watch(tagData, (val) => {
emit("update:modelValue", val);
});
watch(inputValue, (val) => {
emit("update:inputValue", val);
});
onMounted(() => {});
</script>
<template>
<lay-input
class="layui-input-tag"
v-model="inputValue"
:placeholder="placeholder"
:readonly="readonly"
@keydown.enter="handleInputEnter"
@keyup="handlerInputBackspaceKeyUp"
@click="handlerFocus"
>
<template #prefix>
<template
v-for="(item, index) of computedTagData"
:key="`${item}-${index}`"
>
<Tag :closable="!readonly" @close="handlerClose(index)">
{{ item }}
</Tag>
</template>
<template v-if="computedTagData?.length != tagData?.length">
<LayToopTip :isDark="false">
<Tag key="more" :closable="false"
>+{{ tagData?.length - computedTagData?.length }}...</Tag
>
<template #content>
<div class="layui-input-tag-collapsed-panel">
<template
v-for="(item, index) of tagData"
:key="`${item}-${index}`"
>
<Tag
v-if="index >= minCollapsedNum"
:closable="!readonly"
@close="handlerClose(index)"
>
{{ item }}
</Tag>
</template>
</div>
</template>
</LayToopTip>
</template>
</template>
<template #suffix v-if="allowClear && tagData?.length">
<lay-icon type="layui-icon-close-fill" @click.stop="handlerClearClick" />
</template>
</lay-input>
</template>

View File

@ -88,6 +88,7 @@ import LayNoticeBar from "./component/noticeBar/index";
import LayPageHeader from "./component/pageHeader/index";
import LayCascader from "./component/cascader/index";
import LayAffix from "./component/affix/index";
import LayTag from "./component/tag/index";
import LayConfigProvider from "./provider";
import { InstallOptions } from "./types";
@ -174,6 +175,7 @@ const components: Record<string, Plugin> = {
LayPageHeader,
LayCascader,
LayAffix,
LayTag,
};
const install = (app: App, options?: InstallOptions): void => {
@ -267,6 +269,7 @@ export {
LayPageHeader,
LayCascader,
LayAffix,
LayTag,
install,
};

View File

@ -0,0 +1,313 @@
::: anchor
:::
::: title 基本介绍
:::
::: describe 标签组件。
:::
::: title 基础使用
:::
::: demo 标签的基本用法。
<template>
<lay-tag>tag</lay-tag>
&nbsp&nbsp
<lay-tag bordered color="#FFF">tag</lay-tag>
&nbsp&nbsp
<lay-tag closable>tag</lay-tag>
</template>
<script>
</script>
:::
::: title 标签尺寸
:::
::: demo 通过 size 属性控制标签尺寸。
<template>
<lay-tag>default</lay-tag>
&nbsp&nbsp
<lay-tag size="lg">tag lg</lay-tag>
&nbsp&nbsp
<lay-tag size="md">tag md</lay-tag>
&nbsp&nbsp
<lay-tag size="sm">tag sm</lay-tag>
&nbsp&nbsp
<lay-tag size="xs">tag xs</lay-tag>
<br><br>
<lay-tag><template #icon><lay-icon type="layui-icon-addition" /></template>default</lay-tag>
&nbsp&nbsp
<lay-tag size="lg"><template #icon><lay-icon type="layui-icon-addition" /></template>tag lg</lay-tag>
&nbsp&nbsp
<lay-tag size="md"><template #icon><lay-icon type="layui-icon-addition" /></template>tag md</lay-tag>
&nbsp&nbsp
<lay-tag size="sm"><template #icon><lay-icon type="layui-icon-addition" /></template>tag sm</lay-tag>
&nbsp&nbsp
<lay-tag size="xs"><template #icon><lay-icon type="layui-icon-addition" /></template>tag xs</lay-tag>
<br><br>
<lay-tag closable>
<template #icon><lay-icon type="layui-icon-addition" /></template>
default
</lay-tag>
&nbsp&nbsp
<lay-tag size="lg" closable>
<template #icon><lay-icon type="layui-icon-addition" /></template>
tag lg
</lay-tag>
&nbsp&nbsp
<lay-tag size="md" closable>
<template #icon><lay-icon type="layui-icon-addition" /></template>
tag md
</lay-tag>
&nbsp&nbsp
<lay-tag size="sm" closable>
<template #icon><lay-icon type="layui-icon-addition" /></template>
tag sm
</lay-tag>
&nbsp&nbsp
<lay-tag size="xs" closable>
<template #icon><lay-icon type="layui-icon-addition" /></template>
tag xs
</lay-tag>
&nbsp&nbsp
</template>
<script>
</script>
:::
::: title 可关闭
:::
::: demo 通过 closable 属性控制标签是否可关闭
<template>
<lay-tag closable>Tag</lay-tag>
&nbsp&nbsp
<lay-tag closable>
<template #icon><lay-icon type="layui-icon-addition" /></template>
Tag 2
</lay-tag>
&nbsp&nbsp
<lay-tag closable>
<template #icon><lay-icon type="layui-icon-addition" /></template>
自定义关闭图标
<template #close-icon><lay-icon type="layui-icon-close-fill" /></template>
</lay-tag>
</template>
<script>
</script>
:::
::: title 标签颜色
:::
::: demo 标签颜色,待优化。
<template>
<span v-for="color in TAG_COLORS">
<lay-tag :color="color">Tag</lay-tag>
&nbsp&nbsp
</span>
<br><br>
<span v-for="color in TAG_COLORS">
<lay-tag :color="color" bordered>Tag</lay-tag>
&nbsp&nbsp
</span>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const TAG_COLORS = [
"red",
"orange",
"green",
"cyan",
"blue",
"black",
"gray",
"#EEE",
"#5FB878",
"#FFB800",
];
return {
TAG_COLORS
}
}
}
</script>
:::
::: title 图标插槽
:::
::: demo 标签的基本用法。
<template>
<lay-tag bordered size="lg" color="#FFF">
<template #icon>
<lay-icon type="layui-icon-vercode" />
</template>
tag
</lay-tag>
&nbsp&nbsp
<lay-tag bordered size="lg" color="#FFF">
<template #icon>
<lay-icon type="layui-icon-login-qq" />
</template>
tag
</lay-tag>
&nbsp&nbsp
<lay-tag bordered size="lg" color="#FFF">
<template #icon>
<lay-icon type="layui-icon-star-fill" />
</template>
tag
</lay-tag>
&nbsp&nbsp
</template>
<script>
</script>
:::
::: title 动态编辑
:::
::: demo 标签的基本用法。
<template>
{{ tagData }}
<br><br>
<template v-for="(tag, index) of tagData" :key="`tag-${index}`">
<lay-tag
closable
@close="handleClose(index)"
color="#EEE"
style="margin-right: 5px;"
>
{{tag}}
</lay-tag>
</template>
<span id="tagDemo" @click="handlerFocus">
<lay-input
v-if="showInput"
ref="inputRef"
v-model.trim="inputVal"
autofocus
style="width:60px; height:24px"
@keyup.enter="handleAdd"
@blur="handleAdd" />
<lay-tag
v-else
color="#EEE"
>
<template #icon>
<lay-icon type="layui-icon-addition"/>
</template>
添加
</lay-tag>
</span>
</template>
<script>
import { ref, nextTick } from 'vue';
export default {
setup() {
const tagData = ref(['Tag']);
const inputRef = ref(null);
const showInput = ref(false);
const inputVal = ref('');
const handleAdd = () => {
if (inputVal.value) {
tagData.value.push(inputVal.value);
inputVal.value = '';
}
showInput.value = false;
};
const handleClose = (index) => {
tagData.value.splice(index, 1);
};
const handlerFocus = (e) => {
showInput.value = true;
console.log("FIXME 临时")
setTimeout(() => {
document.querySelector('#tagDemo input').focus()
},200)
}
return {
tagData,
inputRef,
showInput,
inputVal,
handleAdd,
handleClose,
handlerFocus,
};
},
};
</script>
:::
::: title Tag 属性
:::
::: table
| 属性 | 描述 | 类型 | 默认值 | 可选值 |
| ----------- | -------- | ------ | ------ | ------ |
| size | 标签大小 | string | md | `lg` `md` `sm` `xs`|
| color | 标签颜色 | string | `green`| `red` `orange` `green` `cyan` `blue` `black` `gray` `string` |
| bordered | 是否显示边框 | boolean | false | `true` `false`|
| closable | 是否可关闭 | boolean | false | `true` `false`|
:::
:::title Tag 插槽
:::
:::table
| 插槽 | 描述 | 参数 |
|------ |----------|-----------|
| default | 默认插槽 | - |
| icon | 图标 | - |
| close-icon | 关闭图标 | - |
:::
:::title Tag 事件
:::
:::table
| 事件 | 描述 | 参数 |
|------ |----------|-----------|
| close | 关闭时触发 | e: MouseEvent |
:::
::: contributor tag
:::
::: previousNext tag
:::

View File

@ -419,6 +419,11 @@ const zhCN = [
component: () => import("../document/zh-CN/components/photo.md"),
meta: { title: "相册" },
},
{
path: "/zh-CN/components/tag",
component: () => import("../document/zh-CN/components/tag.md"),
meta: { title: "标签" },
},
],
},
],