加载中...

Vue3打造SVG设计器+图标库

Vue3打造SVG设计器+图标库

在Web开发中常常会用到SVG图标,下面就是如何用Vue3来打造一个自己专属的SVG设计器+图标库。先上效果图与代码,之后再对核心代码片段进行说明。

New Image

SVG设计器+图标库:sps-svg-lib

注:系统基于笔者自己写的模板 sps-vite-simple 开发。

SVG设计器

基础代码

首先来看一下一个典型的SVG图标代码。SVG图标的HTML节点是由多个图形节点构成的。

js
复制代码
<svg viewBox="0 0 1024 1024"> <circle fill="currentColor" stroke="currentColor" stroke-width="0" r="100" cx="100" cy="100" />, <rect fill="currentColor" stroke="currentColor" stroke-width="0" x="300" y="100" width="150" height="100" /> </svg>

于是,在设计器中定义SvgConfig类型,其属性包含对svg节点的配置(baseConfig)及其所包含的每个component的配置,且每个component的配置项由于其类型不同也会有所区别。

js
复制代码
// components/svgDesign/type.d.ts export interface SvgComponent { id?: string type: SvgComponentType fill?: string stroke?: string [''stroke-width'']?: number } // 圆形 export interface CircleComponet extends SvgComponent { r: number cx: number cy: number } // 矩形 export interface RectComponent extends SvgComponent { x: number y: number width: number height: number } // ... 其他组件 export interface SvgConfig { baseConfig: SvgBaseConfig components: SvgComponent[] }

通过provide / inject的方式来进行局部状态管理。可参考:Vue3+TS 优雅地使用状态管理

在根组件中通过provide注册局部状态(svgState),并将局部状态相关的操作全部封装成一个hook函数。

js
复制代码
// components/svgDesign/index.tsx import { defineComponent, provide } from ''vue'' import { createSvgState, injectSvgStateKey } from ''./hooks/useSvgState'' export default defineComponent({ name: ''SvgDesign'', setup () { provide(injectSvgStateKey, createSvgState()) //... } }) // components/svgDesign/hooks/useSvgState import { inject, InjectionKey, reactive } from ''vue'' import { SvgComponent, SvgState } from ''../type'' import { cloneDeep } from ''lodash-es'' export const createSvgState = () => { const svgState: SvgState = reactive({ baseConfig: { name: ''newIcon'' }, components: [], currentComponent: null, previewColor: ''#1890ff'' }) return svgState } export const injectSvgStateKey: InjectionKey<SvgState> = Symbol(''svg-state'') export const useSvgState = () => { const svgState = inject(injectSvgStateKey)! // 切换当前编辑组件 const setCurrentComponent = (component: SvgComponent) => { svgState.currentComponent = cloneDeep(component) } // 新增组件 const addComponent = (component: SvgComponent) => { svgState.components.push(component) } // 删除组件 const deleteComponent = (index: number) => { const { components, currentComponent } = svgState if (currentComponent && components[index].id === currentComponent.id) { svgState.currentComponent = null } components.splice(index, 1) } // 保存对当前编辑组件的更改 const saveComponent = () => { const { currentComponent } = svgState if (!currentComponent) return const index = svgState.components.findIndex(item => item.id === svgState.currentComponent!.id) index >= 0 && svgState.components.splice(index, 1, cloneDeep(currentComponent)) } return { svgState, setCurrentComponent, addComponent, deleteComponent, saveComponent } }

图标预览

右侧的图标预览组件

js
复制代码
// components/svgDesign/components/SvgPreview.ts import { defineComponent } from ''vue'' import { useSvgState } from ''../hooks/useSvgState'' export default defineComponent({ name: ''SvgPreview'', setup () { const { svgState } = useSvgState() /* render 函数 */ return () => { const { components, previewColor } = svgState const svgComponents = components.map(component => { const { type, ...options } = component return ( <type { ...options } /> ) }) return ( <div class="w-96 h-96 border-2 border-gray-400" style={{ color: previewColor }}> <svg viewBox="0 0 1024 1024">{ svgComponents }</svg> </div> ) } } })

图标配置

左侧的图标配置组件,由于该组件代码较多,且大多数代码都较为简单,这里就不贴完整代码,只对动态配置表单进行下说明。

组件有圆形、矩形等多种类型,每种类型的配置项是不完全相同的,所以配置项表单需要根据组件的类型动态生成。

组件的公共配置项:

js
复制代码
// components/svgDesign/components/SvgConfig.ts let form: JSX.Element | null = null if (currentComponent) { form = ( <a-form class="mt-5" model={ currentComponent }> <a-form-item label="填充色"> <a-input v-model={[ currentComponent.fill, ''value'' ]} /> </a-form-item> <a-form-item label="边框宽度"> <a-input-number v-model={[ currentComponent[''stroke-width''], ''value'' ]} min={ 0 } /> </a-form-item> <a-form-item label="边框颜色"> <a-input v-model={[ currentComponent.stroke, ''value'' ]} /> </a-form-item> { renderFormItem(currentComponent) } <a-form-item> <a-button type="primary" onClick={ saveComponent }><i class="far fa-save" />保存</a-button> </a-form-item> </a-form> ) }

根据组件类型生成对应的动态表单项:

js
复制代码
// components/svgDesign/utils/renderFormItem.tsx const renderCircleConfig = (model: CircleComponet) => { return (<> <a-form-item label="半径"> <a-input-number v-model={[ model.r, ''value'' ]} /> </a-form-item> <a-form-item label="X偏移"> <a-input-number v-model={[ model.cx, ''value'' ]} /> </a-form-item> <a-form-item label="Y偏移"> <a-input-number v-model={[ model.cy, ''value'' ]} /> </a-form-item> </>) } const renderRectConfig = (model: RectComponent) => { return (<> <a-form-item label="宽度"> <a-input-number v-model={[ model.width, ''value'' ]} min={ 0 } /> </a-form-item> <a-form-item label="高度"> <a-input-number v-model={[ model.height, ''value'' ]} min={ 0 } /> </a-form-item> <a-form-item label="X偏移"> <a-input-number v-model={[ model.x, ''value'' ]} /> </a-form-item> <a-form-item label="Y偏移"> <a-input-number v-model={[ model.y, ''value'' ]} /> </a-form-item> </>) } // ...其他组件 const renderMap = { circle: renderCircleConfig, rect: renderRectConfig, // ...其他组件 } export default function renderFormItem (model: SvgComponent) { const handler = renderMap[model.type] return handler(model as any) }

SVG图标库

在SVG设计器页面提供HTML、Symbol和Json三种不同类型的输出方式。其分别对应静态局部引入,静态全局引入,动态引入三种引入方式,可根据实际需要自行选用。

在下面的测试页面中就通过不同方式来使用SVG图标。

js
复制代码
// views/sys/test/index.tsx import { defineComponent } from ''vue'' export default defineComponent({ name: ''Test'', setup () { return () => { return ( <div class="flex-center h-screen"> <div class="flex justify-around"> <svg class="w-40 h-40 text-green-400" viewBox="0 0 1024 1024"> <path fill="currentColor" stroke="currentColor" stroke-width="0" d="M288 352.256l448 0c17.664 0 32-14.336 32-32s-14.336-32-32-32L288 288.256c-17.664 0-32 14.336-32 32S270.336 352.256 288 352.256L288 352.256zM736 479.744 288 479.744c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l448 0c17.664 0 32-14.336 32-32C768 494.08 753.664 479.744 736 479.744L736 479.744zM736 671.744 288 671.744c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l448 0c17.664 0 32-14.336 32-32C768 686.08 753.664 671.744 736 671.744L736 671.744zM418.688 160l160 0c26.528 0 48-21.504 48-48 0-26.496-21.504-48-48-48l-160 0c-26.496 0-48 21.504-48 48C370.688 138.496 392.192 160 418.688 160L418.688 160zM832 96l-32 0c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l32 0c35.296 0 64 28.704 64 64l0 608c0 35.296-28.704 64-64 64L192 896c-35.296 0-64-28.704-64-64L128 224c0-35.296 28.704-64 64-64l32 0c17.664 0 32-14.336 32-32 0-17.664-14.336-32-32-32L192 96C121.312 96 64 153.312 64 224l0 608c0 70.688 57.312 128 128 128l640 0c70.688 0 128-57.312 128-128L960 224C960 153.312 902.688 96 832 96L832 96zM832 96" /> </svg> <svg-icon class="w-40 h-40 text-red-400" name="order" /> <dynamic-svg-icon class="w-40 h-40 text-blue-400" name="order" /> </div> </div> ) } } })

New Image

静态局部引入

将HTML输出方式的结果直接使用,或者封装成一个Vue组件中。

js
复制代码
<svg viewBox="0 0 1024 1024"> <path fill="currentColor" stroke="currentColor" stroke-width="0" d="M288 352.256l448 0c17.664 0 32-14.336 32-32s-14.336-32-32-32L288 288.256c-17.664 0-32 14.336-32 32S270.336 352.256 288 352.256L288 352.256zM736 479.744 288 479.744c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l448 0c17.664 0 32-14.336 32-32C768 494.08 753.664 479.744 736 479.744L736 479.744zM736 671.744 288 671.744c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l448 0c17.664 0 32-14.336 32-32C768 686.08 753.664 671.744 736 671.744L736 671.744zM418.688 160l160 0c26.528 0 48-21.504 48-48 0-26.496-21.504-48-48-48l-160 0c-26.496 0-48 21.504-48 48C370.688 138.496 392.192 160 418.688 160L418.688 160zM832 96l-32 0c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l32 0c35.296 0 64 28.704 64 64l0 608c0 35.296-28.704 64-64 64L192 896c-35.296 0-64-28.704-64-64L128 224c0-35.296 28.704-64 64-64l32 0c17.664 0 32-14.336 32-32 0-17.664-14.336-32-32-32L192 96C121.312 96 64 153.312 64 224l0 608c0 70.688 57.312 128 128 128l640 0c70.688 0 128-57.312 128-128L960 224C960 153.312 902.688 96 832 96L832 96zM832 96" /> </svg>

静态全局引入

封装

将Symbol方式输出symbol节点拷贝到SvgIconProvider.tsx中:

js
复制代码
// components/svgIcon/SvgIconProvider.tsx import { defineComponent, renderSlot } from ''vue'' export default defineComponent({ name: ''SvgIconProvider'', setup (_, { slots }) { /* render 函数 */ return () => { return (<> <svg width="0" height="0"> {/* symbol节点都拷贝到此处 */} <symbol id="order" viewBox="0 0 1024 1024"> <path fill="currentColor" stroke="currentColor" stroke-width="0" d="M288 352.256l448 0c17.664 0 32-14.336 32-32s-14.336-32-32-32L288 288.256c-17.664 0-32 14.336-32 32S270.336 352.256 288 352.256L288 352.256zM736 479.744 288 479.744c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l448 0c17.664 0 32-14.336 32-32C768 494.08 753.664 479.744 736 479.744L736 479.744zM736 671.744 288 671.744c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l448 0c17.664 0 32-14.336 32-32C768 686.08 753.664 671.744 736 671.744L736 671.744zM418.688 160l160 0c26.528 0 48-21.504 48-48 0-26.496-21.504-48-48-48l-160 0c-26.496 0-48 21.504-48 48C370.688 138.496 392.192 160 418.688 160L418.688 160zM832 96l-32 0c-17.664 0-32 14.336-32 32 0 17.664 14.336 32 32 32l32 0c35.296 0 64 28.704 64 64l0 608c0 35.296-28.704 64-64 64L192 896c-35.296 0-64-28.704-64-64L128 224c0-35.296 28.704-64 64-64l32 0c17.664 0 32-14.336 32-32 0-17.664-14.336-32-32-32L192 96C121.312 96 64 153.312 64 224l0 608c0 70.688 57.312 128 128 128l640 0c70.688 0 128-57.312 128-128L960 224C960 153.312 902.688 96 832 96L832 96zM832 96" /> </symbol> </svg> { renderSlot(slots, ''default'') } </>) } } })

SvgIcon组件根据传入的name来引入SvgIconProvider组件中注册的图标。

js
复制代码
// components/svgIcon/SvgIcon.tsx import { defineComponent } from ''vue'' export default defineComponent({ name: ''SvgIcon'', props: { name: { type: String, required: true }, }, setup (props, { attrs }) { /* render 函数 */ return () => { const { name } = props return ( <svg class="svg-icon" { ...attrs }> <use href={ `#${name}` } /> </svg> ) } } })

使用

App.tsx中全局注册图标库:

js
复制代码
// App.tsx import { defineComponent } from ''vue'' import SvgIconProvider from ''@/components/svgIcon/SvgIconProvider'' export default defineComponent({ name: ''App'', setup () { return () => { return ( <SvgIconProvider> <router-view /> </SvgIconProvider> ) } } })

传入name使用对应的图标。

js
复制代码
<svg-icon class="w-40 h-40 text-red-400" name="order" />

动态引入

封装

根据name从后端动态获取图标的配置信息,即Json格式输出的内容。

js
复制代码
// components/svgIcon/DynamicSvgIcon.tsx import { getIconApi } from ''@/api/icon'' import { useRemoteData } from ''@/hooks/useRemoteData'' import { defineComponent, onMounted } from ''vue'' export default defineComponent({ name: ''DynamicSvgIcon'', props: { name: { type: String, required: true } }, setup (props) { const { state, fetchData } = useRemoteData(getIconApi) onMounted(() => { fetchData(props.name) }) /* render 函数 */ return () => { const { data } = state let svgComponents: JSX.Element[] | null = null if (data) { const { components } = data svgComponents = components.map(component => { const { type, ...options } = component return ( <type { ...options } /> ) }) } return ( <svg viewBox="0 0 1024 1024">{ svgComponents }</svg> ) } } })

使用

使用方式与静态全局引入一样,只是不需要全局注册图标。

js
复制代码
dynamic-svg-icon class="w-40 h-40 text-blue-400" name="order" />

拓展思路

  1. 增加对图标的基础配置,如视口大小(目前固定为1024 * 1024)。
  2. 新增更多的组件类型,为每种类型新增更丰富的配置项。
  3. 新增图标动画配置,这也是笔者接下来准备做的。