feat(projects): refactor icon system, unify icon usage [重构图标系统,统一图标用法]
7
.env
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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__'
|
||||
})
|
||||
];
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 322 B |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 387 B After Width: | Height: | Size: 387 B |
Before Width: | Height: | Size: 448 B After Width: | Height: | Size: 448 B |
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 702 B After Width: | Height: | Size: 702 B |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 371 B After Width: | Height: | Size: 371 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
1
src/assets/svg-icon/no-icon.svg
Normal 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 |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 326 B |
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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和localIcon,localIcon会优先渲染
|
||||
*/
|
||||
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
@ -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
|
||||
};
|
||||
};
|
@ -3,3 +3,4 @@ export * from './router';
|
||||
export * from './layout';
|
||||
export * from './events';
|
||||
export * from './echarts';
|
||||
export * from './icon';
|
||||
|
@ -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 {
|
||||
|
@ -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' })
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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' });
|
||||
|
@ -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' })
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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/'
|
||||
}
|
||||
}
|
||||
|
8
src/typings/env.d.ts
vendored
@ -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;
|
||||
/** 开启请求代理 */
|
||||
|
6
src/typings/route.d.ts
vendored
@ -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;
|
||||
/** 外链链接 */
|
||||
|
@ -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 }) });
|
||||
}
|
@ -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';
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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' });
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|