|
@@ -0,0 +1,189 @@
|
|
|
+<template>
|
|
|
+ <div class="choose-container">
|
|
|
+ <el-input ref="refInput" v-model="internalValue" v-bind="$attrs" readonly @input="changeValue" @click="showPopover">
|
|
|
+ <template #prepend>
|
|
|
+ <el-icon size="18">
|
|
|
+ <SvgIcon v-if="internalValue.startsWith('svg-')" :name="internalValue.substring(4)" />
|
|
|
+ <component v-else-if="internalValue" :is="internalValue" />
|
|
|
+ </el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+
|
|
|
+ <div ref="chooseDialogRef" class="choose-wrap" :class="{ show: isShow }">
|
|
|
+ <el-input v-model="search" class="w-50 m-2" placeholder="搜索图标" prefix-icon="Search" />
|
|
|
+
|
|
|
+ <el-tabs v-model="activeTab">
|
|
|
+ <el-tab-pane label="Element" name="element">
|
|
|
+ <el-scrollbar max-height="280">
|
|
|
+ <div class="choose-box">
|
|
|
+ <div class="choose-item" v-for="item in filteredElementIcons" :key="item">
|
|
|
+ <div @click="chooseIcon(item)">
|
|
|
+ <el-icon size="30">
|
|
|
+ <component :is="item" />
|
|
|
+ </el-icon>
|
|
|
+ {{ item }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-scrollbar>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="Svg" name="svg">
|
|
|
+ <el-scrollbar max-height="280">
|
|
|
+ <div class="choose-box">
|
|
|
+ <div class="choose-item" v-for="item in filteredSvgIcons" :key="item">
|
|
|
+ <div @click="chooseIcon(item)">
|
|
|
+ <el-icon size="30">
|
|
|
+ <SvgIcon :name="item.substring(4)" />
|
|
|
+ </el-icon>
|
|
|
+ {{ item }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-scrollbar>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts" name="IconChoose">
|
|
|
+// element icons
|
|
|
+import * as Icons from '@element-plus/icons-vue'
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: 'IconChoose'
|
|
|
+})
|
|
|
+type Props = {
|
|
|
+ modelValue: string
|
|
|
+}
|
|
|
+const props = defineProps<Props>()
|
|
|
+const emits = defineEmits<{
|
|
|
+ 'update:modelValue': [string]
|
|
|
+}>()
|
|
|
+
|
|
|
+const internalValue = ref(props.modelValue)
|
|
|
+
|
|
|
+const changeValue = () => {
|
|
|
+ emits('update:modelValue', internalValue.value)
|
|
|
+}
|
|
|
+
|
|
|
+const activeTab = ref('element')
|
|
|
+
|
|
|
+// 图标搜索
|
|
|
+const search = ref('')
|
|
|
+const elementIcons = Object.keys(Icons).map(key => {
|
|
|
+ return Icons[key as keyof typeof Icons].name
|
|
|
+})
|
|
|
+
|
|
|
+const svgIcons: string[] = []
|
|
|
+const svgIconsFiles = import.meta.glob('@/assets/icons/**/*.svg')
|
|
|
+for (const key in svgIconsFiles) {
|
|
|
+ const matchArray = key.match(/\/assets\/icons\/([^/]+)\.svg$/)
|
|
|
+ if (matchArray && matchArray.length >= 2) {
|
|
|
+ svgIcons.push(`svg-${matchArray[1]}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 过滤图标
|
|
|
+const filteredElementIcons = computed(() => {
|
|
|
+ return elementIcons.filter(item => item?.toLowerCase().includes(search.value.toLowerCase()))
|
|
|
+})
|
|
|
+
|
|
|
+const filteredSvgIcons = computed(() => {
|
|
|
+ return svgIcons.filter(item => item.toLowerCase().includes(search.value.toLowerCase()))
|
|
|
+})
|
|
|
+
|
|
|
+/**
|
|
|
+ * 弹窗
|
|
|
+ */
|
|
|
+const isShow = ref(false)
|
|
|
+
|
|
|
+/**
|
|
|
+ * 显示
|
|
|
+ */
|
|
|
+const showPopover = () => {
|
|
|
+ isShow.value = true
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 隐藏
|
|
|
+ */
|
|
|
+const hidePopover = () => {
|
|
|
+ isShow.value = false
|
|
|
+}
|
|
|
+
|
|
|
+// 弹窗外部触发
|
|
|
+const chooseDialogRef = ref(null)
|
|
|
+onClickOutside(chooseDialogRef, () => {
|
|
|
+ if (isShow.value) {
|
|
|
+ hidePopover()
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const chooseIcon = (name: string | undefined) => {
|
|
|
+ if (!name) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ hidePopover()
|
|
|
+ internalValue.value = name
|
|
|
+ emits('update:modelValue', name)
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.choose-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ .choose-wrap {
|
|
|
+ position: absolute;
|
|
|
+ margin-top: 6px;
|
|
|
+ padding: 10px;
|
|
|
+ z-index: 100;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: 0 0 12px rgb(0 0 0 / 5%);
|
|
|
+ background-color: #ffffff;
|
|
|
+ width: 100%;
|
|
|
+ height: 0;
|
|
|
+ opacity: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ transition:
|
|
|
+ height 0.3s,
|
|
|
+ opacity 0.3s;
|
|
|
+ box-sizing: border-box;
|
|
|
+ &.show {
|
|
|
+ opacity: 1;
|
|
|
+ height: 400px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.choose-box {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ margin: 0 10px;
|
|
|
+ .choose-item {
|
|
|
+ width: 25%;
|
|
|
+ padding: 5px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ > div {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 5px;
|
|
|
+ border: 1px solid rgb(204 204 204 / 50%);
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ &:hover {
|
|
|
+ > div {
|
|
|
+ border-color: rgb(0 150 136 / 50%);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.close-box {
|
|
|
+ position: absolute;
|
|
|
+ top: -15px;
|
|
|
+ right: -15px;
|
|
|
+}
|
|
|
+</style>
|