feat: iconSelect图标选择器和动态路由菜单支持element图标

This commit is contained in:
郝先瑞
2024-02-20 17:44:01 +08:00
parent a7207ce83d
commit b96826c1f8
2 changed files with 139 additions and 54 deletions

View File

@@ -1,18 +1,25 @@
<template> <template>
<div ref="iconSelectRef" :style="'width:' + width" class="relative"> <div ref="iconSelectRef" :style="'width:' + width" class="relative">
<el-input <el-input
v-model="inputValue" v-model="selectedIcon"
readonly readonly
placeholder="点击选择图标" placeholder="点击选择图标"
@click="visible = !visible" @click="popoverVisible = !popoverVisible"
> >
<template #prepend> <template #prepend>
<svg-icon :icon-class="inputValue" /> <template v-if="selectedIcon && selectedIcon.startsWith('el-icon-')">
<el-icon>
<component :is="renderIcon(selectedIcon.replace('el-icon-', ''))" />
</el-icon>
</template>
<template v-else>
<svg-icon :icon-class="selectedIcon" />
</template>
</template> </template>
</el-input> </el-input>
<el-popover <el-popover
:visible="visible" :popoverVisible="popoverVisible"
placement="bottom-end" placement="bottom-end"
trigger="click" trigger="click"
:width="width" :width="width"
@@ -20,43 +27,64 @@
<template #reference> <template #reference>
<div <div
class="cursor-pointer text-[#999] absolute-tr height-[32px] leading-[32px] px-1" class="cursor-pointer text-[#999] absolute-tr height-[32px] leading-[32px] px-1"
@click="visible = !visible" @click="popoverVisible = !popoverVisible"
> >
<i-ep-caret-top v-show="visible" /> <i-ep-caret-top v-show="popoverVisible" />
<i-ep-caret-bottom v-show="!visible" /> <i-ep-caret-bottom v-show="!popoverVisible" />
</div> </div>
</template> </template>
<!-- 下拉选择弹窗 --> <!-- 下拉选择弹窗 -->
<div ref="iconSelectDialogRef"> <div ref="popoverContentRef">
<el-input <el-input
v-model="filterValue" v-model="searchText"
placeholder="搜索图标" placeholder="搜索图标"
clearable clearable
@input="handleFilter" @input="filterIcons"
/> />
<el-divider border-style="dashed" /> <el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="SVG 图标" name="svg">
<el-scrollbar height="300px"> <el-scrollbar height="300px">
<ul class="flex flex-wrap"> <ul class="flex flex-wrap">
<li <li
v-for="(iconName, index) in filterIconNames" v-for="(icon, index) in filteredSvgIcons"
:key="index" :key="'svg-' + index"
class="p-2 border border-solid border-gray-300 cursor-pointer hover:border-color-[var(--el-color-primary)] hover:text-[var(--el-color-primary)] hover:scale-110 hover:transition-all mt-1 ml-1" class="icon-item"
@click="handleSelect(iconName)" @click="selectIcon(icon)"
> >
<el-tooltip :content="iconName" placement="bottom" effect="light"> <el-tooltip :content="icon" placement="bottom" effect="light">
<svg-icon :icon-class="iconName" /> <svg-icon :icon-class="icon" />
</el-tooltip> </el-tooltip>
</li> </li>
</ul> </ul>
</el-scrollbar> </el-scrollbar>
</el-tab-pane>
<el-tab-pane label="Element 图标" name="element">
<el-scrollbar height="300px">
<ul class="flex flex-wrap">
<li
v-for="(icon, name) in elementIcons"
:key="name"
class="icon-item"
@click="selectIcon(name)"
>
<el-icon>
<component :is="icon" />
</el-icon>
</li>
</ul>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</div> </div>
</el-popover> </el-popover>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
import { ElTabPane } from "element-plus";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: String, type: String,
@@ -71,45 +99,56 @@ const props = defineProps({
}); });
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);
const inputValue = toRef(props, "modelValue"); const selectedIcon = toRef(props, "modelValue");
const width = toRef(props, "width"); const width = toRef(props, "width");
const visible = ref(false); // 弹窗显示状态
const allIconNames: string[] = []; // 所有的图标名称集合
const filterValue = ref(""); // 筛选的值
const filterIconNames = ref<string[]>([]); // 过滤后的图标名称集合
const iconSelectRef = ref(); const iconSelectRef = ref();
const iconSelectDialogRef = ref(); const popoverContentRef = ref();
const activeTab = ref("svg"); // 默认激活的Tab
const searchText = ref(""); // 筛选的值
const popoverVisible = ref(false); // 弹窗显示状态
const svgIcons: string[] = []; // SVG图标集合
const filteredSvgIcons = ref<string[]>([]); // 过滤后的SVG图标名称集合
const elementIcons = ref(ElementPlusIconsVue); // Element Plus图标集合
function handleTabClick(tabPane: any) {
activeTab.value = tabPane.name;
filterIcons();
}
/** /**
* icon 筛选 * icon 筛选
*/ */
function handleFilter() { function filterIcons() {
if (filterValue.value) { if (activeTab.value === "svg") {
filterIconNames.value = allIconNames.filter((iconName) => // 过滤SVG图标逻辑
iconName.includes(filterValue.value) filteredSvgIcons.value = searchText.value
); ? svgIcons.filter((iconName) => iconName.includes(searchText.value))
: svgIcons;
} else { } else {
filterIconNames.value = allIconNames; // 过滤Element Plus图标逻辑 TODO
} }
} }
/** /**
* icon 选择 * 选择图标
*/ */
function handleSelect(iconName: string) { function selectIcon(iconName: string) {
if (activeTab.value === "element") {
iconName = "el-icon-" + iconName;
}
emit("update:modelValue", iconName); emit("update:modelValue", iconName);
visible.value = false; popoverVisible.value = false;
} }
/** /**
* 点击容器外的区域关闭弹窗 VueUse onClickOutside * 点击容器外的区域关闭弹窗 VueUse onClickOutside
*/ */
onClickOutside(iconSelectRef, () => (visible.value = false), { onClickOutside(iconSelectRef, () => (popoverVisible.value = false), {
ignore: [iconSelectDialogRef], ignore: [popoverContentRef],
}); });
/** /**
@@ -117,16 +156,45 @@ onClickOutside(iconSelectRef, () => (visible.value = false), {
*/ */
function loadIcons() { function loadIcons() {
const icons = import.meta.glob("../../assets/icons/*.svg"); const icons = import.meta.glob("../../assets/icons/*.svg");
for (const icon in icons) { for (const path in icons) {
const iconName = icon.split("assets/icons/")[1].split(".svg")[0]; const iconName = path.replace(/.*\/(.*)\.svg$/, "$1");
allIconNames.push(iconName); svgIcons.push(iconName);
} }
filterIconNames.value = allIconNames; filteredSvgIcons.value = svgIcons;
} }
/**
* 渲染图标组件
*/
type IconNames = keyof typeof ElementPlusIconsVue;
const renderIcon = (iconName: string) => {
const iconComponent = ElementPlusIconsVue[iconName as IconNames];
if (iconComponent) {
return h(resolveComponent(iconComponent.name));
}
return null;
};
onMounted(() => { onMounted(() => {
loadIcons(); loadIcons();
}); });
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss">
.icon-item {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
margin: 4px;
cursor: pointer;
border: 1px solid #dcdfe6;
border-radius: 4px;
transition: all 0.3s;
}
.icon-item:hover {
border-color: #409eff;
scale: 1.2;
}
</style>

View File

@@ -1,5 +1,7 @@
<template> <template>
<el-icon v-if="icon && icon.includes('el-icon')" class="sub-el-icon" /> <el-icon v-if="icon && icon.startsWith('el-icon')" class="sub-el-icon">
<component :is="renderIcon(icon.replace('el-icon-', ''))" />
</el-icon>
<SvgIcon v-else-if="icon" :icon-class="icon" /> <SvgIcon v-else-if="icon" :icon-class="icon" />
<SvgIcon v-else icon-class="menu" /> <SvgIcon v-else icon-class="menu" />
<span v-if="title" class="ml-1">{{ translateRouteTitle(title) }}</span> <span v-if="title" class="ml-1">{{ translateRouteTitle(title) }}</span>
@@ -8,6 +10,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { translateRouteTitle } from "@/utils/i18n"; import { translateRouteTitle } from "@/utils/i18n";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
defineProps({ defineProps({
icon: { icon: {
type: String, type: String,
@@ -18,12 +22,25 @@ defineProps({
default: "", default: "",
}, },
}); });
/**
* 渲染图标组件
*/
type IconNames = keyof typeof ElementPlusIconsVue;
const renderIcon = (iconName: string) => {
const iconComponent = ElementPlusIconsVue[iconName as IconNames];
if (iconComponent) {
return h(resolveComponent(iconComponent.name));
}
return null;
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.sub-el-icon { .sub-el-icon {
width: 1em; width: 14px !important;
height: 1em; margin-right: 0 !important;
font-size: 14px !important;
color: currentcolor; color: currentcolor;
} }