This commit is contained in:
王佑琳 2026-03-09 18:43:00 +08:00
commit 6c2ac44b33
3 changed files with 520 additions and 0 deletions

2
components.d.ts vendored
View File

@ -12,6 +12,7 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AIgenerate: typeof import('./src/components/AIgenerate/AIgenerate.vue')['default'] 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'] Collection: typeof import('./src/components/collection/index.vue')['default']
copy: typeof import('./src/components/ModelDescription copy.vue')['default'] copy: typeof import('./src/components/ModelDescription copy.vue')['default']
DeepseekPopover: typeof import('./src/components/AIgenerate/DeepseekPopover.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'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader'] ElHeader: typeof import('element-plus/es')['ElHeader']

View File

@ -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>

View File

@ -75,6 +75,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'
import brush from '@/assets/display/brush.svg' import brush from '@/assets/display/brush.svg'
import { useDisplayStore, useParamStore, useUserStore } from '@/stores' import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
import { downloadImage } from '@/utils/downloadImage.js' import { downloadImage } from '@/utils/downloadImage.js'
@ -88,10 +89,17 @@ const props = defineProps({
default: () => ({}) default: () => ({})
} }
}) })
const emit = defineEmits(['open-canvas'])
const useDisplay = useDisplayStore() const useDisplay = useDisplayStore()
const useParams = useParamStore() const useParams = useParamStore()
const useUser = useUserStore() const useUser = useUserStore()
const AIvideo = (file, index) => {
emit('open-canvas', file)
}
const reEdit = (url, number) => { const reEdit = (url, number) => {
console.log(number) console.log(number)
// const index = String(number + 1) // const index = String(number + 1)