Prechádzať zdrojové kódy

feat: IconPicker 组件

lanceJiang 1 rok pred
rodič
commit
a578ca3c99

+ 47 - 0
src/components/IconPicker/iconsData.ts

@@ -0,0 +1,47 @@
+import svgIcons from 'virtual:svg-icons-names'
+import * as ElIcons from '@element-plus/icons-vue'
+import leIconData from './json_leIconData.ts'
+// 来自于 src/assets/icons 的svg: 'icon-[dir]-[name]'
+
+// const icons = [] as string[]
+// const modules = import.meta.glob('../../assets/icons/*.svg')
+// for (const path in modules) {
+// 	const p = path.split('assets/icons/')[1].split('.svg')[0]
+// 	icons.push(p)
+// }
+export const svgIconData = {
+	label: 'SvgIcon',
+	prefix: 'icon-',
+	icons: svgIcons.map((icon: string) => icon.replace('icon-', ''))
+}
+// 来自于 element svg 链接
+export const elIconData = {
+	label: 'ElIcon',
+	prefix: '',
+	icons: Object.keys(ElIcons)
+}
+export {
+	// 来自于 le-iconfont svg 链接: 'le-[name]'
+	leIconData
+}
+
+export const iconTypeOptions = [
+	// svg
+	{
+		label: svgIconData.label,
+		value: svgIconData.prefix,
+		icons: svgIconData.icons
+	},
+	// le-icon
+	{
+		label: leIconData.label,
+		value: leIconData.prefix,
+		icons: leIconData.icons
+	},
+	// el-icon
+	{
+		label: elIconData.label,
+		value: elIconData.prefix,
+		icons: elIconData.icons
+	}
+]

+ 211 - 0
src/components/IconPicker/index.vue

@@ -0,0 +1,211 @@
+<template>
+	<el-input v-model="currentSelect" class="le-icon-picker" :disabled="disabled" clearable placeholder="请输入图标名称">
+		<template #append>
+			<el-popover v-model:visible="visible" popper-class="le-icon-picker_popover" placement="bottom-end" trigger="click">
+				<template #reference>
+					<MenuIcon class="icon-selected" :icon-class="currentSelect || 'Grid'" />
+				</template>
+				<el-input v-model="searchValue" placeholder="搜索图标" @clear="searchIconsHandler" @input="searchIconsHandler">
+					<template #prepend>
+						<el-select v-model="curIconType" :teleported="false" placeholder="Select" style="width: 100px">
+							<el-option v-for="item in iconTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+						</el-select>
+					</template>
+				</el-input>
+				<el-scrollbar class="local_scrollbar">
+					<div class="icon-list">
+						<template v-if="currentList.length">
+							<div
+								v-for="(item, index) in currentList"
+								:key="index"
+								:title="item"
+								:class="`item ${item === currentSelect_name ? 'active' : ''}`"
+								@click="updateIcon(item)"
+							>
+								<!--<le-icon class="local_icon" :icon-class="`${curIconType}${item}`" />-->
+								<MenuIcon class="local_icon" :icon-class="`${curIconType}${item}`" />
+								<span class="text text-overflow_ellipsis_line_2">{{ item }}</span>
+							</div>
+						</template>
+						<LeNoData v-else size="small"></LeNoData>
+					</div>
+				</el-scrollbar>
+			</el-popover>
+		</template>
+	</el-input>
+</template>
+
+<script setup lang="ts">
+import MenuIcon from '@/layout/components/Menu/MenuIcon.vue'
+// import svgIcons from 'virtual:svg-icons-names'
+import { iconTypeOptions } from './iconsData.ts'
+import { useDebounceFn } from '@vueuse/core'
+import { ref, watch, computed } from 'vue'
+const props = defineProps({
+	modelValue: {
+		type: String,
+		default: ''
+	},
+	disabled: {
+		type: Boolean,
+		default: false
+	}
+})
+
+const curIconType = ref(iconTypeOptions[0].value)
+const curIconType_icons = ref(iconTypeOptions[0].icons)
+const currentList = ref(iconTypeOptions[0].icons)
+watch(curIconType, value => {
+	const option = iconTypeOptions.find(v => v.value === value)
+	if (option) {
+		curIconType_icons.value = option.icons
+		currentList.value = option.icons
+		// 更新当前filter
+		update_currentList()
+	}
+})
+
+console.error(curIconType_icons, 'curIconType_icons')
+
+const currentSelect = ref('')
+const currentSelect_type = ref<string>()
+// 去除前缀的 name
+const currentSelect_name = computed(() => {
+	let name = currentSelect.value
+	if (name) {
+		// icon, le-iconfont
+		const bool = ['icon-', 'le-'].some(prefix => {
+			if (name.startsWith(prefix)) {
+				name = name.replace(prefix, '')
+				currentSelect_type.value = prefix
+				return true
+			}
+		})
+		if (!bool) {
+			// eslint-disable-next-line vue/no-side-effects-in-computed-properties
+			currentSelect_type.value = ''
+		}
+	} else {
+		// eslint-disable-next-line vue/no-side-effects-in-computed-properties
+		currentSelect_type.value = undefined
+	}
+	return name
+})
+const visible = ref(false)
+watch(visible, (bool: boolean) => {
+	// 弹窗开启时 进行定位筛查
+	if (bool) {
+		searchValue.value = currentSelect_name.value
+		// 更新当前 icon类型
+		if (typeof currentSelect_type.value === 'string') {
+			curIconType.value = currentSelect_type.value
+		}
+		// 更新当前filter
+		update_currentList()
+	}
+})
+watch(
+	() => props.modelValue,
+	(val: string) => {
+		currentSelect.value = val
+	}
+)
+const searchValue = ref('')
+const update_currentList = () => {
+	if (searchValue.value) {
+		currentList.value = curIconType_icons.value.filter(item => item.indexOf(searchValue.value) !== -1)
+	} else {
+		currentList.value = curIconType_icons.value
+	}
+}
+// 搜索
+const searchIconsHandler = useDebounceFn(update_currentList, 80)
+const emit = defineEmits(['update:modelValue', 'change'])
+
+function updateIcon(name: string) {
+	const realName = curIconType.value + name
+	// const realName = name
+	emit('update:modelValue', realName)
+	currentSelect.value = realName
+	document.body.click()
+}
+
+function reset() {
+	currentSelect.value = ''
+	currentList.value = curIconType_icons
+}
+
+defineExpose({
+	reset
+})
+</script>
+
+<style lang="scss">
+.#{$prefix}icon-picker {
+	.el-input-group__append {
+		padding: 0;
+	}
+	// 选中的icon
+	.icon-selected {
+		font-size: 16px;
+		margin: 0 0.5rem;
+		cursor: pointer;
+	}
+	// 弹窗
+	&_popover {
+		&.el-popper {
+			//width: 360px;
+			min-width: 300px;
+			line-height: 1;
+			//padding: 12px 0 0;
+		}
+		//.el-scrollbar__view {
+		.local_scrollbar {
+			margin-top: 10px;
+			box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
+			border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
+		}
+		.el-scrollbar__wrap {
+			overflow-y: scroll;
+			overflow-x: hidden;
+			max-height: 22vh;
+		}
+		.icon-list {
+			margin-top: 8px;
+			//height: 200px;
+			//overflow-y: scroll;
+			.item {
+				//height: 30px;
+				//line-height: 30px;
+				padding-bottom: 5px;
+				cursor: pointer;
+				width: 20%;
+				float: left;
+				//color: #999;
+				//color: var(--el-text-color-secondary);
+				color: var(--el-text-color-placeholder);
+				display: flex;
+				flex-direction: column;
+				align-items: center;
+				justify-content: center;
+				&.active {
+					color: var(--el-color-primary);
+				}
+			}
+			.local_icon {
+				height: 24px;
+				width: 24px;
+				font-size: 24px;
+				//width: 16px;
+				//margin-right: 5px;
+			}
+			.text {
+				font-size: 12px;
+				padding: 2px 2px 0;
+				-webkit-line-clamp: 1;
+				// flex: 1;
+			}
+		}
+	}
+}
+</style>

+ 263 - 0
src/components/IconPicker/json_leIconData.ts

@@ -0,0 +1,263 @@
+// copy le-iconfont.css 全局替换
+export default {
+	label: 'LeIcon',
+	prefix: 'le-',
+	icons: [
+		'en_o',
+		'zh-cn_o',
+		'lang_en',
+		'lang_zh-cn',
+		'suoxiao1',
+		'fangda1',
+		'text-size',
+		'fangda',
+		'suoxiao',
+		'frozen',
+		'add_box',
+		'add_sku',
+		'add_location',
+		'log',
+		'mix',
+		'small_parcel',
+		'freight',
+		'air_cargo',
+		'ocean_cargo',
+		'truck',
+		'arrow_up_1',
+		'arrow_down_1',
+		'invoiced',
+		'shipstation',
+		'shopify',
+		'ebay',
+		'amazon',
+		'ops',
+		'default_brand',
+		'put_away_1',
+		'time_2',
+		'log_user',
+		'detail',
+		'collapse',
+		'expand',
+		'right',
+		'left',
+		'up',
+		'down',
+		'internal_warehouse',
+		'external_warehouse',
+		'successful',
+		'failure',
+		'tips',
+		'question',
+		'warning',
+		'warning_1',
+		'loading',
+		'locked',
+		'unlocked',
+		'system',
+		'client_2',
+		'sign',
+		'insurance',
+		'commercial',
+		'residential_1',
+		'commercial_1',
+		'residential',
+		'reconciliation',
+		'account_name',
+		'email',
+		'company',
+		'address',
+		'phone',
+		'time_1',
+		'log_history',
+		'international_air_transport',
+		'print_1',
+		'create_1',
+		'template_file',
+		'invoice_count',
+		'pod',
+		'attachment',
+		'store',
+		'total_invoice_count',
+		'total_shipment_count',
+		'shipment_match',
+		'total_amount',
+		'upload',
+		'wire_1',
+		'check_1',
+		'check',
+		'wire',
+		'invoiced_error',
+		'pdf',
+		'memo',
+		'star',
+		'share',
+		'america',
+		'canada',
+		'component',
+		'bundle',
+		'image',
+		'single_package',
+		'packages',
+		'wallet',
+		'out_for_delivery',
+		'label_created',
+		'delivered',
+		'tradeber',
+		'usps',
+		'firstmile',
+		'pitney_bowes',
+		'fedex',
+		'stamps',
+		'ups',
+		'dhl',
+		'tips_2',
+		'no_data',
+		'transaction_record',
+		'no_tracking',
+		'picking_1',
+		'no_result',
+		'put_away',
+		'bill',
+		'receive',
+		'checkbox_checked',
+		'picking',
+		'checkbox',
+		'packing',
+		'checkbox_half_choice',
+		'confirm_shipment',
+		'radio_checked_disable',
+		'radio_checked',
+		'change_address',
+		'radio',
+		'delete',
+		'a-duoxuankuang-bufenxuanzhongjinyong',
+		'add',
+		'warehouse',
+		'search',
+		'my_brand',
+		'switch',
+		'brand_setup',
+		'confirm',
+		'subscription',
+		'close',
+		'email_1',
+		'edit',
+		'client_invoice',
+		'arrow_down',
+		'back',
+		'arrow_up',
+		'hot',
+		'arrow_left',
+		'claim',
+		'arrow_right',
+		'mark_success',
+		'filter',
+		'mark_failed',
+		'clear',
+		'sorting',
+		'download',
+		'limited_time',
+		'calendar',
+		'time',
+		'view_detail',
+		'order',
+		'order_operation',
+		'drag',
+		'rate_shopping',
+		'export',
+		'import',
+		'other',
+		'print',
+		'copy',
+		'view',
+		'view_close',
+		'hide_column',
+		'save',
+		'audit',
+		'reset_password',
+		'weight',
+		'calculator',
+		'quantity',
+		'cancel',
+		'test',
+		'clear_1',
+		'successflly',
+		'create',
+		'validate',
+		'preview',
+		'shopping_cart',
+		'refresh',
+		'reupload',
+		'awaiting_shipment_update1',
+		'reactivate',
+		'refresh_2',
+		'switch_1',
+		'enlarge',
+		'enter',
+		'review',
+		'not_approved',
+		'contract_setup',
+		'pushpin',
+		'download_template',
+		'setting_1',
+		'transshipment_order',
+		'setting',
+		'send_email',
+		'printing_setup',
+		'add_funds',
+		'approved',
+		'consumption',
+		'submit',
+		'withdraw',
+		'entrance',
+		'label',
+		'misc_active',
+		'navigation',
+		'refresh_1',
+		'expand_1',
+		'add_1',
+		'collapse_1',
+		'add_2',
+		'language',
+		'notice',
+		'finance',
+		'calculator_1',
+		'unfold',
+		'fold',
+		'navigation_1',
+		'navigation_2',
+		'product',
+		'category',
+		'store_aliases',
+		'listings',
+		'supplier',
+		'create_shipment',
+		'shipment_history',
+		'return',
+		'tracking',
+		'invoice_list',
+		'invoice_detail',
+		'dispute_shipment',
+		'outbound_order',
+		'inbound_order',
+		'inventory',
+		'location',
+		'warehouse_job',
+		'management',
+		'integration',
+		'shipping',
+		'account',
+		'plugin',
+		'loan',
+		'refund_shipment',
+		'payment',
+		'log_entry',
+		'transaction',
+		'statement',
+		'shipping_charge',
+		'income',
+		'add_3',
+		'remove',
+		'normal'
+	]
+}

+ 3 - 3
src/layout/components/Menu/MenuIcon.vue

@@ -15,9 +15,9 @@ const icon = computed(() => {
 	let type = {
 		// 来自于 src/assets/icons 的svg: 'icon-[dir]-[name]'
 		icon: 'icon',
-		// 来自于 iconfont svg 链接: 'le-[name]'
-		le: 'iconfont'
-		// // 来自于 element svg 链接: 'el-[name]' => 实际icon name为: [name] el-仅用于标记
+		// 来自于 le-iconfont svg 链接: 'le-[name]'
+		le: 'le-iconfont'
+		// // 来自于 element svg 链接
 		// el: 'element'
 	}[iconClass.split('-')[0]]
 	// 匹配不到icon- & le- 默认element

+ 1 - 1
src/main.ts

@@ -22,7 +22,7 @@ import 'virtual:svg-icons-register'
 	// const existIconVersion = [...d.querySelectorAll('.le-icon_svg')].map(v => v.getAttribute('version')).includes(version)
 	const existIconVersion = false
 	if (!existIconVersion) {
-		/** update 最新 iconfont(.css && .js) */
+		/** update 最新 le-iconfont(.css && .js) */
 		const origin_prefix = '//at.alicdn.com/t/c/font_4091949_0v9i1byqy04'
 		const link = d.createElement('link')
 		link.rel = 'stylesheet'

+ 1 - 1
src/router/index.ts

@@ -183,7 +183,7 @@ export const constantRoutes: Array<AppRouteRecordRaw> = [
 							name: 'adminManage',
 							component: () => import('@/views/demo/adminManage/index'),
 							// component: 'demo/adminManage/index',
-							meta: { title: 'demo_adminManage', icon: 'setting' }
+							meta: { title: 'demo_adminManage', icon: 'Setting' }
 						}
 					]
 				}

+ 6 - 1
src/types/global.d.ts

@@ -11,7 +11,7 @@ declare namespace Menu {
 	interface MetaProps {
 		// 关于icon 描述:
 		// 1.来自本地src/assets/icons 的svg: 'icon-[dir]-[name]'
-		// 2.iconfont svg 链接: 'le-[name]'
+		// 2.le-iconfont svg 链接: 'le-[name]'
 		// 3. 匹配不到icon- & le- 默认element
 		icon: string
 		title: string
@@ -93,3 +93,8 @@ declare module '*.vue' {
 	const Component: DefineComponent<{}, {}, any>
 	export default Component
 }
+
+declare module 'virtual:*' {
+	const result: any
+	export default result
+}

+ 1 - 1
src/views/components/index.vue

@@ -17,7 +17,7 @@
 		<InputNumberDemo v-if="true" />
 		<LeDraggableNestDemo v-if="true" />
 
-		<div class="common_title">iconfont && LeIcon</div>
+		<div class="common_title">le-iconfont && LeIcon</div>
 		<div class="content">
 			<!--  单色样式类  -->
 			<!--也可拼接 对应icon文件夹注入的icon 文件-->