增加画笔弹窗组件
This commit is contained in:
parent
d9d7ff22b6
commit
422588ff6e
|
|
@ -12,6 +12,7 @@ export {}
|
|||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AIgenerate: typeof import('./src/components/AIgenerate/AIgenerate.vue')['default']
|
||||
Canvas: typeof import('./src/components/canvas/index.vue')['default']
|
||||
Collection: typeof import('./src/components/collection/index.vue')['default']
|
||||
copy: typeof import('./src/components/ModelDescription copy.vue')['default']
|
||||
DeepseekPopover: typeof import('./src/components/AIgenerate/DeepseekPopover.vue')['default']
|
||||
|
|
@ -24,6 +25,7 @@ declare module 'vue' {
|
|||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,510 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="visible" class="canvas-modal">
|
||||
<div class="canvas-container">
|
||||
<div class="header">
|
||||
<div class="preview-area">
|
||||
<span class="preview-text">图片预览</span>
|
||||
</div>
|
||||
<div class="close-btn" @click="handleClose">
|
||||
<svg viewBox="0 0 1024 1024" width="24" height="24">
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66 0.3L512 563.4l-99.3 119-66.1-0.3c-4.4 0-8-3.6-8-8 0-1.9 0.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 0 1-1.9-5.2c0-4.4 3.6-8 8-8l66.1 0.3L512 464.6l99.3-118.4 66-0.3c4.4 0 8 3.6 8 8 0 1.9-0.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z" fill="#333"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="canvas-area">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
@mousedown="handleMouseDown"
|
||||
@mousemove="handleMouseMove"
|
||||
@mouseup="handleMouseUp"
|
||||
@mouseleave="handleMouseUp"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="input-area">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入描述..."
|
||||
resize="none"
|
||||
/>
|
||||
</div>
|
||||
<div class="control-area">
|
||||
<div class="shape-selector">
|
||||
<div
|
||||
class="shape-btn"
|
||||
:class="{ active: currentShape === 'rectangle' }"
|
||||
@click="currentShape = 'rectangle'"
|
||||
>
|
||||
<svg viewBox="0 0 1024 1024" width="20" height="20">
|
||||
<path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32z m-40 728H184V184h656v656z" fill="#333"/>
|
||||
</svg>
|
||||
<span>矩形</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="shape-btn"
|
||||
:class="{ active: currentShape === 'circle' }"
|
||||
@click="currentShape = 'circle'"
|
||||
>
|
||||
<svg viewBox="0 0 1024 1024" width="20" height="20">
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="#333"/>
|
||||
</svg>
|
||||
<span>圆形</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-btns">
|
||||
<div class="shape-btn" @click="undo">
|
||||
<svg viewBox="0 0 1024 1024" width="20" height="20">
|
||||
<path d="M384 512c0-70.7 57.3-128 128-128s128 57.3 128 128v192h-64v-192c0-35.3-28.7-64-64-64s-64 28.7-64 64v192h-64v-192z" fill="#333"/>
|
||||
<path d="M704 256H192c-35.3 0-64 28.7-64 64v256c0 35.3 28.7 64 64 64h512c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64z m0 320H192V320h512v256z" fill="#333"/>
|
||||
</svg>
|
||||
<span>上一步</span>
|
||||
</div>
|
||||
|
||||
<div class="shape-btn" @click="redo">
|
||||
<svg viewBox="0 0 1024 1024" width="20" height="20">
|
||||
<path d="M640 512c0 70.7-57.3 128-128 128s-128-57.3-128-128v-192h64v192c0 35.3 28.7 64 64 64s64-28.7 64-64v-192h64v192z" fill="#333"/>
|
||||
<path d="M320 256H832c35.3 0 64 28.7 64 64v256c0 35.3-28.7 64-64 64H320c-35.3 0-64-28.7-64-64V320c0-35.3 28.7-64 64-64z m0 320V320h512v256H320z" fill="#333"/>
|
||||
</svg>
|
||||
<span>下一步</span>
|
||||
</div>
|
||||
|
||||
<div class="shape-btn" @click="deleteShape">
|
||||
<svg viewBox="0 0 1024 1024" width="20" height="20">
|
||||
<path d="M416 256h192v512H416V256z m128 640h-64v-448h64v448z m-192 0h-64v-448h64v448z m384-640v512h-192V256h192z m-128 640h-64v-448h64v448z" fill="#333"/>
|
||||
<path d="M864 192H640v-64c0-35.2-28.8-64-64-64H448c-35.2 0-64 28.8-64 64v64H160c-35.2 0-64 28.8-64 64v32h768v-32c0-35.2-28.8-64-64-64z" fill="#333"/>
|
||||
</svg>
|
||||
<span>删除</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-button type="primary" @click="handleSend">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
image: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'send'])
|
||||
|
||||
const canvasRef = ref(null)
|
||||
const currentImage = ref('')
|
||||
const bgImage = ref(null)
|
||||
const scale = ref(1)
|
||||
const inputText = ref('')
|
||||
const currentShape = ref('rectangle')
|
||||
const isDrawing = ref(false)
|
||||
const startX = ref(0)
|
||||
const startY = ref(0)
|
||||
const shapes = ref([])
|
||||
const history = ref([])
|
||||
const historyIndex = ref(-1)
|
||||
const shapeColors = ['#ff0000', '#ff7f00', '#00ff00', '#0000ff', '#8b00ff']
|
||||
const maxShapes = 5
|
||||
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
currentImage.value = props.image
|
||||
bgImage.value = null
|
||||
history.value = []
|
||||
historyIndex.value = -1
|
||||
nextTick(() => {
|
||||
initCanvas()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const initCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
const container = canvas.parentElement
|
||||
const containerWidth = container.clientWidth
|
||||
const containerHeight = container.clientHeight
|
||||
|
||||
if (currentImage.value) {
|
||||
if (currentImage.value.startsWith('data:')) {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const imgScale = Math.min(containerWidth / img.width, containerHeight / img.height)
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
bgImage.value = img
|
||||
scale.value = imgScale
|
||||
}
|
||||
img.src = currentImage.value
|
||||
} else {
|
||||
fetch(currentImage.value)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const imgScale = Math.min(containerWidth / img.width, containerHeight / img.height)
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
bgImage.value = img
|
||||
scale.value = imgScale
|
||||
}
|
||||
img.src = URL.createObjectURL(blob)
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('图片加载失败')
|
||||
canvas.width = containerWidth
|
||||
canvas.height = containerHeight
|
||||
ctx.fillStyle = '#f5f5f5'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.fillStyle = '#999'
|
||||
ctx.font = '20px Microsoft YaHei'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('图片加载失败', canvas.width / 2, canvas.height / 2)
|
||||
scale.value = 1
|
||||
})
|
||||
}
|
||||
} else {
|
||||
canvas.width = containerWidth
|
||||
canvas.height = containerHeight
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
scale.value = 1
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseDown = (e) => {
|
||||
if (shapes.value.length >= maxShapes) {
|
||||
ElMessage.warning('最多只能画5笔')
|
||||
return
|
||||
}
|
||||
isDrawing.value = true
|
||||
const canvas = canvasRef.value
|
||||
const ratio = canvas.width / canvas.offsetWidth
|
||||
startX.value = e.offsetX * ratio
|
||||
startY.value = e.offsetY * ratio
|
||||
}
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
if (!isDrawing.value) return
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const ctx = canvas.getContext('2d')
|
||||
const ratio = canvas.width / canvas.offsetWidth
|
||||
const currentX = e.offsetX * ratio
|
||||
const currentY = e.offsetY * ratio
|
||||
const savedStartX = startX.value
|
||||
const savedStartY = startY.value
|
||||
const savedCurrentX = currentX
|
||||
const savedCurrentY = currentY
|
||||
|
||||
if (bgImage.value) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.drawImage(bgImage.value, 0, 0)
|
||||
|
||||
shapes.value.forEach(shape => {
|
||||
drawShape(ctx, shape)
|
||||
})
|
||||
|
||||
const currentShapeData = {
|
||||
type: currentShape.value,
|
||||
startX: savedStartX,
|
||||
startY: savedStartY,
|
||||
endX: savedCurrentX,
|
||||
endY: savedCurrentY,
|
||||
color: shapeColors[shapes.value.length] || '#ff0000'
|
||||
}
|
||||
drawShape(ctx, currentShapeData)
|
||||
} else {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
shapes.value.forEach(shape => {
|
||||
drawShape(ctx, shape)
|
||||
})
|
||||
|
||||
const currentShapeData = {
|
||||
type: currentShape.value,
|
||||
startX: startX.value,
|
||||
startY: startY.value,
|
||||
endX: currentX,
|
||||
endY: currentY,
|
||||
color: shapeColors[shapes.value.length] || '#ff0000'
|
||||
}
|
||||
drawShape(ctx, currentShapeData)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseUp = (e) => {
|
||||
if (!isDrawing.value) return
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const ratio = canvas.width / canvas.offsetWidth
|
||||
const endX = e.offsetX * ratio
|
||||
const endY = e.offsetY * ratio
|
||||
|
||||
if (shapes.value.length >= maxShapes) {
|
||||
ElMessage.warning('最多只能画5笔')
|
||||
isDrawing.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const colorIndex = shapes.value.length
|
||||
shapes.value.push({
|
||||
type: currentShape.value,
|
||||
startX: startX.value,
|
||||
startY: startY.value,
|
||||
endX: endX,
|
||||
endY: endY,
|
||||
color: shapeColors[colorIndex]
|
||||
})
|
||||
|
||||
saveHistory()
|
||||
isDrawing.value = false
|
||||
}
|
||||
|
||||
const saveHistory = () => {
|
||||
history.value = history.value.slice(0, historyIndex.value + 1)
|
||||
history.value.push([...shapes.value])
|
||||
historyIndex.value = history.value.length - 1
|
||||
}
|
||||
|
||||
const undo = () => {
|
||||
if (historyIndex.value >= 0) {
|
||||
const newIndex = historyIndex.value - 1
|
||||
historyIndex.value = newIndex
|
||||
shapes.value = newIndex >= 0 ? [...history.value[newIndex]] : []
|
||||
redrawCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
const redo = () => {
|
||||
if (historyIndex.value < history.value.length - 1) {
|
||||
historyIndex.value++
|
||||
shapes.value = [...history.value[historyIndex.value]]
|
||||
redrawCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteShape = () => {
|
||||
shapes.value = []
|
||||
saveHistory()
|
||||
redrawCanvas()
|
||||
}
|
||||
|
||||
const redrawCanvas = () => {
|
||||
const canvas = canvasRef.value
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
if (bgImage.value) {
|
||||
ctx.drawImage(bgImage.value, 0, 0)
|
||||
} else {
|
||||
ctx.fillStyle = '#fff'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
shapes.value.forEach(shape => {
|
||||
drawShape(ctx, shape)
|
||||
})
|
||||
}
|
||||
|
||||
const drawShape = (ctx, shape) => {
|
||||
ctx.strokeStyle = shape.color || '#ff0000'
|
||||
ctx.lineWidth = 2
|
||||
|
||||
if (shape.type === 'rectangle') {
|
||||
const width = shape.endX - shape.startX
|
||||
const height = shape.endY - shape.startY
|
||||
ctx.strokeRect(shape.startX, shape.startY, width, height)
|
||||
} else if (shape.type === 'circle') {
|
||||
const radius = Math.sqrt(
|
||||
Math.pow(shape.endX - shape.startX, 2) + Math.pow(shape.endY - shape.startY, 2)
|
||||
)
|
||||
ctx.beginPath()
|
||||
ctx.arc(shape.startX, shape.startY, radius, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
shapes.value = []
|
||||
inputText.value = ''
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
const canvas = canvasRef.value
|
||||
const imageData = canvas.toDataURL('image/png')
|
||||
emit('send', {
|
||||
image: imageData,
|
||||
text: inputText.value,
|
||||
shapes: shapes.value
|
||||
})
|
||||
handleClose()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.canvas-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
width: 40vw;
|
||||
height: 70vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
.preview-area {
|
||||
.preview-text {
|
||||
color: #333;
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f3f4f5;
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
background: #fff;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
padding: 15px 20px;
|
||||
|
||||
.input-area {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.control-area {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shape-selector {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.shape-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #409eff;
|
||||
border-color: #409eff;
|
||||
color: #fff;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: 20px;
|
||||
|
||||
.shape-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -74,6 +74,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import brush from '@/assets/display/brush.svg'
|
||||
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
|
||||
import { downloadImage } from '@/utils/downloadImage.js'
|
||||
|
|
@ -84,10 +85,17 @@ const props = defineProps({
|
|||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['open-canvas'])
|
||||
|
||||
const useDisplay = useDisplayStore()
|
||||
const useParams = useParamStore()
|
||||
const useUser = useUserStore()
|
||||
|
||||
const AIvideo = (file, index) => {
|
||||
emit('open-canvas', file)
|
||||
}
|
||||
|
||||
const reEdit = (url, number) => {
|
||||
console.log(number)
|
||||
// const index = String(number + 1)
|
||||
|
|
|
|||
|
|
@ -39,17 +39,25 @@
|
|||
:active="active"
|
||||
:data-index="index"
|
||||
>
|
||||
<Set :key="`${item.id}`" :item="item" />
|
||||
<Set :key="`${item.id}`" :item="item" @open-canvas="handleOpenCanvas" />
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
|
||||
<CanvasDialog
|
||||
v-model:visible="canvasVisible"
|
||||
:image="currentCanvasImage"
|
||||
@send="handleCanvasSend"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useDisplayStore, useParamStore } from '@/stores'
|
||||
import Set from './components/set.vue'
|
||||
import RefreshOverlay from './components/RefreshOverlay.vue'
|
||||
import CanvasDialog from '@/components/canvas/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
if: {
|
||||
|
|
@ -65,6 +73,18 @@ const props = defineProps({
|
|||
const useDisplay = useDisplayStore()
|
||||
const useParams = useParamStore() // 参数状态管理
|
||||
|
||||
const canvasVisible = ref(false)
|
||||
const currentCanvasImage = ref('')
|
||||
|
||||
const handleOpenCanvas = (file) => {
|
||||
currentCanvasImage.value = file
|
||||
canvasVisible.value = true
|
||||
}
|
||||
|
||||
const handleCanvasSend = (data) => {
|
||||
console.log('发送数据:', data)
|
||||
}
|
||||
|
||||
const refreshing = ref(false)
|
||||
const activeTab = ref('all')
|
||||
// const tempList = ref([])
|
||||
|
|
|
|||
Loading…
Reference in New Issue