画布图片预览

This commit is contained in:
lzp
2026-03-06 10:21:05 +08:00
parent 68d3a90940
commit a3938662c9
7 changed files with 170 additions and 107 deletions

View File

@@ -1,6 +1,6 @@
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.66623 17.7762C7.27988 17.7762 7.77735 17.2787 7.77735 16.6651C7.77735 16.0514 7.27988 15.5539 6.66623 15.5539C6.05259 15.5539 5.55512 16.0514 5.55512 16.6651C5.55512 17.2787 6.05259 17.7762 6.66623 17.7762Z" fill="black"/>
<path d="M16.144 11.404C16.3218 10.9206 17.0051 10.9206 17.1885 11.404L17.7773 12.9984C17.8329 13.1484 17.9551 13.2706 18.1051 13.3262L19.6996 13.9151C20.1829 14.0928 20.1829 14.7762 19.6996 14.9595L18.1051 15.5484C17.9551 15.604 17.8329 15.7262 17.7773 15.8762L17.1885 17.4706C17.0107 17.9539 16.3273 17.9539 16.144 17.4706L15.5551 15.8762C15.4996 15.7262 15.3773 15.604 15.2273 15.5484L13.6329 14.9595C13.1496 14.7817 13.1496 14.0984 13.6329 13.9151L15.2273 13.3262C15.3773 13.2706 15.4996 13.1484 15.5551 12.9984L16.144 11.404Z" fill="black"/>
<path d="M4.47734 8.62617C4.65512 8.14284 5.33846 8.14284 5.52179 8.62617L5.81068 9.40951C5.86623 9.55951 5.98846 9.68173 6.13846 9.73728L6.92179 10.0262C7.40512 10.2039 7.40512 10.8873 6.92179 11.0706L6.13846 11.3595C5.98846 11.4151 5.86623 11.5373 5.81068 11.6873L5.52179 12.4706C5.34401 12.954 4.66068 12.954 4.47734 12.4706L4.18845 11.6873C4.1329 11.5373 4.01068 11.4151 3.86068 11.3595L3.07734 11.0706C2.59401 10.8928 2.59401 10.2095 3.07734 10.0262L3.86068 9.73728C4.01068 9.68173 4.1329 9.55951 4.18845 9.40951L4.47734 8.62617Z" fill="black"/>
<path d="M17.222 4.44531H4.99978C4.38867 4.44531 3.88867 4.94531 3.88867 5.55642C3.88867 6.16753 4.38867 6.66753 4.99978 6.66753H9.99978V17.7786C9.99978 18.3898 10.4998 18.8898 11.1109 18.8898C11.722 18.8898 12.222 18.3898 12.222 17.7786V6.66753H17.222C17.8331 6.66753 18.3331 6.16753 18.3331 5.55642C18.3331 4.94531 17.8331 4.44531 17.222 4.44531Z" fill="black"/>
<path d="M6.66623 17.7762C7.27988 17.7762 7.77735 17.2787 7.77735 16.6651C7.77735 16.0514 7.27988 15.5539 6.66623 15.5539C6.05259 15.5539 5.55512 16.0514 5.55512 16.6651C5.55512 17.2787 6.05259 17.7762 6.66623 17.7762Z" fill="#FF7A51"/>
<path d="M16.144 11.404C16.3218 10.9206 17.0051 10.9206 17.1885 11.404L17.7773 12.9984C17.8329 13.1484 17.9551 13.2706 18.1051 13.3262L19.6996 13.9151C20.1829 14.0928 20.1829 14.7762 19.6996 14.9595L18.1051 15.5484C17.9551 15.604 17.8329 15.7262 17.7773 15.8762L17.1885 17.4706C17.0107 17.9539 16.3273 17.9539 16.144 17.4706L15.5551 15.8762C15.4996 15.7262 15.3773 15.604 15.2273 15.5484L13.6329 14.9595C13.1496 14.7817 13.1496 14.0984 13.6329 13.9151L15.2273 13.3262C15.3773 13.2706 15.4996 13.1484 15.5551 12.9984L16.144 11.404Z" fill="#FF7A51"/>
<path d="M4.47734 8.62617C4.65512 8.14284 5.33846 8.14284 5.52179 8.62617L5.81068 9.40951C5.86623 9.55951 5.98846 9.68173 6.13846 9.73728L6.92179 10.0262C7.40512 10.2039 7.40512 10.8873 6.92179 11.0706L6.13846 11.3595C5.98846 11.4151 5.86623 11.5373 5.81068 11.6873L5.52179 12.4706C5.34401 12.954 4.66068 12.954 4.47734 12.4706L4.18845 11.6873C4.1329 11.5373 4.01068 11.4151 3.86068 11.3595L3.07734 11.0706C2.59401 10.8928 2.59401 10.2095 3.07734 10.0262L3.86068 9.73728C4.01068 9.68173 4.1329 9.55951 4.18845 9.40951L4.47734 8.62617Z" fill="#FF7A51"/>
<path d="M17.222 4.44434H4.99978C4.38867 4.44434 3.88867 4.94434 3.88867 5.55545C3.88867 6.16656 4.38867 6.66656 4.99978 6.66656H9.99978V17.7777C9.99978 18.3888 10.4998 18.8888 11.1109 18.8888C11.722 18.8888 12.222 18.3888 12.222 17.7777V6.66656H17.222C17.8331 6.66656 18.3331 6.16656 18.3331 5.55545C18.3331 4.94434 17.8331 4.44434 17.222 4.44434Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,103 +1,31 @@
<template>
<div class="canvas-test">
<!-- <div class="canvas-main" ref="canvasMain" @mousedown="onMouseDown" @wheel="onMouseWheel">
<div
class="canvas-content"
:style="{
top: `${data.y}px`,
left: `${data.x}px`,
transform: `scale(${data.scale})`
}"
>
<result-image />
<card type="cards-select" />
<card type="to-real-style" />
<card type="surface-edit" />
<card type="scene-composition" />
<card type="color-palette" />
<card type="to-3d-model" />
<card type="to-3view" />
</div>
</div> -->
<flow-canvas />
<Assistant />
<button @click="dialogVisible = true">打开画布</button>
<fullscreen-dialog v-model="dialogVisible" hide-destroy>
<flow-canvas :config="config" />
<Assistant />
</fullscreen-dialog>
</div>
</template>
<script setup lang="ts">
import fullscreenDialog from './components/fullscreen-dialog.vue'
import flowCanvas from './FlowCanvas/flow-canvas.vue'
import card from './FlowCanvas/components/nodes/cards/index.vue'
import Assistant from '../Assistant/assistant.vue'
import { computed, ref, markRaw, onMounted, reactive, nextTick } from 'vue'
const data = reactive({
x: 100,
y: 200,
scale: 1
const dialogVisible = ref(true)
const config = ref({
url: 'https://s3-alpha-sig.figma.com/img/ea2f/590e/9638f62a2fc91e31f33db0022db1642c?Expires=1773014400&Key-Pair-Id=APKAQ4GOSFWCW27IBOMQ&Signature=M0B8oJJOk~dGG0aZAqOIocAp7T0LFdJ9FYmCrEZVTCRzYxM6SJRNtYMTX-rTO3Z~s14QINh~o-S41XiZnBv-0zcKjuWot~VVaNHfd0~1LesfNe2KwvCinT~72btFut1pheLnKE-wWCX5ewtonxU77bnw386YPMTqv7DBZzksf2udsJA7NmOYD6~TUG3Q2dWSt~zPH~lkaidscPqpCnCbqzljCEi4RiHY4U3A45l5XypcX2umqn1UaYUFCTqV9471J4qdB6Dg2pcKocdp-7-3s1De6Q~2SmBOrSgDQ~KEADCB2lhKfhxgWmy0lwMvhTd4l90ygVZDWZRABgjHNrGUvg__'
})
const onMouseDown = (e: MouseEvent) => {
// if (e.button !== 1) return
const ox = data.x
const oy = data.y
const X = e.clientX
const Y = e.clientY
const onMouseMove = (e: MouseEvent) => {
const dx = e.clientX - X
const dy = e.clientY - Y
data.x = ox + dx
data.y = oy + dy
}
const onMouseUp = (e: MouseEvent) => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
const onMouseWheel = (e: WheelEvent) => {
var delta = e.deltaY
var scale = data.scale - delta / 1000
if (scale < 0.2) scale = 0.2
if (scale > 10) scale = 10
data.scale = scale
}
</script>
<style lang="less" scoped>
.canvas-test {
// overflow-y: auto;
// display: flex;
// flex-wrap: wrap;
// align-content: flex-start;
// align-items: flex-start;
// padding: 2rem;
// gap: 2rem;
width: 100%;
height: 100%;
background-color: #fcf8f1;
position: relative;
overflow: hidden;
> .canvas-main {
width: 100%;
height: 100%;
position: relative;
> .canvas-content {
position: absolute;
width: auto;
height: auto;
display: flex;
align-items: flex-start;
gap: 5rem;
transform-origin: top left;
}
}
> .vue-flow {
width: 100%;
height: 100%;
}
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -102,7 +102,7 @@
border-radius: 0.4rem;
&:not(.disabled).active,
&:not(.disabled):hover {
background-color: #dfdfdf;
background-color: #ebebeb;
}
&.disabled {
opacity: 0.5;

View File

@@ -1,22 +1,26 @@
<template>
<!-- 结果图片 -->
<div class="result-image">
<div class="header" v-show="showHeader">
<div class="header" v-show="showHeader" @mousedown.stop>
<span class="icon">
<svg-icon name="chat-compose" size="24" size-unit="px" />
<svg-icon name="chat-compose" size="20" size-unit="px" />
</span>
<span class="icon">
<svg-icon name="expand-lg" size="24" size-unit="px" />
<span class="icon" @click="onPreview">
<svg-icon name="expand-lg" size="20" size-unit="px" />
</span>
<span class="icon">
<svg-icon name="download" size="24" size-unit="px" />
<span class="icon" @click="onDownload">
<svg-icon name="download" size="20" size-unit="px" />
</span>
<button class="edit">
<span class="icon"><svg-icon name="edit" size="11" /></span>
<span class="text">Edit</span>
</button>
</div>
<img class="image" :src="data.url" :style="{transform: `scale(${data?.scale?.x || 1}, ${data?.scale?.y || 1})`}" />
<img
class="image"
:src="data.url"
:style="{ transform: `scale(${data?.scale?.x || 1}, ${data?.scale?.y || 1})` }"
/>
<div class="more" @click="showMenu = !showMenu" @mousedown.stop>
<svg-icon name="more" size="24" size-unit="px" color="#C9C9C9" />
</div>
@@ -37,7 +41,9 @@
</template>
<script setup lang="ts">
import { downloadImage } from '../../../tools/tools'
import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch } from 'vue'
const openImagePreview = inject('openImagePreview') as (url: string) => void
const props = defineProps({
config: {
type: Object,
@@ -48,13 +54,19 @@
default: () => ({})
}
})
const emit = defineEmits(['delete-node', 'copy-node', 'bring-to-font', 'send-to-back', 'update-data'])
const emit = defineEmits([
'delete-node',
'copy-node',
'bring-to-font',
'send-to-back',
'update-data'
])
const attrs = useAttrs()
const showHeader = ref(!!attrs.node?.data?.isHeader)
const showMenu = ref(false)
const data = reactive({
url: props.data?.url || '',
scale: props.data?.scale || { x:1,y:1 }
scale: props.data?.scale || { x: 1, y: 1 }
})
watch(
() => props.data.url,
@@ -71,12 +83,44 @@
disabled: !!props.config?.disableDelete
},
{ isDivide: true },
{ label: 'Bring to font', tip: '', on: () => {emit('bring-to-font')} },
{ label: 'Send to back', tip: '', on: () => {emit('send-to-back')} },
{
label: 'Bring to font',
tip: '',
on: () => {
emit('bring-to-font')
}
},
{
label: 'Send to back',
tip: '',
on: () => {
emit('send-to-back')
}
},
{ isDivide: true },
{ label: 'Flip horizontal', tip: '', on: () => {data.scale.x = -data.scale.x; emit('update-data',data)} },
{ label: 'Flip vertical', tip: '', on: () => {data.scale.y = -data.scale.y; emit('update-data',data)} }
{
label: 'Flip horizontal',
tip: '',
on: () => {
data.scale.x = -data.scale.x
emit('update-data', data)
}
},
{
label: 'Flip vertical',
tip: '',
on: () => {
data.scale.y = -data.scale.y
emit('update-data', data)
}
}
])
const onPreview = () => {
openImagePreview(data.url)
}
const onDownload = () => {
downloadImage(data.url, 'image.png')
}
const onMenuItem = (v) => {
if (v.disabled) return
v.on && v.on()
@@ -101,7 +145,7 @@
padding: 25px 6px;
user-select: none;
background-color: #fff;
&.active{
&.active {
box-shadow: 0 15px 21px 0 rgba(0, 0, 0, 0.05), 0 0 0 2px #d9d9d9;
}
> .header {
@@ -126,12 +170,12 @@
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
width: 32px;
height: 32px;
--svg-icon-color: #000;
border-radius: 4px;
border-radius: 3px;
&:hover {
background-color: #dfdfdf;
background-color: #ebebeb;
}
}
> .edit {

View File

@@ -52,6 +52,7 @@
@sub="(e) => flowManager.setZoom(e)"
@home="() => fitView({ maxZoom: 1 })"
/>
<image-preview ref="imagePreviewRef" />
</template>
<script setup lang="ts">
@@ -62,6 +63,7 @@
// 组件
import headerTools from './components/header-tools.vue'
import zoom from '../components/zoom.vue'
import imagePreview from '../components/image-preview.vue'
// 节点
import node from './components/node.vue'
import resultImage from './components/nodes/result-image.vue'
@@ -181,6 +183,13 @@
}
}
const imagePreviewRef = ref<any>()
/** 打开图片预览 */
const openImagePreview = (url: string) => {
imagePreviewRef.value.open(url)
}
provide('openImagePreview', openImagePreview)
onMounted(() => {
// window['vueFlow'] = vueFlow
// window['nodes'] = nodes

View File

@@ -0,0 +1,69 @@
<template>
<el-dialog
class="image-preview"
v-model="showDialog"
align-center
:show-close="false"
width="70vw"
style="border-radius: 20px; padding: 0; --el-dialog-padding-primary: 0"
>
<template #header="{ close }">
<div class="header-close" @click="close">
<svg-icon name="close" size="23" size-unit="px" />
</div>
</template>
<div class="preview-box">
<div class="image-box">
<img :src="url" />
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
const showDialog = ref(false)
const url = ref('')
const open = (url_: any) => {
showDialog.value = true
url.value = url_
}
const close = () => {
showDialog.value = false
}
defineExpose({
open,
close
})
</script>
<style lang="less" scoped>
.header-close {
position: absolute;
top: 30px;
right: 40px;
width: 40px;
height: 40px;
cursor: pointer;
}
.preview-box {
width: 100%;
height: 70vh;
padding: 67px;
> .image-box {
margin: auto;
max-width: 90%;
height: 100%;
padding: 50px;
border-radius: 10px;
border: 1px solid #d9d9d9;
> img {
max-width: 100%;
width: auto;
height: 100%;
object-fit: contain;
}
}
}
</style>

View File

@@ -2,4 +2,17 @@ export const createId = (before: string = 'node') => {
const time = Date.now().toString(36)
const random = Math.random().toString(36).substring(2, 20)
return `${before}_${time}${random}`
}
/** 下载图片 */
export const downloadImage = (url: string, name: string) => {
fetch(url)
.then((res) => res.blob())
.then((blob) => {
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.target = '_blank'
a.download = name || 'image.png'
a.click()
})
}