feat(projects): refactor icon system, unify icon usage [重构图标系统,统一图标用法]
7
.env
@ -11,3 +11,10 @@ VITE_AUTH_ROUTE_MODE=dynamic
|
|||||||
|
|
||||||
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
# 路由首页(根路由重定向), 用于static模式的权限路由,dynamic模式取决于后端返回的路由首页
|
||||||
VITE_ROUTE_HOME_PATH=/dashboard/analysis
|
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 - 环境变量配置
|
* @param viteEnv - 环境变量配置
|
||||||
*/
|
*/
|
||||||
export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] {
|
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') {
|
if (viteEnv.VITE_VISUALIZER === 'Y') {
|
||||||
plugins.push(visualizer as PluginOption);
|
plugins.push(visualizer as PluginOption);
|
||||||
|
@ -7,29 +7,38 @@ import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
|||||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||||
import { getSrcPath } from '../utils';
|
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 [
|
/** 本地svg图标集合名称 */
|
||||||
VueMacros(),
|
const collectionName = VITE_ICON_LOCAL_PREFFIX.replace(`${VITE_ICON_PREFFIX}-`, '');
|
||||||
Icons({
|
|
||||||
compiler: 'vue3',
|
return [
|
||||||
customCollections: {
|
VueMacros(),
|
||||||
custom: FileSystemIconLoader(customIconPath)
|
Icons({
|
||||||
},
|
compiler: 'vue3',
|
||||||
scale: 1,
|
customCollections: {
|
||||||
defaultClass: 'inline-block'
|
[collectionName]: FileSystemIconLoader(localIconPath)
|
||||||
}),
|
},
|
||||||
Components({
|
scale: 1,
|
||||||
dts: 'src/typings/components.d.ts',
|
defaultClass: 'inline-block'
|
||||||
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
}),
|
||||||
resolvers: [NaiveUiResolver(), IconsResolver({ customCollections: ['custom'], componentPrefix: 'icon' })]
|
Components({
|
||||||
}),
|
dts: 'src/typings/components.d.ts',
|
||||||
createSvgIconsPlugin({
|
types: [{ from: 'vue-router', names: ['RouterLink', 'RouterView'] }],
|
||||||
iconDirs: [customIconPath],
|
resolvers: [
|
||||||
symbolId: 'icon-custom-[dir]-[name]',
|
NaiveUiResolver(),
|
||||||
inject: 'body-last',
|
IconsResolver({ customCollections: [collectionName], componentPrefix: VITE_ICON_PREFFIX })
|
||||||
customDomId: '__CUSTOM_SVG_ICON__'
|
]
|
||||||
})
|
}),
|
||||||
];
|
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: {
|
meta: {
|
||||||
title: '项目文档',
|
title: '项目文档',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
customIcon: 'logo'
|
localIcon: 'logo'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -83,7 +83,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
|||||||
meta: {
|
meta: {
|
||||||
title: '项目文档(外链)',
|
title: '项目文档(外链)',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
customIcon: 'logo',
|
localIcon: 'logo',
|
||||||
href: 'https://docs.soybean.pro/'
|
href: 'https://docs.soybean.pro/'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,7 +250,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
|||||||
meta: {
|
meta: {
|
||||||
title: '图标',
|
title: '图标',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
customIcon: 'custom-icon'
|
localIcon: 'custom-icon'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -555,7 +555,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'vue文档',
|
title: 'vue文档',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'mdi:vuejs'
|
icon: 'logos:vue'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -565,16 +565,36 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'vite文档',
|
title: 'vite文档',
|
||||||
requiresAuth: true,
|
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',
|
name: 'document_project',
|
||||||
path: '/document/project',
|
path: '/document/project',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '项目文档',
|
||||||
|
requiresAuth: true,
|
||||||
|
localIcon: 'logo'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_project-link',
|
||||||
|
path: '/document/project-link',
|
||||||
meta: {
|
meta: {
|
||||||
title: '项目文档(外链)',
|
title: '项目文档(外链)',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'mdi:file-link-outline',
|
localIcon: 'logo',
|
||||||
href: 'https://docs.soybean.pro/'
|
href: 'https://docs.soybean.pro/'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -741,7 +761,7 @@ export const routeModel: Record<Auth.RoleType, AuthRoute.Route[]> = {
|
|||||||
meta: {
|
meta: {
|
||||||
title: '图标',
|
title: '图标',
|
||||||
requiresAuth: true,
|
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>
|
||||||
<div v-show="isEmpty" class="absolute-center">
|
<div v-show="isEmpty" class="absolute-center">
|
||||||
<div class="relative">
|
<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>
|
<p class="absolute-lb w-full text-center" :class="descClass">{{ emptyDesc }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!network" class="absolute-center">
|
<div v-show="!network" class="absolute-center">
|
||||||
<div class="relative" :class="{ 'cursor-pointer': showNetworkReload }" @click="handleReload">
|
<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>
|
<p class="absolute-lb w-full text-center" :class="descClass">{{ networkErrorDesc }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex-col-center wh-full">
|
<div class="flex-col-center wh-full">
|
||||||
<div class="text-400px text-primary">
|
<div class="text-400px text-primary">
|
||||||
<icon-custom-no-permission v-if="type === '403'" />
|
<icon-local-no-permission v-if="type === '403'" />
|
||||||
<icon-custom-not-found v-if="type === '404'" />
|
<icon-local-not-found v-if="type === '404'" />
|
||||||
<icon-custom-service-error v-if="type === '500'" />
|
<icon-local-service-error v-if="type === '500'" />
|
||||||
</div>
|
</div>
|
||||||
<router-link :to="{ name: routeHomePath }">
|
<router-link :to="{ name: routeHomePath }">
|
||||||
<n-button type="primary">回到首页</n-button>
|
<n-button type="primary">回到首页</n-button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<icon-custom-logo-fill v-if="fill" />
|
<icon-local-logo-fill v-if="fill" />
|
||||||
<icon-custom-logo v-else />
|
<icon-local-logo v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-input v-model:value="modelValue" readonly placeholder="点击选择图标">
|
<n-input v-model:value="modelValue" readonly placeholder="点击选择图标">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<Icon :icon="modelValue ? modelValue : emptyIcon" class="text-30px p-5px" />
|
<svg-icon :icon="selectedIcon" class="text-30px p-5px" />
|
||||||
</template>
|
</template>
|
||||||
</n-input>
|
</n-input>
|
||||||
</template>
|
</template>
|
||||||
@ -12,10 +12,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<div v-if="iconsList.length > 0" class="grid grid-cols-9 h-auto overflow-auto">
|
<div v-if="iconsList.length > 0" class="grid grid-cols-9 h-auto overflow-auto">
|
||||||
<template v-for="iconItem in iconsList" :key="iconItem">
|
<template v-for="iconItem in iconsList" :key="iconItem">
|
||||||
<Icon
|
<svg-icon
|
||||||
:icon="iconItem"
|
:icon="iconItem"
|
||||||
class="border-1px border-[#d9d9d9] text-30px m-2px p-5px"
|
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)"
|
@click="handleChange(iconItem)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@ -26,8 +26,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { useThemeStore } from '@/store';
|
|
||||||
|
|
||||||
defineOptions({ name: 'IconSelect' });
|
defineOptions({ name: 'IconSelect' });
|
||||||
|
|
||||||
@ -50,11 +48,6 @@ interface Emits {
|
|||||||
|
|
||||||
const emit = defineEmits<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({
|
const modelValue = computed({
|
||||||
get() {
|
get() {
|
||||||
return props.value;
|
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) {
|
function handleChange(iconItem: string) {
|
||||||
modelValue.value = iconItem;
|
modelValue.value = iconItem;
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg aria-hidden="true" width="1em" height="1em">
|
<template v-if="renderLocalIcon">
|
||||||
<use :xlink:href="symbolId" fill="currentColor" />
|
<svg aria-hidden="true" width="1em" height="1em" v-bind="bindAttrs">
|
||||||
</svg>
|
<use :xlink:href="symbolId" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<Icon :icon="icon" v-bind="bindAttrs" />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, useAttrs } from 'vue';
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
|
|
||||||
defineOptions({ name: 'SvgIcon' });
|
defineOptions({ name: 'SvgIcon' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图标组件
|
||||||
|
* - 支持iconify和本地svg图标
|
||||||
|
* - 同时传递了icon和localIcon,localIcon会优先渲染
|
||||||
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
/** 前缀 */
|
/** 图标名称 */
|
||||||
prefix?: string;
|
icon?: string;
|
||||||
/** 图标名称(图片的文件名) */
|
/** 本地svg的文件名 */
|
||||||
icon: string;
|
localIcon?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = defineProps<Props>();
|
||||||
prefix: 'icon-custom'
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<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 './layout';
|
||||||
export * from './events';
|
export * from './events';
|
||||||
export * from './echarts';
|
export * from './echarts';
|
||||||
|
export * from './icon';
|
||||||
|
@ -10,8 +10,7 @@
|
|||||||
<n-thing class="px-15px" :class="{ 'opacity-30': item.isRead }">
|
<n-thing class="px-15px" :class="{ 'opacity-30': item.isRead }">
|
||||||
<template #avatar>
|
<template #avatar>
|
||||||
<n-avatar v-if="item.avatar" :src="item.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" />
|
<svg-icon v-else class="text-34px text-primary" :icon="item.icon" :local-icon="item.svgIcon" />
|
||||||
<Icon v-else-if="item.icon" class="text-34px text-primary" :icon="item.icon" />
|
|
||||||
</template>
|
</template>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-ellipsis :line-clamp="1">
|
<n-ellipsis :line-clamp="1">
|
||||||
@ -36,8 +35,6 @@
|
|||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'MessageList' });
|
defineOptions({ name: 'MessageList' });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-dropdown :options="options" @select="handleDropdown">
|
<n-dropdown :options="options" @select="handleDropdown">
|
||||||
<hover-container class="px-12px" :inverted="theme.header.inverted">
|
<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>
|
<span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span>
|
||||||
</hover-container>
|
</hover-container>
|
||||||
</n-dropdown>
|
</n-dropdown>
|
||||||
@ -10,18 +10,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { DropdownOption } from 'naive-ui';
|
import type { DropdownOption } from 'naive-ui';
|
||||||
import { useAuthStore, useThemeStore } from '@/store';
|
import { useAuthStore, useThemeStore } from '@/store';
|
||||||
import { iconifyRender } from '@/utils';
|
import { useIconRender } from '@/composables';
|
||||||
|
|
||||||
defineOptions({ name: 'UserAvatar' });
|
defineOptions({ name: 'UserAvatar' });
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
|
const { iconRender } = useIconRender();
|
||||||
|
|
||||||
const options: DropdownOption[] = [
|
const options: DropdownOption[] = [
|
||||||
{
|
{
|
||||||
label: '用户中心',
|
label: '用户中心',
|
||||||
key: 'user-center',
|
key: 'user-center',
|
||||||
icon: iconifyRender('carbon:user-avatar')
|
icon: iconRender({ icon: 'carbon:user-avatar' })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
@ -30,7 +31,7 @@ const options: DropdownOption[] = [
|
|||||||
{
|
{
|
||||||
label: '退出登录',
|
label: '退出登录',
|
||||||
key: 'logout',
|
key: 'logout',
|
||||||
icon: iconifyRender('carbon:logout')
|
icon: iconRender({ icon: 'carbon:logout' })
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
@click="handleTo"
|
@click="handleTo"
|
||||||
@mouseenter="handleMouse(item)"
|
@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>
|
<span class="flex-1 ml-5px">{{ item.meta?.title }}</span>
|
||||||
<icon-ant-design-enter-outlined class="icon text-20px p-2px mr-3px" />
|
<icon-ant-design-enter-outlined class="icon text-20px p-2px mr-3px" />
|
||||||
</div>
|
</div>
|
||||||
@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { useThemeStore } from '@/store';
|
import { useThemeStore } from '@/store';
|
||||||
|
|
||||||
defineOptions({ name: 'SearchResult' });
|
defineOptions({ name: 'SearchResult' });
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import type { DropdownOption } from 'naive-ui';
|
import type { DropdownOption } from 'naive-ui';
|
||||||
import { useAppStore, useTabStore } from '@/store';
|
import { useAppStore, useTabStore } from '@/store';
|
||||||
import { iconifyRender } from '@/utils';
|
import { useIconRender } from '@/composables';
|
||||||
|
|
||||||
defineOptions({ name: 'ContextMenu' });
|
defineOptions({ name: 'ContextMenu' });
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ const emit = defineEmits<Emits>();
|
|||||||
|
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const tab = useTabStore();
|
const tab = useTabStore();
|
||||||
|
const { iconRender } = useIconRender();
|
||||||
|
|
||||||
const dropdownVisible = computed({
|
const dropdownVisible = computed({
|
||||||
get() {
|
get() {
|
||||||
@ -66,33 +67,33 @@ const options = computed<Option[]>(() => [
|
|||||||
label: '重新加载',
|
label: '重新加载',
|
||||||
key: 'reload-current',
|
key: 'reload-current',
|
||||||
disabled: props.currentPath !== tab.activeTab,
|
disabled: props.currentPath !== tab.activeTab,
|
||||||
icon: iconifyRender('ant-design:reload-outlined')
|
icon: iconRender({ icon: 'ant-design:reload-outlined' })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭',
|
label: '关闭',
|
||||||
key: 'close-current',
|
key: 'close-current',
|
||||||
disabled: props.currentPath === tab.homeTab.fullPath,
|
disabled: props.currentPath === tab.homeTab.fullPath,
|
||||||
icon: iconifyRender('ant-design:close-outlined')
|
icon: iconRender({ icon: 'ant-design:close-outlined' })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭其他',
|
label: '关闭其他',
|
||||||
key: 'close-other',
|
key: 'close-other',
|
||||||
icon: iconifyRender('ant-design:column-width-outlined')
|
icon: iconRender({ icon: 'ant-design:column-width-outlined' })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭左侧',
|
label: '关闭左侧',
|
||||||
key: 'close-left',
|
key: 'close-left',
|
||||||
icon: iconifyRender('mdi:format-horizontal-align-left')
|
icon: iconRender({ icon: 'mdi:format-horizontal-align-left' })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭右侧',
|
label: '关闭右侧',
|
||||||
key: 'close-right',
|
key: 'close-right',
|
||||||
icon: iconifyRender('mdi:format-horizontal-align-right')
|
icon: iconRender({ icon: 'mdi:format-horizontal-align-right' })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '关闭所有',
|
label: '关闭所有',
|
||||||
key: 'close-all',
|
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)"
|
@close="tab.removeTab(item.fullPath)"
|
||||||
@contextmenu="handleContextMenu($event, 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 }}
|
{{ item.meta.title }}
|
||||||
</component>
|
</component>
|
||||||
</div>
|
</div>
|
||||||
@ -29,7 +33,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||||
import { ButtonTab, ChromeTab } from '@soybeanjs/vue-admin-tab';
|
import { ButtonTab, ChromeTab } from '@soybeanjs/vue-admin-tab';
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { useTabStore, useThemeStore } from '@/store';
|
import { useTabStore, useThemeStore } from '@/store';
|
||||||
import { ContextMenu } from './components';
|
import { ContextMenu } from './components';
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ const document: AuthRoute.Route = {
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'vue文档',
|
title: 'vue文档',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'mdi:vuejs'
|
icon: 'logos:vue'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -20,16 +20,36 @@ const document: AuthRoute.Route = {
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'vite文档',
|
title: 'vite文档',
|
||||||
requiresAuth: true,
|
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',
|
name: 'document_project',
|
||||||
path: '/document/project',
|
path: '/document/project',
|
||||||
|
component: 'self',
|
||||||
|
meta: {
|
||||||
|
title: '项目文档',
|
||||||
|
requiresAuth: true,
|
||||||
|
localIcon: 'logo'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document_project-link',
|
||||||
|
path: '/document/project-link',
|
||||||
meta: {
|
meta: {
|
||||||
title: '项目文档(外链)',
|
title: '项目文档(外链)',
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
icon: 'mdi:file-link-outline',
|
localIcon: 'logo',
|
||||||
href: 'https://docs.soybean.pro/'
|
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_AUTH_ROUTE_MODE: 'static' | 'dynamic';
|
||||||
/** 路由首页的路径 */
|
/** 路由首页的路径 */
|
||||||
readonly VITE_ROUTE_HOME_PATH: Exclude<AuthRoute.RoutePath, '/' | '/not-found-page' | '/:pathMatch(.*)*'>;
|
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;
|
readonly VITE_SERVICE_ENV?: ServiceEnvType;
|
||||||
/** 开启请求代理 */
|
/** 开启请求代理 */
|
||||||
|
6
src/typings/route.d.ts
vendored
@ -96,10 +96,10 @@ declare namespace AuthRoute {
|
|||||||
permissions?: Auth.RoleType[];
|
permissions?: Auth.RoleType[];
|
||||||
/** 缓存页面 */
|
/** 缓存页面 */
|
||||||
keepAlive?: boolean;
|
keepAlive?: boolean;
|
||||||
/** 菜单和面包屑对应的图标 */
|
/** 菜单和面包屑对应的图标(iconify图标名称) */
|
||||||
icon?: string;
|
icon?: string;
|
||||||
/** 自定义的菜单和面包屑对应的图标 */
|
/** 使用本地svg作为的菜单和面包屑对应的图标(assets/svg-icon文件夹的的svg文件名) */
|
||||||
customIcon?: string;
|
localIcon?: string;
|
||||||
/** 是否在菜单中隐藏(一些列表、表格的详情页面需要通过参数跳转,所以不能显示在菜单中) */
|
/** 是否在菜单中隐藏(一些列表、表格的详情页面需要通过参数跳转,所以不能显示在菜单中) */
|
||||||
hide?: boolean;
|
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 './color';
|
||||||
export * from './number';
|
export * from './number';
|
||||||
export * from './object';
|
export * from './object';
|
||||||
export * from './icon';
|
|
||||||
export * from './pattern';
|
export * from './pattern';
|
||||||
export * from './theme';
|
export * from './theme';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { customIconRender, iconifyRender } from '../common';
|
import { useIconRender } from '@/composables';
|
||||||
|
|
||||||
/** 路由不转换菜单 */
|
/** 路由不转换菜单 */
|
||||||
function hideInMenu(route: AuthRoute.Route) {
|
function hideInMenu(route: AuthRoute.Route) {
|
||||||
@ -9,18 +9,25 @@ function hideInMenu(route: AuthRoute.Route) {
|
|||||||
function addPartialProps(config: {
|
function addPartialProps(config: {
|
||||||
menu: GlobalMenuOption;
|
menu: GlobalMenuOption;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
customIcon?: string;
|
localIcon?: string;
|
||||||
children?: GlobalMenuOption[];
|
children?: GlobalMenuOption[];
|
||||||
}) {
|
}) {
|
||||||
|
const { iconRender } = useIconRender();
|
||||||
|
|
||||||
const item = { ...config.menu };
|
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;
|
return item;
|
||||||
}
|
}
|
||||||
@ -46,7 +53,7 @@ export function transformAuthRouteToMenu(routes: AuthRoute.Route[]): GlobalMenuO
|
|||||||
routePath: path
|
routePath: path
|
||||||
},
|
},
|
||||||
icon: meta.icon,
|
icon: meta.icon,
|
||||||
customIcon: meta.customIcon,
|
localIcon: meta.localIcon,
|
||||||
children: menuChildren
|
children: menuChildren
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
:style="`--icon-margin: ${button.props.circle ? 0 : 6}px`"
|
:style="`--icon-margin: ${button.props.circle ? 0 : 6}px`"
|
||||||
>
|
>
|
||||||
<template v-if="button.icon" #icon>
|
<template v-if="button.icon" #icon>
|
||||||
<Icon :icon="button.icon" />
|
<svg-icon :icon="button.icon" />
|
||||||
</template>
|
</template>
|
||||||
{{ button.label }}
|
{{ button.label }}
|
||||||
</n-button>
|
</n-button>
|
||||||
@ -36,7 +36,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ButtonProps } from 'naive-ui';
|
import type { ButtonProps } from 'naive-ui';
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { useLoading } from '@/hooks';
|
import { useLoading } from '@/hooks';
|
||||||
|
|
||||||
interface ButtonDetail {
|
interface ButtonDetail {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<gradient-bg class="h-100px" :start-color="item.colors[0]" :end-color="item.colors[1]">
|
<gradient-bg class="h-100px" :start-color="item.colors[0]" :end-color="item.colors[1]">
|
||||||
<h3 class="text-16px">{{ item.title }}</h3>
|
<h3 class="text-16px">{{ item.title }}</h3>
|
||||||
<div class="flex justify-between pt-12px">
|
<div class="flex justify-between pt-12px">
|
||||||
<Icon :icon="item.icon" class="text-32px" />
|
<svg-icon :icon="item.icon" class="text-32px" />
|
||||||
<count-to
|
<count-to
|
||||||
:prefix="item.unit"
|
:prefix="item.unit"
|
||||||
:start-value="1"
|
:start-value="1"
|
||||||
@ -18,7 +18,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { GradientBg } from './components';
|
import { GradientBg } from './components';
|
||||||
|
|
||||||
defineOptions({ name: 'DashboardAnalysisDataCard' });
|
defineOptions({ name: 'DashboardAnalysisDataCard' });
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<n-card :bordered="false" class="rounded-16px shadow-sm">
|
<n-card :bordered="false" class="rounded-16px shadow-sm">
|
||||||
<div class="flex-y-center justify-between">
|
<div class="flex-y-center justify-between">
|
||||||
<div class="flex-y-center">
|
<div class="flex-y-center">
|
||||||
<icon-custom-avatar class="text-70px" />
|
<icon-local-avatar class="text-70px" />
|
||||||
<div class="pl-12px">
|
<div class="pl-12px">
|
||||||
<h3 class="text-18px font-semibold">早安,{{ auth.userInfo.userName }}, 今天又是充满活力的一天!</h3>
|
<h3 class="text-18px font-semibold">早安,{{ auth.userInfo.userName }}, 今天又是充满活力的一天!</h3>
|
||||||
<p class="leading-30px text-[#999]">今日多云转晴,20℃ - 25℃!</p>
|
<p class="leading-30px text-[#999]">今日多云转晴,20℃ - 25℃!</p>
|
||||||
|
@ -2,14 +2,12 @@
|
|||||||
<div
|
<div
|
||||||
class="flex-col-center p-12px border-1px border-[#efeff5] dark:border-[#ffffff17] rounded-4px hover:shadow-sm cursor-pointer"
|
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>
|
<p class="py-8px text-16px">{{ label }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'DashboardWorkbenchMainShortcutsCard' });
|
defineOptions({ name: 'DashboardWorkbenchMainShortcutsCard' });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
@click="handleOpenSite"
|
@click="handleOpenSite"
|
||||||
>
|
>
|
||||||
<header class="flex-y-center">
|
<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>
|
<h3 class="pl-12px text-18px font-semibold">{{ name }}</h3>
|
||||||
</header>
|
</header>
|
||||||
<p class="py-8px h-56px text-[#999]">{{ description }}</p>
|
<p class="py-8px h-56px text-[#999]">{{ description }}</p>
|
||||||
@ -15,8 +15,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'DashboardWorkbenchMainTechnologyCard' });
|
defineOptions({ name: 'DashboardWorkbenchMainTechnologyCard' });
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<n-list>
|
<n-list>
|
||||||
<n-list-item v-for="item in activity" :key="item.id">
|
<n-list-item v-for="item in activity" :key="item.id">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<icon-custom-avatar class="text-48px" />
|
<icon-local-avatar class="text-48px" />
|
||||||
</template>
|
</template>
|
||||||
<n-thing :title="item.content" :description="item.time" />
|
<n-thing :title="item.content" :description="item.time" />
|
||||||
</n-list-item>
|
</n-list-item>
|
||||||
@ -37,7 +37,7 @@
|
|||||||
</n-grid>
|
</n-grid>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-card title="创意" :bordered="false" size="small" class="shadow-sm rounded-16px">
|
<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-card>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="grid grid-cols-10">
|
<div class="grid grid-cols-10">
|
||||||
<template v-for="item in icons" :key="item">
|
<template v-for="item in icons" :key="item">
|
||||||
<div class="mt-5px flex-x-center">
|
<div class="mt-5px flex-x-center">
|
||||||
<Icon :icon="item" class="text-30px" />
|
<svg-icon :icon="item" class="text-30px" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -18,20 +18,21 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
<n-card title="自定义图标示例" class="mt-10px shadow-sm rounded-16px">
|
<n-card title="自定义图标示例" class="mt-10px shadow-sm rounded-16px">
|
||||||
<div class="pb-12px text-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>
|
||||||
<div class="grid grid-cols-10">
|
<div class="grid grid-cols-10">
|
||||||
<div class="mt-5px flex-x-center">
|
<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>
|
||||||
<div class="mt-5px flex-x-center">
|
<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>
|
</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 class="grid grid-cols-10">
|
||||||
<div v-for="(item, index) in customIcons" :key="index" class="mt-5px flex-x-center">
|
<div v-for="(fileName, index) in localIcons" :key="index" class="mt-5px flex-x-center">
|
||||||
<svg-icon :icon="item" class="text-30px text-primary" />
|
<svg-icon :local-icon="fileName" class="text-30px text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
@ -40,12 +41,11 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { icons } from './icons';
|
import { icons } from './icons';
|
||||||
|
|
||||||
const selectValue = ref('');
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|