fix: 修复菜单图标选择器公网依赖和空白显示

- 使用本地 Iconify 图标数据并补齐菜单常用图标

- 修复未知图标降级和弹出层触发器结构
This commit is contained in:
2026-07-21 14:40:47 +08:00
parent 791649c7d5
commit 4c28eaf393
8 changed files with 198 additions and 69 deletions

View File

@@ -1,6 +1,6 @@
import { defineComponent, h } from 'vue';
import { Icon } from '@iconify/vue';
import { Icon } from '@iconify/vue/offline';
function createIconifyIcon(icon: string) {
return defineComponent({

View File

@@ -2,19 +2,82 @@ import antDesign from '@iconify/json/json/ant-design.json';
import ep from '@iconify/json/json/ep.json';
import mdi from '@iconify/json/json/mdi.json';
import {
addCollection,
addIcon,
addCollection as addOfflineCollection,
addIcon as addOfflineIcon,
Icon as IconifyIcon,
listIcons,
} from '@iconify/vue';
} from '@iconify/vue/offline';
import type {
IconifyIcon as IconifyIconData,
IconifyJSON,
} from '@iconify/vue/offline';
addCollection(antDesign);
addCollection(ep);
addCollection(mdi);
import { LOCAL_ICON_DATA } from './local-icons';
const LOCAL_ICON_NAMES = new Set<string>();
function getCollectionPrefix(data: IconifyJSON, prefix?: string | boolean) {
if (prefix === false) {
return '';
}
return `${typeof prefix === 'string' ? prefix : data.prefix}:`;
}
/**
* Register an icon collection in the offline Iconify store and local name index.
*/
function registerCollection(data: IconifyJSON, prefix?: string | boolean) {
addOfflineCollection(data, prefix);
const iconPrefix = getCollectionPrefix(data, prefix);
Object.keys(data.icons).forEach((name) => {
LOCAL_ICON_NAMES.add(`${iconPrefix}${name}`);
});
Object.keys(data.aliases ?? {}).forEach((name) => {
LOCAL_ICON_NAMES.add(`${iconPrefix}${name}`);
});
}
/**
* Register a single icon in the offline Iconify store.
*/
function registerIcon(name: string, data: IconifyIconData) {
addOfflineIcon(name, data);
LOCAL_ICON_NAMES.add(name);
}
/**
* Return locally registered icon names for the requested collection.
*/
function listIcons(provider = '', prefix = ''): string[] {
const providerPrefix = provider ? `@${provider}:` : '';
const iconPrefix = prefix ? `${prefix}:` : '';
return [...LOCAL_ICON_NAMES].filter((name) =>
name.startsWith(`${providerPrefix}${iconPrefix}`),
);
}
/**
* Check whether an icon name is available without a network request.
*/
function isLocalIcon(name: string) {
return LOCAL_ICON_NAMES.has(name);
}
registerCollection(antDesign);
registerCollection(ep);
registerCollection(mdi);
Object.entries(LOCAL_ICON_DATA).forEach(([name, data]) => {
registerIcon(name, data);
});
export * from './create-icon';
export * from './lucide';
export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue';
export { addCollection, addIcon, IconifyIcon, listIcons };
export type { IconifyIcon as IconifyIconStructure } from '@iconify/vue/offline';
export {
IconifyIcon,
isLocalIcon,
listIcons,
registerCollection as addCollection,
registerIcon as addIcon,
};

View File

@@ -0,0 +1,88 @@
import type { IconifyIcon } from '@iconify/vue/offline';
/**
* Icon names persisted in routes or database migrations that are not part of
* the small set of full local collections loaded by the application.
*/
export const LOCAL_ICON_DATA: Record<string, IconifyIcon> = {
'carbon:workspace': {
width: 32,
height: 32,
body: '<path fill="currentColor" d="M16 17v8H6v-8zm0-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2m11-9v5H17V6zm0-2H17a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2m0 13v5h-5v-5zm0-2h-5a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2v-5a2 2 0 0 0-2-2M11 6v5H6V6zm0-2H6a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V6z"/>',
},
'lucide:bot': {
width: 24,
height: 24,
body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2m16 0h2m-7-1v2m-6-2v2"/></g>',
},
'lucide:message-square': {
width: 24,
height: 24,
body: '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/>',
},
'lucide:badge-check': {
width: 24,
height: 24,
body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77a4 4 0 0 1 6.74 0a4 4 0 0 1 4.78 4.78a4 4 0 0 1 0 6.74a4 4 0 0 1-4.77 4.78a4 4 0 0 1-6.75 0a4 4 0 0 1-4.78-4.77a4 4 0 0 1 0-6.76"/><path d="m9 12l2 2l4-4"/></g>',
},
'lucide:user': {
width: 24,
height: 24,
body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></g>',
},
'lucide:lock': {
width: 24,
height: 24,
body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></g>',
},
'lucide:area-chart': {
width: 24,
height: 24,
body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M3 3v18h18"/><path d="M7 12v5h12V8l-5 5l-4-4Z"/></g>',
},
'lucide:layout-dashboard': {
width: 24,
height: 24,
body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></g>',
},
'lucide:book-open-text': {
width: 24,
height: 24,
body: '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7v14m4-9h2m-2-4h2M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4a4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3a3 3 0 0 0-3-3zm3-6h2M6 8h2"/>',
},
'lucide:copyright': {
width: 24,
height: 24,
body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M14.83 14.83a4 4 0 1 1 0-5.66"/></g>',
},
'clarity:database': {
width: 36,
height: 36,
body: '<path fill="currentColor" d="M33 6.69c-.18-3.41-9.47-4.33-15-4.33S3 3.29 3 6.78v22.59c0 3.49 9.43 4.43 15 4.43s15-.93 15-4.43zm-2 7.56c-.33.86-5.06 2.45-13 2.45a37.5 37.5 0 0 1-11-1.36v2.08a43.3 43.3 0 0 0 11 1.28c4 0 9.93-.48 13-2v5.17c-.33.86-5.06 2.45-13 2.45a37.5 37.5 0 0 1-11-1.4V25a43.3 43.3 0 0 0 11 1.28c4 0 9.93-.48 13-2v5.1c-.35.86-5.08 2.45-13 2.45S5.3 30.2 5 29.37V6.82c.3-.82 5-2.46 13-2.46c7.77 0 12.46 1.53 13 2.37c-.52.87-5.21 2.39-13 2.39A37.6 37.6 0 0 1 7 7.76v2.09a43.5 43.5 0 0 0 11 1.27c4 0 9.93-.48 13-2Z"/>',
},
'clarity:time-line': {
width: 36,
height: 36,
body: '<path fill="currentColor" d="M10 18c0-1.3-.8-2.4-2-2.8v-3.4c1.2-.4 2-1.5 2-2.8c0-1.7-1.3-3-3-3S4 7.3 4 9c0 1.3.8 2.4 2 2.8v3.4c-1.2.4-2 1.5-2 2.8s.8 2.4 2 2.8v3.4c-1.2.4-2 1.5-2 2.8c0 1.7 1.3 3 3 3s3-1.3 3-3c0-1.3-.8-2.4-2-2.8v-3.4c1.2-.4 2-1.5 2-2.8m21-8H15c-.6 0-1-.4-1-1s.4-1 1-1h16c.6 0 1 .4 1 1s-.4 1-1 1m0 9H15c-.6 0-1-.4-1-1s.4-1 1-1h16c.6 0 1 .4 1 1s-.4 1-1 1m0 9H15c-.6 0-1-.4-1-1s.4-1 1-1h16c.6 0 1 .4 1 1s-.4 1-1 1"/><path fill="none" d="M0 0h36v36H0z"/>',
},
'solar:widget-5-outline': {
width: 24,
height: 24,
body: '<g fill="none" stroke="currentColor" stroke-width="1.5"><rect width="8" height="8" x="2.75" y="2.75" rx="1.5"/><rect width="8" height="8" x="13.25" y="13.25" rx="1.5"/><rect width="8" height="5" x="13.25" y="2.75" rx="1.5"/><rect width="5" height="8" x="2.75" y="13.25" rx="1.5"/></g>',
},
'solar:stop-circle-outline': {
width: 24,
height: 24,
body: '<circle cx="12" cy="12" r="9.25" fill="none" stroke="currentColor" stroke-width="1.5"/><rect width="6" height="6" x="9" y="9" fill="currentColor" rx="1"/>',
},
'solar:plain-linear': {
width: 24,
height: 24,
body: '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21.4 3.7l-6.1 16.8l-3.6-6.2l-6.2-3.6z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="m21.4 3.7l-9.7 10.6"/>',
},
'fluent-mdl2:world-clock': {
width: 2048,
height: 2048,
body: '<path fill="currentColor" d="M896 768H512V256h128v384h256zm1152 640q0 87-22 168t-64 152t-100 130t-128 101t-152 66t-168 23q-134 0-251-49t-205-136t-139-204t-51-251q0-132 50-248t138-204t203-137t249-51q132 0 248 50t204 138t137 203t51 249m-640 512q21 0 37-15t29-40t21-53t15-58t9-53t5-37h-230q1 13 5 37t10 52t15 58t21 54t27 39t36 16m125-384q3-64 3-128q0-63-3-128h-250q-3 65-3 128q0 64 3 128zm-637-128q0 32 4 64t12 64h243q-6-128 0-256H912q-8 32-12 64t-4 64m512-512q-19 0-34 15t-27 40t-21 54t-15 58t-11 53t-5 36h225q-1-11-5-34t-11-52t-16-59t-21-54t-27-41t-32-16m253 384q3 64 3 128t-2 128h242q8-32 12-64t4-64t-4-64t-12-64zm190-128q-43-75-108-131t-145-89q20 53 32 108t20 112zm-637-218q-78 32-142 88t-107 130h200q7-56 18-110t31-108m-249 730q42 73 105 129t142 88q-20-52-30-107t-17-110zm643 215q77-32 139-87t104-128h-198q-5 55-15 109t-30 106M640 0q88 0 170 23t153 64t129 100t100 130t65 153t23 170h-128q0-106-40-199t-110-162t-163-110t-199-41t-199 40t-162 110t-110 163t-41 199t40 199t110 162t163 110t199 41v128q-88 0-170-23t-153-64t-129-100T88 963T23 810T0 640q0-132 50-248t138-204T391 51T640 0"/>',
},
};

View File

@@ -3,7 +3,7 @@ import type { Component } from 'vue';
import { computed } from 'vue';
import { IconDefault, IconifyIcon } from '@easyflow-core/icons';
import { IconDefault, IconifyIcon, isLocalIcon } from '@easyflow-core/icons';
import {
isFunction,
isHttpUrl,
@@ -21,6 +21,12 @@ const isRemoteIcon = computed(() => {
return isString(props.icon) && isHttpUrl(props.icon);
});
const isUnavailableIcon = computed(() => {
return (
isString(props.icon) && !isRemoteIcon.value && !isLocalIcon(props.icon)
);
});
const isComponent = computed(() => {
const { icon } = props;
return !isString(icon) && (isObject(icon) || isFunction(icon));
@@ -30,6 +36,10 @@ const isComponent = computed(() => {
<template>
<component :is="icon as Component" v-if="isComponent" v-bind="$attrs" />
<img v-else-if="isRemoteIcon" :src="icon as string" v-bind="$attrs" />
<IconifyIcon v-else-if="icon" v-bind="$attrs" :icon="icon as string" />
<IconifyIcon
v-else-if="icon && !isUnavailableIcon"
v-bind="$attrs"
:icon="icon as string"
/>
<IconDefault v-else-if="fallback" v-bind="$attrs" />
</template>

View File

@@ -22,6 +22,7 @@ interface Props extends PopoverRootProps {
contentClass?: ClassType;
contentProps?: PopoverContentProps;
triggerClass?: ClassType;
triggerAsChild?: boolean;
}
const props = withDefaults(defineProps<Props>(), {});
@@ -34,6 +35,7 @@ const delegatedProps = computed(() => {
contentClass: _,
contentProps: _cProps,
triggerClass: _tClass,
triggerAsChild: _tAsChild,
...delegated
} = props;
@@ -45,16 +47,15 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<PopoverRoot v-bind="forwarded">
<PopoverTrigger :class="triggerClass">
<PopoverTrigger :class="triggerClass" :as-child="triggerAsChild">
<slot name="trigger"></slot>
<PopoverContent
:class="contentClass"
class="side-content z-popup"
v-bind="contentProps"
>
<slot></slot>
</PopoverContent>
</PopoverTrigger>
<PopoverContent
:class="contentClass"
class="side-content z-popup"
v-bind="contentProps"
>
<slot></slot>
</PopoverContent>
</PopoverRoot>
</template>