feat(projects): refactor icon system, unify icon usage [重构图标系统,统一图标用法]

This commit is contained in:
Soybean 2022-09-23 00:15:00 +08:00
parent fe8cab3d1c
commit 811f820644
47 changed files with 270 additions and 156 deletions

7
.env
View File

@ -11,3 +11,10 @@ VITE_AUTH_ROUTE_MODE=dynamic
# 路由首页(根路由重定向), 用于static模式的权限路由dynamic模式取决于后端返回的路由首页
VITE_ROUTE_HOME_PATH=/dashboard/analysis
# iconify图标作为组件的前缀
VITE_ICON_PREFFIX=icon
# 本地SVG图标作为组件的前缀, 请注意一定要包含 VITE_ICON_PREFFIX
# 格式 {VITE_ICON_PREFFIX}-{本地图标集合名称}
VITE_ICON_LOCAL_PREFFIX=icon-local

View File

@ -15,7 +15,7 @@ import compress from './compress';
* @param viteEnv -
*/
export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
const plugins = [vue(), vueJsx(), VitePWA(), html(viteEnv), ...unplugin, unocss(), mock, progress()];
const plugins = [vue(), vueJsx(), VitePWA(), html(viteEnv), ...unplugin(viteEnv), unocss(), mock, progress()];
if (viteEnv.VITE_VISUALIZER === 'Y') {
plugins.push(visualizer as PluginOption);

View File

@ -7,29 +7,38 @@ import { FileSystemIconLoader } from 'unplugin-icons/loaders';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import { getSrcPath } from '../utils';
const srcPath = getSrcPath();
export default function unplugin(viteEnv: ImportMetaEnv) {
const { VITE_ICON_PREFFIX, VITE_ICON_LOCAL_PREFFIX } = viteEnv;
const customIconPath = `${srcPath}/assets/svg`;
const srcPath = getSrcPath();
const localIconPath = `${srcPath}/assets/svg-icon`;
export default [
VueMacros(),
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader(customIconPath)
},
scale: 1,
defaultClass: 'inline-block'
}),
Components({
dts: 'src/typings/components.d.ts',
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })]
}),
createSvgIconsPlugin({
iconDirs: [customIconPath],
symbolId: 'icon-custom-[dir]-[name]',
inject: 'body-last',
customDomId: '__CUSTOM_SVG_ICON__'
})
];
/** 本地svg图标集合名称 */
const collectionName = VITE_ICON_LOCAL_PREFFIX.replace(`${VITE_ICON_PREFFIX}-`, '');
return [
VueMacros(),
Icons({
compiler: 'vue3',
customCollections: {
[collectionName]: FileSystemIconLoader(localIconPath)
},
scale: 1,
defaultClass: 'inline-block'
}),
Components({
dts: 'src/typings/components.d.ts',
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
resolvers: [
NaiveUiResolver(),
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFFIX })
]
}),
createSvgIconsPlugin({
iconDirs: [localIconPath],
symbolId: `${VITE_ICON_LOCAL_PREFFIX}-[dir]-[name]`,
inject: 'body-last',
customDomId: '__SVG_ICON_LOCAL__'
})
];
}

View File

@ -74,7 +74,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: {
title: '项目文档',
requiresAuth: true,
customIcon: 'logo'
localIcon: 'logo'
}
},
{
@ -83,7 +83,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: {
title: '项目文档(外链)',
requiresAuth: true,
customIcon: 'logo',
localIcon: 'logo',
href: 'https://docs.soybean.pro/'
}
}
@ -250,7 +250,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: {
title: '图标',
requiresAuth: true,
customIcon: 'custom-icon'
localIcon: 'custom-icon'
}
},
{
@ -555,7 +555,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: {
title: 'vue文档',
requiresAuth: true,
icon: 'mdi:vuejs'
icon: 'logos:vue'
}
},
{
@ -565,16 +565,36 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: {
title: 'vite文档',
requiresAuth: true,
icon: 'simple-icons:vite'
icon: 'logos:vitejs'
}
},
{
name: 'document_naive',
path: '/document/naive',
component: 'self',
meta: {
title: 'naive文档',
requiresAuth: true,
icon: 'logos:naiveui'
}
},
{
name: 'document_project',
path: '/document/project',
component: 'self',
meta: {
title: '项目文档',
requiresAuth: true,
localIcon: 'logo'
}
},
{
name: 'document_project-link',
path: '/document/project-link',
meta: {
title: '项目文档(外链)',
requiresAuth: true,
icon: 'mdi:file-link-outline',
localIcon: 'logo',
href: 'https://docs.soybean.pro/'
}
}
@ -741,7 +761,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
meta: {
title: '图标',
requiresAuth: true,
customIcon: 'custom-icon'
localIcon: 'custom-icon'
}
},
{

View File

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 282 B

View File

Before

Width:  |  Height:  |  Size: 322 B

After

Width:  |  Height:  |  Size: 322 B

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 387 B

View File

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 448 B

View File

Before

Width:  |  Height:  |  Size: 351 B

After

Width:  |  Height:  |  Size: 351 B

View File

Before

Width:  |  Height:  |  Size: 702 B

After

Width:  |  Height:  |  Size: 702 B

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 371 B

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--flat-color-icons" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 48 48"><g fill="#FFCC80"><path d="M13 22H8v-8.5c0-1.4 1.1-2.5 2.5-2.5s2.5 1.1 2.5 2.5V22zm7 0h-5V7.5C15 6.1 16.1 5 17.5 5S20 6.1 20 7.5V22zm7 0h-5V5.5C22 4.1 23.1 3 24.5 3S27 4.1 27 5.5V22zm7 0h-5V8.5C29 7.1 30.1 6 31.5 6S34 7.1 34 8.5V22zm-1.9 21l-5-5l10-10c1.4-1.4 3.6-1.4 4.9 0c1.4 1.4 1.4 3.6 0 4.9L32.1 43z"></path><path d="M29 21c0 .6-.4 1-1 1s-1-.4-1-1h-5c0 .6-.4 1-1 1s-1-.4-1-1h-5c0 .6-.4 1-1 1s-1-.4-1-1H8v16c0 4.4 3.6 8 8 8h11.2c3.7 0 6.8-3 6.8-6.8V21h-5z"></path></g><g fill="#F44336"><path d="m15.413 28.971l2.474-2.474l10.605 10.605l-2.474 2.474z"></path><path d="m25.993 26.504l2.475 2.474l-10.605 10.605l-2.475-2.474z"></path></g></svg>

After

Width:  |  Height:  |  Size: 879 B

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 326 B

View File

@ -7,13 +7,13 @@
</div>
<div v-show="isEmpty" class="absolute-center">
<div class="relative">
<icon-custom-empty-data :class="iconClass" />
<icon-local-empty-data :class="iconClass" />
<p class="absolute-lb w-full text-center" :class="descClass">{{ emptyDesc }}</p>
</div>
</div>
<div v-show="!network" class="absolute-center">
<div class="relative" :class="{ 'cursor-pointer': showNetworkReload }" @click="handleReload">
<icon-custom-network-error :class="iconClass" />
<icon-local-network-error :class="iconClass" />
<p class="absolute-lb w-full text-center" :class="descClass">{{ networkErrorDesc }}</p>
</div>
</div>

View File

@ -1,9 +1,9 @@
<template>
<div class="flex-col-center wh-full">
<div class="text-400px text-primary">
<icon-custom-no-permission v-if="type === '403'" />
<icon-custom-not-found v-if="type === '404'" />
<icon-custom-service-error v-if="type === '500'" />
<icon-local-no-permission v-if="type === '403'" />
<icon-local-not-found v-if="type === '404'" />
<icon-local-service-error v-if="type === '500'" />
</div>
<router-link :to="{ name: routeHomePath }">
<n-button type="primary">回到首页</n-button>

View File

@ -1,6 +1,6 @@
<template>
<icon-custom-logo-fill v-if="fill" />
<icon-custom-logo v-else />
<icon-local-logo-fill v-if="fill" />
<icon-local-logo v-else />
</template>
<script lang="ts" setup>

View File

@ -3,7 +3,7 @@
<template #trigger>
<n-input v-model:value="modelValue" readonly placeholder="点击选择图标">
<template #suffix>
<Icon :icon="modelValue ? modelValue : emptyIcon" class="text-30px p-5px" />
<svg-icon :icon="selectedIcon" class="text-30px p-5px" />
</template>
</n-input>
</template>
@ -12,10 +12,10 @@
</template>
<div v-if="iconsList.length > 0" class="grid grid-cols-9 h-auto overflow-auto">
<template v-for="iconItem in iconsList" :key="iconItem">
<Icon
<svg-icon
:icon="iconItem"
class="border-1px border-[#d9d9d9] text-30px m-2px p-5px"
:style="{ 'border-color': modelValue === iconItem ? theme.themeColor : '' }"
:class="{ 'border-primary': modelValue === iconItem }"
@click="handleChange(iconItem)"
/>
</template>
@ -26,8 +26,6 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { Icon } from '@iconify/vue';
import { useThemeStore } from '@/store';
defineOptions({ name: 'IconSelect' });
@ -50,11 +48,6 @@ interface Emits {
const emit = defineEmits<Emits>();
const theme = useThemeStore();
const searchValue = ref('');
const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
const modelValue = computed({
get() {
return props.value;
@ -64,6 +57,12 @@ const modelValue = computed({
}
});
const selectedIcon = computed(() => modelValue.value || props.emptyIcon);
const searchValue = ref('');
const iconsList = computed(() => props.icons.filter(v => v.includes(searchValue.value)));
function handleChange(iconItem: string) {
modelValue.value = iconItem;
}

View File

@ -1,26 +1,53 @@
<template>
<svg aria-hidden="true" width="1em" height="1em">
<use :xlink:href="symbolId" fill="currentColor" />
</svg>
<template v-if="renderLocalIcon">
<svg aria-hidden="true" width="1em" height="1em" v-bind="bindAttrs">
<use :xlink:href="symbolId" fill="currentColor" />
</svg>
</template>
<template v-else>
<Icon :icon="icon" v-bind="bindAttrs" />
</template>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, useAttrs } from 'vue';
import { Icon } from '@iconify/vue';
defineOptions({ name: 'SvgIcon' });
/**
* 图标组件
* - 支持iconify和本地svg图标
* - 同时传递了icon和localIconlocalIcon会优先渲染
*/
interface Props {
/** 前缀 */
prefix?: string;
/** 图标名称(图片的文件名) */
icon: string;
/** 图标名称 */
icon?: string;
/** 本地svg的文件名 */
localIcon?: string;
}
const props = withDefaults(defineProps<Props>(), {
prefix: 'icon-custom'
const props = defineProps<Props>();
const attrs = useAttrs();
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || ''
}));
const symbolId = computed(() => {
const { VITE_ICON_LOCAL_PREFFIX: preffix } = import.meta.env;
const defaultLocalIcon = 'no-icon';
const icon = props.localIcon || defaultLocalIcon;
return `#${preffix}-${icon}`;
});
const symbolId = computed(() => `#${props.prefix}-${props.icon}`);
/** 渲染本地icon */
const renderLocalIcon = computed(() => props.localIcon || !props.icon);
</script>
<style scoped></style>

60
src/composables/icon.ts Normal file
View File

@ -0,0 +1,60 @@
import { h } from 'vue';
import SvgIcon from '@/components/custom/SvgIcon.vue';
/**
*
* - vue的render函数
*/
export const useIconRender = () => {
interface IconConfig {
/**
* (iconify图标的名称)
* - mdi-account mdi:account
*/
icon?: string;
/**
* svg图标文件名(assets/svg-icon文件夹下)
*/
localIcon?: string;
/** 图标颜色 */
color?: string;
/** 图标大小 */
fontSize?: number;
}
interface IconStyle {
color?: string;
fontSize?: string;
}
/**
*
* @param config
* @property icon - (iconify图标的名称), mdi-account mdi:account
* @property localIcon - svg图标文件名(assets/svg-icon文件夹下)
* @property color -
* @property fontSize -
*/
const iconRender = (config: IconConfig) => {
const { color, fontSize, icon, localIcon } = config;
const style: IconStyle = {};
if (color) {
style.color = color;
}
if (fontSize) {
style.fontSize = `${fontSize}px`;
}
if (!icon && !localIcon) {
window.console.warn('没有传递图标名称请确保给icon或localIcon传递有效值!');
}
return () => h(SvgIcon, { icon, localIcon, style });
};
return {
iconRender
};
};

View File

@ -3,3 +3,4 @@ export * from './router';
export * from './layout';
export * from './events';
export * from './echarts';
export * from './icon';

View File

@ -10,8 +10,7 @@
<n-thing class="px-15px" :class="{ 'opacity-30': item.isRead }">
<template #avatar>
<n-avatar v-if="item.avatar" :src="item.avatar" />
<svg-icon v-else-if="item.svgIcon" class="text-34px text-primary" :icon="item.svgIcon" />
<Icon v-else-if="item.icon" class="text-34px text-primary" :icon="item.icon" />
<svg-icon v-else class="text-34px text-primary" :icon="item.icon" :local-icon="item.svgIcon" />
</template>
<template #header>
<n-ellipsis :line-clamp="1">
@ -36,8 +35,6 @@
</n-scrollbar>
</template>
<script lang="ts" setup>
import { Icon } from '@iconify/vue';
defineOptions({ name: 'MessageList' });
interface Props {

View File

@ -1,7 +1,7 @@
<template>
<n-dropdown :options="options" @select="handleDropdown">
<hover-container class="px-12px" :inverted="theme.header.inverted">
<icon-custom-avatar class="text-32px" />
<icon-local-avatar class="text-32px" />
<span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span>
</hover-container>
</n-dropdown>
@ -10,18 +10,19 @@
<script lang="ts" setup>
import type { DropdownOption } from 'naive-ui';
import { useAuthStore, useThemeStore } from '@/store';
import { iconifyRender } from '@/utils';
import { useIconRender } from '@/composables';
defineOptions({ name: 'UserAvatar' });
const auth = useAuthStore();
const theme = useThemeStore();
const { iconRender } = useIconRender();
const options: DropdownOption[] = [
{
label: '用户中心',
key: 'user-center',
icon: iconifyRender('carbon:user-avatar')
icon: iconRender({ icon: 'carbon:user-avatar' })
},
{
type: 'divider',
@ -30,7 +31,7 @@ const options: DropdownOption[] = [
{
label: '退出登录',
key: 'logout',
icon: iconifyRender('carbon:logout')
icon: iconRender({ icon: 'carbon:logout' })
}
];

View File

@ -11,7 +11,7 @@
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<Icon :icon="item.meta?.icon ?? 'mdi:bookmark-minus-outline'" />
<svg-icon :icon="item.meta.icon" :local-icon="item.meta.localIcon" />
<span class="flex-1 ml-5px">{{ item.meta?.title }}</span>
<icon-ant-design-enter-outlined class="icon text-20px p-2px mr-3px" />
</div>
@ -22,7 +22,6 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Icon } from '@iconify/vue';
import { useThemeStore } from '@/store';
defineOptions({ name: 'SearchResult' });

View File

@ -14,7 +14,7 @@
import { computed } from 'vue';
import type { DropdownOption } from 'naive-ui';
import { useAppStore, useTabStore } from '@/store';
import { iconifyRender } from '@/utils';
import { useIconRender } from '@/composables';
defineOptions({ name: 'ContextMenu' });
@ -42,6 +42,7 @@ const emit = defineEmits<Emits>();
const app = useAppStore();
const tab = useTabStore();
const { iconRender } = useIconRender();
const dropdownVisible = computed({
get() {
@ -66,33 +67,33 @@ const options = computed<Option[]>(() => [
label: '重新加载',
key: 'reload-current',
disabled: props.currentPath !== tab.activeTab,
icon: iconifyRender('ant-design:reload-outlined')
icon: iconRender({ icon: 'ant-design:reload-outlined' })
},
{
label: '关闭',
key: 'close-current',
disabled: props.currentPath === tab.homeTab.fullPath,
icon: iconifyRender('ant-design:close-outlined')
icon: iconRender({ icon: 'ant-design:close-outlined' })
},
{
label: '关闭其他',
key: 'close-other',
icon: iconifyRender('ant-design:column-width-outlined')
icon: iconRender({ icon: 'ant-design:column-width-outlined' })
},
{
label: '关闭左侧',
key: 'close-left',
icon: iconifyRender('mdi:format-horizontal-align-left')
icon: iconRender({ icon: 'mdi:format-horizontal-align-left' })
},
{
label: '关闭右侧',
key: 'close-right',
icon: iconifyRender('mdi:format-horizontal-align-right')
icon: iconRender({ icon: 'mdi:format-horizontal-align-right' })
},
{
label: '关闭所有',
key: 'close-all',
icon: iconifyRender('ant-design:line-outlined')
icon: iconRender({ icon: 'ant-design:line-outlined' })
}
]);

View File

@ -13,7 +13,11 @@
@close="tab.removeTab(item.fullPath)"
@contextmenu="handleContextMenu($event, item.fullPath)"
>
<Icon v-if="item.meta.icon" :icon="item.meta.icon" class="inline-block align-text-bottom mr-4px text-16px" />
<svg-icon
:icon="item.meta.icon"
:local-icon="item.meta.localIcon"
class="inline-block align-text-bottom mr-4px text-16px"
/>
{{ item.meta.title }}
</component>
</div>
@ -29,7 +33,6 @@
<script setup lang="ts">
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { ButtonTab, ChromeTab } from '@soybeanjs/vue-admin-tab';
import { Icon } from '@iconify/vue';
import { useTabStore, useThemeStore } from '@/store';
import { ContextMenu } from './components';

View File

@ -10,7 +10,7 @@ const document: AuthRoute.Route = {
meta: {
title: 'vue文档',
requiresAuth: true,
icon: 'mdi:vuejs'
icon: 'logos:vue'
}
},
{
@ -20,16 +20,36 @@ const document: AuthRoute.Route = {
meta: {
title: 'vite文档',
requiresAuth: true,
icon: 'simple-icons:vite'
icon: 'logos:vitejs'
}
},
{
name: 'document_naive',
path: '/document/naive',
component: 'self',
meta: {
title: 'naive文档',
requiresAuth: true,
icon: 'logos:naiveui'
}
},
{
name: 'document_project',
path: '/document/project',
component: 'self',
meta: {
title: '项目文档',
requiresAuth: true,
localIcon: 'logo'
}
},
{
name: 'document_project-link',
path: '/document/project-link',
meta: {
title: '项目文档(外链)',
requiresAuth: true,
icon: 'mdi:file-link-outline',
localIcon: 'logo',
href: 'https://docs.soybean.pro/'
}
}

View File

@ -35,6 +35,14 @@ interface ImportMetaEnv {
readonly VITE_AUTH_ROUTE_MODE: 'static' | 'dynamic';
/** 路由首页的路径 */
readonly VITE_ROUTE_HOME_PATH: Exclude<AuthRoute.RoutePath, '/' | '/not-found-page' | '/:pathMatch(.*)*'>;
/** iconify图标作为组件的前缀 */
readonly VITE_ICON_PREFFIX: string;
/**
* SVG图标作为组件的前缀, VITE_ICON_PREFFIX
* - {VITE_ICON_PREFFIX}-{}
* - icon-local
*/
readonly VITE_ICON_LOCAL_PREFFIX: string;
/** 后端服务的环境类型 */
readonly VITE_SERVICE_ENV?: ServiceEnvType;
/** 开启请求代理 */

View File

@ -96,10 +96,10 @@ declare namespace AuthRoute {
permissions?: Auth.RoleType[];
/** 缓存页面 */
keepAlive?: boolean;
/** 菜单和面包屑对应的图标 */
/** 菜单和面包屑对应的图标(iconify图标名称) */
icon?: string;
/** 自定义的菜单和面包屑对应的图标 */
customIcon?: string;
/** 使用本地svg作为的菜单和面包屑对应的图标(assets/svg-icon文件夹的的svg文件名) */
localIcon?: string;
/** 是否在菜单中隐藏(一些列表、表格的详情页面需要通过参数跳转,所以不能显示在菜单中) */
hide?: boolean;
/** 外链链接 */

View File

@ -1,39 +0,0 @@
import { h } from 'vue';
import { NIcon } from 'naive-ui';
import { Icon } from '@iconify/vue';
import SvgIcon from '@/components/custom/SvgIcon.vue';
/**
* iconify
* @param icon -
* @param color -
* @param fontSize -
*/
export function iconifyRender(icon: string, color?: string, fontSize?: number) {
const style: { color?: string; fontSize?: string } = {};
if (color) {
style.color = color;
}
if (fontSize) {
style.fontSize = `${fontSize}px`;
}
return () => h(NIcon, null, { default: () => h(Icon, { icon, style }) });
}
/**
*
* @param icon -
* @param color -
* @param fontSize -
*/
export function customIconRender(icon: string, color?: string, fontSize?: number) {
const style: { color?: string; fontSize?: string } = {};
if (color) {
style.color = color;
}
if (fontSize) {
style.fontSize = `${fontSize}px`;
}
return () => h(NIcon, null, { default: () => h(SvgIcon, { icon, style }) });
}

View File

@ -2,6 +2,5 @@ export * from './typeof';
export * from './color';
export * from './number';
export * from './object';
export * from './icon';
export * from './pattern';
export * from './theme';

View File

@ -1,4 +1,4 @@
import { customIconRender, iconifyRender } from '../common';
import { useIconRender } from '@/composables';
/** 路由不转换菜单 */
function hideInMenu(route: AuthRoute.Route) {
@ -9,18 +9,25 @@ function hideInMenu(route: AuthRoute.Route) {
function addPartialProps(config: {
menu: GlobalMenuOption;
icon?: string;
customIcon?: string;
localIcon?: string;
children?: GlobalMenuOption[];
}) {
const { iconRender } = useIconRender();
const item = { ...config.menu };
if (config.icon) {
Object.assign(item, { icon: iconifyRender(config.icon) });
const { icon, localIcon, children } = config;
if (localIcon) {
Object.assign(item, { icon: iconRender({ localIcon }) });
}
if (config.customIcon) {
Object.assign(item, { icon: customIconRender(config.customIcon) });
if (icon) {
Object.assign(item, { icon: iconRender({ icon }) });
}
if (config.children) {
Object.assign(item, { children: config.children });
if (children) {
Object.assign(item, { children });
}
return item;
}
@ -46,7 +53,7 @@ export function transformAuthRouteToMenu(routes: AuthRoute.Route[]): GlobalMenuO
routePath: path
},
icon: meta.icon,
customIcon: meta.customIcon,
localIcon: meta.localIcon,
children: menuChildren
});

View File

@ -13,7 +13,7 @@
:style="`--icon-margin: ${button.props.circle ? 0 : 6}px`"
>
<template v-if="button.icon" #icon>
<Icon :icon="button.icon" />
<svg-icon :icon="button.icon" />
</template>
{{ button.label }}
</n-button>
@ -36,7 +36,6 @@
<script setup lang="ts">
import type { ButtonProps } from 'naive-ui';
import { Icon } from '@iconify/vue';
import { useLoading } from '@/hooks';
interface ButtonDetail {

View File

@ -4,7 +4,7 @@
<gradient-bg class="h-100px" :start-color="item.colors[0]" :end-color="item.colors[1]">
<h3 class="text-16px">{{ item.title }}</h3>
<div class="flex justify-between pt-12px">
<Icon :icon="item.icon" class="text-32px" />
<svg-icon :icon="item.icon" class="text-32px" />
<count-to
:prefix="item.unit"
:start-value="1"
@ -18,7 +18,6 @@
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { GradientBg } from './components';
defineOptions({ name: 'DashboardAnalysisDataCard' });

View File

@ -2,7 +2,7 @@
<n-card :bordered="false" class="rounded-16px shadow-sm">
<div class="flex-y-center justify-between">
<div class="flex-y-center">
<icon-custom-avatar class="text-70px" />
<icon-local-avatar class="text-70px" />
<div class="pl-12px">
<h3 class="text-18px font-semibold">早安{{ auth.userInfo.userName }}, 今天又是充满活力的一天</h3>
<p class="leading-30px text-[#999]">今日多云转晴20 - 25</p>

View File

@ -2,14 +2,12 @@
<div
class="flex-col-center p-12px border-1px border-[#efeff5] dark:border-[#ffffff17] rounded-4px hover:shadow-sm cursor-pointer"
>
<Icon :icon="icon" :style="{ color: iconColor }" class="text-30px" />
<svg-icon :icon="icon" :style="{ color: iconColor }" class="text-30px" />
<p class="py-8px text-16px">{{ label }}</p>
</div>
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue';
defineOptions({ name: 'DashboardWorkbenchMainShortcutsCard' });
interface Props {

View File

@ -4,7 +4,7 @@
@click="handleOpenSite"
>
<header class="flex-y-center">
<Icon :icon="icon" :style="{ color: iconColor }" class="text-30px" />
<svg-icon :icon="icon" :style="{ color: iconColor }" class="text-30px" />
<h3 class="pl-12px text-18px font-semibold">{{ name }}</h3>
</header>
<p class="py-8px h-56px text-[#999]">{{ description }}</p>
@ -15,8 +15,6 @@
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue';
defineOptions({ name: 'DashboardWorkbenchMainTechnologyCard' });
interface Props {

View File

@ -19,7 +19,7 @@
<n-list>
<n-list-item v-for="item in activity" :key="item.id">
<template #prefix>
<icon-custom-avatar class="text-48px" />
<icon-local-avatar class="text-48px" />
</template>
<n-thing :title="item.content" :description="item.time" />
</n-list-item>
@ -37,7 +37,7 @@
</n-grid>
</n-card>
<n-card title="创意" :bordered="false" size="small" class="shadow-sm rounded-16px">
<icon-custom-banner class="text-400px text-primary" />
<icon-local-banner class="text-400px text-primary" />
</n-card>
</n-space>
</n-grid-item>

View File

@ -4,7 +4,7 @@
<div class="grid grid-cols-10">
<template v-for="item in icons" :key="item">
<div class="mt-5px flex-x-center">
<Icon :icon="item" class="text-30px" />
<svg-icon :icon="item" class="text-30px" />
</div>
</template>
</div>
@ -18,20 +18,21 @@
</n-card>
<n-card title="自定义图标示例" class="mt-10px shadow-sm rounded-16px">
<div class="pb-12px text-16px">
在src/assets/svg文件夹下的svg文件通过在template里面以 icon-custom-{文件名} 直接渲染
在src/assets/svg-icon文件夹下的svg文件通过在template里面以 icon-local-{文件名} 直接渲染,
其中icon-local为.env文件里的 VITE_ICON_LOCAL_PREFFIX
</div>
<div class="grid grid-cols-10">
<div class="mt-5px flex-x-center">
<icon-custom-activity class="text-40px text-success" />
<icon-local-activity class="text-40px text-success" />
</div>
<div class="mt-5px flex-x-center">
<icon-custom-cast class="text-20px text-error" />
<icon-local-cast class="text-20px text-error" />
</div>
</div>
<div class="py-12px text-16px">通过SvgIcon组件动态渲染, 菜单通过meta的customIcon属性渲染自定义图标</div>
<div class="py-12px text-16px">通过SvgIcon组件动态渲染, 菜单通过meta的localIcon属性渲染自定义图标</div>
<div class="grid grid-cols-10">
<div v-for="(item, index) in customIcons" :key="index" class="mt-5px flex-x-center">
<svg-icon :icon="item" class="text-30px text-primary" />
<div v-for="(fileName, index) in localIcons" :key="index" class="mt-5px flex-x-center">
<svg-icon :local-icon="fileName" class="text-30px text-primary" />
</div>
</div>
</n-card>
@ -40,12 +41,11 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { Icon } from '@iconify/vue';
import { icons } from './icons';
const selectValue = ref('');
const customIcons = ['custom-icon', 'activity', 'at-sign', 'cast', 'chrome', 'copy', 'wind'];
const localIcons = ['custom-icon', 'activity', 'at-sign', 'cast', 'chrome', 'copy', 'wind'];
</script>
<style scoped></style>