♻️(component): 初步完成 select 组件重构

This commit is contained in:
就眠儀式 2022-09-23 12:41:44 +08:00
parent ff335e56df
commit f52ff6207b
10 changed files with 80 additions and 536 deletions

View File

@ -42,6 +42,8 @@ const matchModule: string[] = [
"empty",
"dropdownMenu",
"dropdownMenuItem",
"tag",
"tagInput"
];
export default (): UserConfigExport => {

View File

@ -1,173 +1,12 @@
@import "../badge/index.less";
@import "../checkbox/index.less";
@import "../input/index.less";
@select-lg: 44px;
@select-md: 38px;
@select-sm: 32px;
@select-xs: 26px;
@select-lg-badge:32px;
@select-md-badge:26px;
@select-sm-badge:20px;
@select-xs-badge:14px;
@select-lg-width: 260px;
@select-md-width: 220px;
@select-sm-width: 180px;
@select-xs-width: 140px;
.set-size(@size,@badge-size,@width) {
& {
width: @width;
height: @size;
.layui-input {
height: @size;
line-height: @size;
}
.layui-badge{
height: @badge-size;
line-height: @badge-size;
box-sizing: border-box;
}
}
}
dl.layui-anim-upbit > dd input[type="checkbox"] {
display: none;
}
dl.layui-anim-upbit > dd .layui-form-checkbox,
.layui-form-item dl.layui-anim-upbit > dd .layui-form-checkbox[lay-skin] {
margin-top: -3px;
}
.layui-multiple-select-row {
position: absolute;
height: 100%;
top: 0;
left: 0;
right: 34px;
padding: 5px 0 5px 6px;
box-sizing: border-box;
overflow: hidden;
overflow-x: scroll;
cursor: pointer;
.layui-multiple-select-badge {
width: ms-max-content;
width: max-content;
.layui-badge {
margin-left: 5px;
height: 25px;
line-height: 25px;
border:1px solid #f0f0f0;
&:first-of-type {
margin-left: 0;
}
}
.layui-icon {
font-size: 12px;
padding-left: 3px;
}
}
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-thumb {
height: 50px;
background-color: whitesmoke;
border-radius: 6px;
opacity: 0.5;
}
&::-webkit-scrollbar-thumb:hover {
height: 50px;
background-color: whitesmoke;
border-radius: 6px;
}
}
.layui-select {
height: 38px;
line-height: 1.3;
line-height: 38px\9;
border-width: 1px;
border-style: solid;
background-color: #fff;
border-color: #eee;
color: rgba(0, 0, 0, 0.85);
border-radius: 2px;
padding: 0 10px;
width: 220px;
}
.layui-select::-webkit-input-placeholder {
line-height: 1.3;
}
.layui-form-select {
position: relative;
}
.layui-select-title{
height: 100%;
}
.layui-select-title .layui-input {
padding-right: 36px;
padding-left: 10px;
cursor: pointer;
}
.layui-form-select .layui-edge {
position: absolute;
right: 10px;
top: 50%;
margin-top: -3px;
cursor: pointer;
border-width: 6px;
border-top-color: var(--global-neutral-color-8);
border-top-style: solid;
transition: all 0.3s;
-webkit-transition: all 0.3s;
}
.layui-input-icon-area{
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
z-index: 1;
cursor: pointer;
.layui-icon-close-fill{
margin-left: 10px;
color: rgba(0, 0, 0, 0.45);
}
}
.layui-form-select .layui-icon-triangle-d {
display: inline-block;
transition: all 0.3s;
-webkit-transition: all 0.3s;
color:var(--global-neutral-color-8);
}
.layui-form-select dl {
display: none;
position: absolute;
left: 0;
top: 42px;
.layui-select-options {
padding: 5px 0;
z-index: 899;
min-width: 100%;
border: 1px solid var(--global-neutral-color-3);
max-height: 300px;
overflow-y: auto;
background-color: #fff;
border-radius: var(--global-border-radius);
box-sizing: border-box;
box-shadow: 1px 1px 4px rgb(0 0 0 / 8%);
}
.layui-form-select dl dd,
.layui-form-select dl dt {
.layui-select-options .layui-select-option {
padding: 0 10px;
line-height: 36px;
white-space: nowrap;
@ -175,66 +14,7 @@ dl.layui-anim-upbit > dd .layui-form-checkbox,
text-overflow: ellipsis;
}
.layui-form-select dl dt {
font-size: 12px;
color: #999;
}
.layui-form-select dl dd {
cursor: pointer;
}
.layui-form-select dl dd:hover {
background-color: var(--global-neutral-color-2);
-webkit-transition: 0.5s all;
transition: 0.5s all;
}
.layui-form-select .layui-select-group dd {
padding-left: 20px;
}
.layui-form-select dl dd.layui-select-tips {
padding-left: 10px !important;
color: #999;
}
.layui-form-select dl dd.layui-this {
.layui-select-options .layui-select-option.layui-this {
background-color: var(--global-neutral-color-2);
color: var(--global-checked-color);
font-weight: 700;
}
.layui-form-checkbox,
.layui-form-select dl dd.layui-disabled {
background-color: #fff;
}
.layui-form-selected dl {
display: block;
}
.layui-multiple-select-input {
margin-bottom: 5px;
padding: 0 10px;
cursor: pointer;
.layui-input {
height: 30px !important;
line-height: 30px !important;
}
}
.layui-form-select{
&[size="lg"] {
.set-size(@select-lg, @select-lg-badge, @select-lg-width);
}
&[size="md"] {
.set-size(@select-md, @select-md-badge, @select-md-width);
}
&[size="sm"] {
.set-size(@select-sm, @select-sm-badge, @select-sm-width);
}
&[size="xs"] {
.set-size(@select-xs, @select-xs-badge, @select-xs-width);
}
}

View File

@ -6,286 +6,72 @@ export default {
<script setup lang="ts">
import "./index.less";
import LaySelectOption from "../selectOption/index.vue";
import { provide, ref, watch, computed, Ref, nextTick, shallowRef } from "vue";
import LayBadge from "../badge/index.vue";
import { provide, computed, WritableComputedRef } from "vue";
import LayInput from "../input/index.vue";
import LayScroll from "../scroll/index.vue";
import { onClickOutside } from "@vueuse/core";
import { SelectItem } from "../../types";
import LayTagInput from "../tagInput/index.vue";
import LayDropdown from "../dropdown/index.vue";
import LaySelectOption, { LaySelectOptionProps } from "../selectOption/index.vue";
export interface LaySelectProps {
name?: string;
placeholder?: string;
disabled?: boolean;
showEmpty?: boolean;
emptyMessage?: string;
modelValue?: any;
multiple?: boolean;
create?: boolean;
items?: {
label: string;
value: any;
key: string;
disabled: boolean;
keyword: string;
}[];
items?: LaySelectOptionProps[];
size?: "lg" | "md" | "sm" | "xs";
allowClear?: boolean;
}
const selectRef = shallowRef<undefined | HTMLElement>(undefined);
const multipleSearchInputRef = shallowRef<HTMLElement | undefined>(undefined);
onClickOutside(selectRef, (event: Event) => {
openState.value = false;
});
export interface SelectEmits {
(e: "update:modelValue", value: string): void;
(e: "change", value: string): void;
}
const props = withDefaults(defineProps<LaySelectProps>(), {
modelValue: null,
placeholder: "请选择",
disabled: false,
showEmpty: true,
multiple: false,
create: false,
size: "md",
allowClear: false,
});
const openState = ref(false);
const emits = defineEmits<SelectEmits>();
const open = function () {
if (props.disabled) {
openState.value = false;
return;
}
openState.value = !openState.value;
nextTick(() => {
multipleSearchInputRef.value?.querySelector("input")?.focus();
});
};
const emit = defineEmits(["update:modelValue", "change", "search", "create"]);
const selectItem = ref<SelectItem>({
value: !props.multiple
? props.modelValue
: props.modelValue
? ([] as any[]).concat(props.modelValue)
: [],
label: props.multiple ? [] : null,
multiple: props.multiple,
} as SelectItem);
watch(
() => selectItem.value.value,
(val) => {
emit("update:modelValue", val);
emit("change", val);
},
{ deep: true }
);
watch(props, () => {
let value = props.modelValue;
if (props.multiple) {
if (Array.isArray(value)) {
selectItem.value.value = value;
selectItem.value.label = value.map((o) => ItemsMap.value[o]);
} else {
console.error("多选时请传入数组值");
}
} else {
selectItem.value.value = value;
selectItem.value.label = ItemsMap.value[value] || "";
}
});
const txt = ref("");
const disabledItemMap: { [key: string | number]: boolean } = {};
const input = ref(false);
const value = computed({
set(v: any) {
txt.value = v;
emit("search", v);
},
const selectedValue = computed({
get() {
if (input.value) {
return txt.value;
}
return !selectItem.value.multiple && selectItem.value.value !== null
? selectItem.value.label
: null;
return props.modelValue;
},
});
const selectItemHandle = async function (
_selectItem: SelectItem,
isChecked?: boolean
) {
if (!props.multiple) {
openState.value = false;
set(val) {
emits('update:modelValue', val);
emits('change', val);
}
txt.value = "";
disabledItemMap[_selectItem.value as string | number] =
_selectItem.disabled as boolean;
if (typeof isChecked !== "boolean") {
props.multiple
? (selectItem.value.label as any[]).push(_selectItem.label)
: (selectItem.value.label = _selectItem.label);
return;
}
let values = selectItem.value.value;
if (props.multiple && Array.isArray(values)) {
const _values = values as any[];
const _labels = selectItem.value.label as any[];
if (isChecked) {
_values.push(_selectItem.value);
_labels.push(_selectItem.label);
} else {
_values.splice(_values.indexOf(_selectItem.value), 1);
_labels.splice(_labels.indexOf(_selectItem.label), 1);
}
selectItem.value.value = _values;
selectItem.value.label = _labels;
} else {
selectItem.value.value = _selectItem.value;
selectItem.value.label = _selectItem.label;
}
};
})
const removeItemHandle = function (e: MouseEvent, _selectItem: SelectItem) {
e.stopPropagation();
selectItemHandle(_selectItem, false);
};
const ItemsMap: Ref<{ [index: string]: string }> = ref({});
const selectItemPush = function (p: SelectItem) {
if (p.value !== null) {
//@ts-ignore
ItemsMap.value[p.value] = p.label;
}
};
const multiple = computed(() => {
return props.multiple;
})
const clear = () => {
emit("update:modelValue", props.multiple ? [] : "");
};
provide("selectItemHandle", selectItemHandle);
provide("selectItemPush", selectItemPush);
provide("selectItem", selectItem);
provide("keyword", txt);
provide('selectedValue', selectedValue);
provide('multiple', multiple);
</script>
<template>
<div
ref="selectRef"
class="layui-unselect layui-form-select"
:class="{
'layui-form-selected': openState,
'layui-form-select-multiple': multiple,
}"
:size="size"
>
<div class="layui-select-title" @click="open">
<input
type="text"
:placeholder="
selectItem.value !== null &&
Array.isArray(selectItem.value) &&
selectItem.value.length > 0
? ''
: emptyMessage ?? placeholder
"
:disabled="disabled"
v-model="value"
@input="input = true"
@blur="input = false"
:name="name"
:class="[
'layui-input',
'layui-unselect',
{ 'layui-disabled': disabled },
]"
/>
<span class="layui-input-icon-area">
<i
:class="[
'layui-icon layui-icon-triangle-d',
{ 'layui-disabled': disabled },
]"
:style="{ transform: `rotate(${openState ? 180 : 0}deg)` }"
></i>
<i
class="layui-icon layui-icon-close-fill"
v-if="(value || selectItem.label?.length) && allowClear"
@click.stop="clear"
></i>
</span>
<!-- 多选 -->
<div
v-if="selectItem.multiple && Array.isArray(selectItem.label)"
class="layui-multiple-select-row"
>
<div class="layui-multiple-select-badge">
<template v-for="(item, index) in selectItem.label" :key="index">
<lay-badge theme="gray">
<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>
</div>
</div>
</div>
<!-- 下拉内容 -->
<dl class="layui-anim layui-anim-upbit">
<div ref="multipleSearchInputRef" class="layui-multiple-select-input">
<lay-input
v-if="multiple"
v-model="value"
@input="input = true"
@blur="input = false"
:placeholder="placeholder"
prefix-icon="layui-icon-search"
allow-clear
>
</lay-input>
</div>
<template v-if="!multiple && showEmpty && !props.create">
<lay-select-option value="-1" :label="emptyMessage ?? placeholder" />
</template>
<template v-if="props.create">
<dd @click="emit('create', txt)">{{ txt }}</dd>
</template>
<template v-if="props.items">
<lay-select-option
v-for="(v, k) in props.items"
:key="k"
:value="v.value"
:label="v.label"
:disabled="v.disabled"
:keyword="v.keyword"
></lay-select-option>
</template>
<slot></slot>
</dl>
<div class="layui-select">
<lay-dropdown update-at-scroll>
<lay-tag-input v-if="multiple" v-model="selectedValue"></lay-tag-input>
<lay-input v-else :placeholder="placeholder" v-model="selectedValue"></lay-input>
<template #content>
<dl class="layui-select-options">
<template v-if="items">
<lay-select-option v-for="(item, index) in items" v-bind="item" :key="index"></lay-select-option>
</template>
<slot></slot>
</dl>
</template>
</lay-dropdown>
</div>
</template>

View File

@ -6,11 +6,10 @@ export default {
<script setup lang="ts">
import LayCheckbox from "../checkbox/index.vue";
import { SelectItem, SelectItemHandle, SelectItemPush } from "../../types";
import { computed, inject, onMounted, Ref, ref } from "vue";
import { computed, ComputedRef, inject, WritableComputedRef } from "vue";
export interface LaySelectOptionProps {
value: string | null | undefined | number;
value: string | object;
label: string;
keyword?: string;
disabled?: boolean;
@ -22,67 +21,33 @@ const props = withDefaults(defineProps<LaySelectOptionProps>(), {
label: "",
});
const selectItemHandle = inject("selectItemHandle") as SelectItemHandle;
const selectItem = inject("selectItem") as Ref<SelectItem>;
const selectItemPush = inject("selectItemPush") as Ref<SelectItemPush>;
const keyword = inject("keyword") as Ref<string>;
const selectedValue: WritableComputedRef<any> = inject("selectedValue") as WritableComputedRef<any>;
const multiple: ComputedRef = inject("multiple") as ComputedRef;
const selectHandle = function () {
!props.disabled && callSelectItemHandle(!selected.value);
};
const callSelectItemHandle = function (isChecked?: boolean) {
// console.log("callSelectItemHandle");
selectItemHandle(
{
value: props.value,
label: props.label,
disabled: props.disabled,
},
isChecked
);
};
const selected = computed({
get() {
const selectValues = selectItem.value.value;
if (Array.isArray(selectValues)) {
return (selectValues as any[]).indexOf(props.value) > -1;
}
return selectItem.value.value === props.value;
},
set(val) {},
});
const callSelectItemPush = function () {
let item = {
value: props.value,
label: props.label,
disabled: props.disabled,
};
// @ts-ignore
selectItemPush(item);
};
const search = ref("");
onMounted(() => {
search.value = props.keyword || props.label;
callSelectItemPush();
selected.value && callSelectItemHandle();
const handleSelect = () => {
if(!multiple.value) {
selectedValue.value = props.value;
}
}
const isSelected = computed(() => {
if (multiple.value) {
return selectedValue.value.indexOf(props.value) != -1;
} else {
return selectedValue.value === props.value;
}
});
</script>
<template>
<dd
v-show="keyword ? search.includes(keyword) : true"
:value="value"
:class="[{ 'layui-this': selected }, { 'layui-disabled': disabled }]"
@click="selectHandle"
>
<template v-if="selectItem.multiple">
<lay-checkbox
skin="primary"
v-model="selected"
@change="selectHandle"
:value="props.value"
/>
<dd class="layui-select-option" :class="{ 'layui-this': isSelected && !multiple }" @click="handleSelect">
<template v-if="multiple">
<lay-checkbox v-model="selectedValue" :value="value" skin="primary"
><slot>{{ label }}</slot></lay-checkbox
>
</template>
<template v-else>
<slot>{{ label }}</slot>
</template>
<slot>{{ label }}</slot>
</dd>
</template>

View File

@ -8,7 +8,7 @@ import "./index.less";
import { LayIcon } from "@layui/icons-vue";
import { computed, ref } from "vue";
import { TinyColor } from "@ctrl/tinycolor";
import { TAG_COLORS, tagType } from "./interface";
import { tagType } from "./interface";
export interface LayTagProps {
type?: tagType;

View File

@ -182,6 +182,12 @@ watch(
}
);
const moreCount = computed(() => {
if(tagData.value && computedTagData.value) {
return tagData.value.length - computedTagData.value.length
}
})
onMounted(() => {
handleResize();
});
@ -216,7 +222,7 @@ defineExpose({
<template v-if="computedTagData?.length != tagData?.length">
<LayToopTip :isDark="false" trigger="click" popperStyle="padding:6px">
<LayTag v-bind="tagProps" key="more" :closable="false" :size="size">
+{{tagData!.length - computedTagData!.length }}...
+{{ moreCount }}...
</LayTag>
<template #content>
<div class="layui-tag-input-collapsed-panel">

View File

@ -0,0 +1,3 @@
export const check = (arr: any[], value: any) => {
return arr.indexOf(value) > -1;
}

View File

@ -1,2 +1,3 @@
export * from "./domUtil";
export * from "./DomUtil";
export * from "./withInstall";
export * from "./arrayUtil";

View File

@ -19,6 +19,7 @@
<li>[新增] tag-input 标签输入框组件。</li>
<li>[新增] table 组件 header 插槽, 用于在工具栏与表格之间插入元素。</li>
<li>[新增] tabitem 组件 icon 属性, 用于自定义 tab-item 图标。</li>
<li>[修复] layout 组件只引入了 footer 时, layui-layout-vertical样式不生效。</li>
<li>[修复] cascader 外部清空modelValue, 内部displayValue不清空问题。</li>
<li>[修复] tolltip 组件 content 自动定位。</li>
<li>[修复] breadcrumb-item 组件无法正确传递 attrs 的问题。</li>