Vue 3 Signature Pad集成到WeChat Markdown Editor
WeChat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器:支持 Markdown 语法、色盘取色、多图上传、一键下载文档、自定义 CSS 样式、一键重置等特性
Vue 3 Signature Pad - A beautiful signature pad component for Vue 3.
本人是把Vue 3 Signature Pad
集成到编辑
菜单和编辑区右键菜单里,并增加了自定义颜色,而且支持黑暗模式
, 当绘制好需要的绘画后,只要点击保存就会自动把文件上传到配置的服务器里,然后返回图片的地址到编辑区鼠标所在位置。非常方便!!233333333
集成效果
手绘配置:
绘画效果:
Vue 3 Signature Pad安装和基础使用教程
安装
npm install @selemondev/vue3-signature-pad
将其注册为本地组件
import { VueSignaturePad } from '@selemondev/vue3-signature-pad'
使用
你可以从 Icones[1] 库中获取 svgs。
<script setup lang='ts'>
import { onMounted, ref } from 'vue'
import { VueSignaturePad } from '@selemondev/vue3-signature-pad'
import type { CanvasSignature } from '@selemondev/vue3-signature-pad'
const options = ref({
penColor: 'rgb(0,0,0)',
backgroundColor: 'rgb(255, 255, 255)',
maxWidth: 2,
minWidth: 2,
})
const colors = [
{
color: 'rgb(51, 133, 255)',
},
{
color: 'rgb(85, 255, 51)',
},
{
color: 'rgb(255, 85, 51)',
},
]
const signature = ref<Signature>()
function handleUndo() {
return signature.value?.undo()
}
function handleClearCanvas() {
return signature.value?.clearCanvas()
}
function handleSaveSignature() {
return signature.value?.saveSignature() && alert(signature.value?.saveSignature())
}
</script>
<template>
<div class='flex flex-col space-y-2'>
<div class='p-4 bg-white rounded-md'>
<div class='relative bg-gray-100 rounded-md'>
<VueSignaturePad
ref='signature'
height='400px'
width='950px'
:max-width='options.maxWidth'
:min-width='options.minWidth'
:options='{
penColor: options.penColor,
backgroundColor: options.backgroundColor,
}'
/>
<div class='absolute flex flex-col space-y-2 top-3 right-4'>
<button
type='button'
class='grid p-2 bg-white rounded-md shadow-md place-items-center'
@click='handleUndo'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 24 24'
>
<path
fill='none'
stroke='#000'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
d='M10 8H5V3m.291 13.357a8 8 0 1 0 .188-8.991'
/>
</svg>
</button>
<button
type='button'
class='grid p-2 bg-white rounded-md shadow-md place-items-center'
@click='handleClearCanvas'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 14 14'
>
<path
fill='none'
stroke='#000'
stroke-linecap='round'
stroke-linejoin='round'
d='M11.5 8.5h-9l-.76 3.8a1 1 0 0 0 .21.83a1 1 0 0 0 .77.37h8.56a1.002 1.002 0 0 0 .77-.37a1.001 1.001 0 0 0 .21-.83zm0-3a1 1 0 0 1 1 1v2h-11v-2a1 1 0 0 1 1-1H4a1 1 0 0 0 1-1v-2a2 2 0 1 1 4 0v2a1 1 0 0 0 1 1zm-3 8V11'
/>
</svg>
</button>
<button
type='button'
class='grid p-2 bg-white rounded-md shadow-md place-items-center'
@click='handleSaveSignature'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 24 24'
>
<path
fill='#000'
d='M21 7v14H3V3h14zm-2 .85L16.15 5H5v14h14zM12 18q1.25 0 2.125-.875T15 15t-.875-2.125T12 12t-2.125.875T9 15t.875 2.125T12 18m-6-8h9V6H6zM5 7.85V19V5z'
/>
</svg>
</button>
</div>
</div>
</div>
<div class='flex items-center justify-between w-full p-3 bg-white rounded-md'>
<div>
<h1 class='text-lg'>Choose pen color</h1>
</div>
<div class='flex items-center space-x-4'>
<div v-for='color in colors' :key='color.color'>
<button
type='button'
:style='{ background: color.color }'
class='grid w-8 h-8 rounded-full place-items-center'
@click='options.penColor = color.color'
>
<p v-if='options.penColor === color.color'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='15'
height='15'
viewBox='0 0 48 48'
>
<path
fill='#ffffff'
fill-rule='evenodd'
stroke='#ffffff'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='4'
d='m4 24l5-5l10 10L39 9l5 5l-25 25z'
clip-rule='evenodd'
/>
</svg>
</p>
</button>
</div>
</div>
</div>
<div class='flex items-center justify-between w-full p-3 bg-white rounded-md'>
<div>
<h1 class='text-lg'>Choose maximum pen line thickness</h1>
</div>
<div class='flex items-center space-x-4'>
<input v-model='options.maxWidth' type='range' :min='0' :max='10' />
<p>{{ options.maxWidth }}</p>
</div>
</div>
<div class='flex items-center justify-between w-full p-3 bg-white rounded-md'>
<div>
<h1 class='text-lg'>Choose minimum pen line thickness</h1>
</div>
<div class='flex items-center space-x-4'>
<input v-model='options.minWidth' type='range' :min='0' :max='10' />
<p>{{ options.minWidth }}</p>
</div>
</div>
</div>
</template>
更多内容可以点击这里[2],也可以在这里在线体验。
简要步骤和代码
具体代码
新建一个HandPaintedDialog.vue
具体代码如下:
<script setup lang="ts">
import { useDisplayStore } from '@/stores'
import { toTypedSchema } from '@vee-validate/yup'
import { Field, Form } from 'vee-validate'
import * as yup from 'yup'
import { useStore } from '@/stores'
const displayStore = useDisplayStore()
const { toggleShowHandPaintedDialog } = displayStore
import { cn } from '@/lib/utils'
import { VueSignaturePad } from '@selemondev/vue3-signature-pad'
import type { CanvasSignature } from '@selemondev/vue3-signature-pad'
import { Label, type LabelProps } from 'radix-vue'
import { computed, type HTMLAttributes } from 'vue'
import PickColors from 'vue-pick-colors'
const props = defineProps<LabelProps & { class?: HTMLAttributes[`class`] }>()
const store = useStore()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
const signaturePadOptions = ref({
maxWidth: 2,
minWidth: 2,
})
const colors = [
{
color: 'rgb(15, 76, 129)',
},
{
color: 'rgb(250, 81, 81)',
},
{
color: 'rgb(0, 152, 116)',
},
{
color: 'rgb(51, 133, 255)',
},
{
color: 'rgb(85, 201, 234)',
},
{
color: 'rgb(85, 255, 51)',
},
]
const signature = ref<Signature>()
function handleUndo() {
return signature.value?.undo()
}
function handleClearCanvas() {
return signature.value?.clearCanvas()
}
function handleSaveSignature(format: string) {
copyBase64ImageToClipboard(signature.value?.saveSignature(format));
(${signature.value?.saveSignature(format)})\n`, `end`)
toggleShowHandPaintedDialog()
toast.success(`已复制到粘贴板!`)
}
async function copyBase64ImageToClipboard(base64Image: string) {
const img = new Image()
img.src = base64Image
img.onload = async () => {
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
if (ctx) {
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
canvas.toBlob(async (blob) => {
if (blob) {
const clipboardItem = new ClipboardItem({ "image/png": blob })
await navigator.clipboard.write([clipboardItem])
console.log("PNG image copied to clipboard successfully!")
toRaw(store.editor!).replaceSelection(`\n${await readClipboardAsMarkdown()}`, `end`)
} else {
console.error("Failed to convert canvas to blob.")
}
}, "image/png")
}
};
img.onerror = (error) => {
console.error("Failed to load image:", error)
};
}
async function readClipboardAsMarkdown() {
try {
const clipboardItems = await navigator.clipboard.read()
let markdown = '';
for (const clipboardItem of clipboardItems) {
const types = clipboardItem.types
for (const type of types) {
if (type.startsWith("text/")) {
const blob = await clipboardItem.getType(type)
const text = await blob.text()
markdown += text + '\n'
} else if (type.startsWith("image/")) {
const blob = await clipboardItem.getType(type)
// const imageUrl = URL.createObjectURL(blob)
const imageUrl = await uploadImage(blob)
markdown += `\n`
}
}
}
console.log("Markdown Output:")
console.log(markdown)
return markdown
} catch (error) {
console.error("Failed to read clipboard contents:", error)
}
}
async function uploadImage(blob: Blob): Promise<string> {
const file = new File([blob], 'image.png', { type: 'image/png' })
const formData = new FormData()
formData.append('file', file)
const response = await fetch(vueSignaturePadConfig.value.imgHost, {
method: 'POST',
body: formData,
})
if (!response.ok) {
const imageUrl = URL.createObjectURL(blob)
const customFileName = 'image_' + Date.now() + '.png'
return imageUrl+'/'+customFileName
}
const result = await response.json()
return result.url
}
const options = [
{
value: `default`,
label: `默认`,
},
{
value: `vueSignaturePad`,
label: `配置`,
},
]
const handPaintedHost = ref(`default`)
const activeName = ref(`handPainted`)
const vueSignaturePadSchema = toTypedSchema(yup.object({
width: yup.string().optional(),
height: yup.string().optional(),
maxWidth: yup.string().optional(),
minWidth: yup.string().optional(),
penColor:yup.string().optional(),
backgroundColor: yup.string().optional(),
imgHost: yup.string().required(`域名不能为空`),
}))
const vueSignaturePadConfig = ref(localStorage.getItem(`vueSignaturePadConfig`)
? JSON.parse(localStorage.getItem(`vueSignaturePadConfig`)!)
: { imgHost: `http://localhost:3000/upload`, width: `950px`, height: `400px`, maxWidth: `10`, minWidth: `10`, penColor: 'rgb(0,0,0)', backgroundColor: `hsl(var(--muted))` })
function vueSignaturePadSubmit(formValues: any) {
localStorage.setItem(`vueSignaturePadConfig`, JSON.stringify(formValues))
vueSignaturePadConfig.value = formValues
toast.success(`保存成功`)
}
</script>
<template>
<Dialog v-model:open="displayStore.isShowHandPaintedDialog">
<DialogContent class="max-w-max" @pointer-down-outside="ev => ev.preventDefault()">
<Tabs v-model="activeName" class="w-max">
<TabsList>
<TabsTrigger value="handPainted">
手绘
</TabsTrigger>
<TabsTrigger v-for="item in options.filter(item => item.value !== 'default')" :key="item.value" :value="item.value">
{{ item.label }}
</TabsTrigger>
</TabsList>
<TabsContent value="handPainted">
<div class='flex flex-col space-y-2'>
<div class='p-4 rounded-md'>
<div class='relative bg-muted text-muted-foreground rounded-md' :style="{background: vueSignaturePadConfig.backgroundColor}">
<VueSignaturePad
ref='signature'
:height='vueSignaturePadConfig.height'
:width='vueSignaturePadConfig.width'
:max-width='signaturePadOptions.maxWidth'
:min-width='signaturePadOptions.minWidth'
:options='{
penColor: vueSignaturePadConfig.penColor,
backgroundColor: vueSignaturePadConfig.backgroundColor,
}'
/>
<div class='absolute flex flex-col space-y-2 top-3 right-4'>
<button
type='button'
class='grid p-2 bg-white rounded-md shadow-md place-items-center'
@click='handleUndo'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 24 24'
>
<path
fill='none'
stroke='#000'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
d='M10 8H5V3m.291 13.357a8 8 0 1 0 .188-8.991'
/>
</svg>
</button>
<button
type='button'
class='grid p-2 bg-white rounded-md shadow-md place-items-center'
@click='handleClearCanvas'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 14 14'
>
<path
fill='none'
stroke='#000'
stroke-linecap='round'
stroke-linejoin='round'
d='M11.5 8.5h-9l-.76 3.8a1 1 0 0 0 .21.83a1 1 0 0 0 .77.37h8.56a1.002 1.002 0 0 0 .77-.37a1.001 1.001 0 0 0 .21-.83zm0-3a1 1 0 0 1 1 1v2h-11v-2a1 1 0 0 1 1-1H4a1 1 0 0 0 1-1v-2a2 2 0 1 1 4 0v2a1 1 0 0 0 1 1zm-3 8V11'
/>
</svg>
</button>
<button
type='button'
class='grid p-2 bg-white rounded-md shadow-md place-items-center'
@click='handleSaveSignature("image/svg")'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 24 24'
>
<path
fill='#000'
d='M21 7v14H3V3h14zm-2 .85L16.15 5H5v14h14zM12 18q1.25 0 2.125-.875T15 15t-.875-2.125T12 12t-2.125.875T9 15t.875 2.125T12 18m-6-8h9V6H6zM5 7.85V19V5z'
/>
</svg>
</button>
</div>
</div>
</div>
<div :class="cn(
'flex items-center justify-center justify-between w-full p-3 bg-muted text-muted-foreground',
props.class,
)">
<div>
<h1 class='text-lg'>画笔颜色</h1>
</div>
<div class='flex items-center space-x-4'>
<div>
<PickColors size=30 id='pick-color' class='grid w-8 h-8 rounded-full place-items-center' placement='bottom' :theme="store.isDark ? 'dark' : 'light'" show-alpha='true' v-model:value="vueSignaturePadConfig.penColor"/>
</div>
<div v-for='color in colors' :key='color.color'>
<button
type='button'
:style='{ background: color.color }'
class='grid w-8 h-8 rounded-full place-items-center'
@click='vueSignaturePadConfig.penColor = color.color'
>
<p v-if='vueSignaturePadConfig.penColor === color.color'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='15'
height='15'
viewBox='0 0 48 48'
>
<path
fill='#ffffff'
fill-rule='evenodd'
stroke='#ffffff'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='4'
d='m4 24l5-5l10 10L39 9l5 5l-25 25z'
clip-rule='evenodd'
/>
</svg>
</p>
</button>
</div>
</div>
</div>
<div :class="cn(
'flex items-center justify-center justify-between w-full p-3 bg-muted text-muted-foreground',
props.class,
)">
<div>
<h1 class='text-lg'>选择最大线条粗细</h1>
</div>
<div class='flex items-center space-x-4'>
<input v-model='signaturePadOptions.maxWidth' type='range' :min='0' :max='vueSignaturePadConfig.maxWidth' />
<p>{{ signaturePadOptions.maxWidth }}</p>
</div>
</div>
<div :class="cn(
'flex items-center justify-center justify-between w-full p-3 bg-muted text-muted-foreground',
props.class,
)">
<div>
<h1 class='text-lg'>选择最小笔线粗细</h1>
</div>
<div class='flex items-center space-x-4'>
<input v-model='signaturePadOptions.minWidth' type='range' :min='0' :max='vueSignaturePadConfig.minWidth' />
<p>{{ signaturePadOptions.minWidth }}</p>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="vueSignaturePad">
<Form :validation-schema="vueSignaturePadSchema" :initial-values="vueSignaturePadConfig" @submit="vueSignaturePadSubmit">
<Field v-slot="{ field, errorMessage }" name="imgHost">
<FormItem label="图片上传域名" required :error="errorMessage">
<Input
v-bind="field"
v-model="field.value"
placeholder="如:http://localhost:5173"
/>
</FormItem>
</Field>
<Field v-slot="{ field, errorMessage }" name="width">
<FormItem label="画板宽度" :error="errorMessage">
<Input
v-bind="field"
v-model="field.value"
placeholder="如:950px"
/>
</FormItem>
</Field>
<Field v-slot="{ field, errorMessage }" name="height">
<FormItem label="画板高度" :error="errorMessage">
<Input
v-bind="field"
v-model="field.value"
placeholder="如:400px"
/>
</FormItem>
</Field>
<Field v-slot="{ field, errorMessage }" name="maxWidth">
<FormItem label="选择最大线条粗细" :error="errorMessage">
<Input
v-bind="field"
v-model="field.value"
placeholder="如:10"
/>
</FormItem>
</Field>
<Field v-slot="{ field, errorMessage }" name="minWidth">
<FormItem label="选择最小笔线粗细" :error="errorMessage">
<Input
v-bind="field"
v-model="field.value"
placeholder="如:10"
/>
</FormItem>
</Field>
<Field v-slot="{ field, errorMessage }" name="penColor">
<FormItem label="画笔颜色" :error="errorMessage">
<Input
v-bind="field"
v-model="field.value"
placeholder="如:rgb(0,0,0)"
/>
</FormItem>
</Field>
<Field v-slot="{ field, errorMessage }" name="backgroundColor">
<FormItem label="画布背景颜色" :error="errorMessage">
<Input
v-bind="field"
v-model="field.value"
placeholder="如:hsl(var(--muted))或者rgb(255, 255, 255)"
/>
</FormItem>
</Field>
<FormItem>
<label>
画布背景颜色在生成图片时不会显示
</label>
</FormItem>
<FormItem>
<Button type="submit">
保存配置
</Button>
</FormItem>
</Form>
</TabsContent>
<TabsContent value="formCustom">
<CustomUploadForm />
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
</template>
<style lang="less" scoped>
#pick-color > :first-child {
border-radius: 15px !important;
vertical-align: bottom;
border: 0px !important;
}
</style>
然后把HandPaintedDialog.vue
组件添加到Codem)irrorEditor
...
<handPaintedDialog />
...
更多手绘效果
本人绘画技术有很大的进步空间~~~~~~~~~~~~~~~~
相关连接
1. WeChat Markdown Editor github[3]
2. WeChat Markdown Editor gitee[4]
3. Vue 3 Signature Pad[5]
4. Vue 3 Signature Pad github[6]
5. vue-pick-colors组件 github[7]
引用链接
[1]
Icones: https://icones.js.org[2]
点击这里: https://vue3-signature-pad.vercel.app[3]
WeChat Markdown Editor github: https://github.com/doocs/md[4]
WeChat Markdown Editor gitee: https://gitee.com/doocs/md[5]
Vue 3 Signature Pad: https://vue3-signature-pad.vercel.app[6]
Vue 3 Signature Pad github: https://github.com/selemondev/vue3-signature-pad[7]
vue-pick-colors组件 github: https://github.com/qiuzongyuan/vue-pick-colors
没有评论:
发表评论