|
@@ -0,0 +1,852 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * useCanvasLayers - Canvas, page, layer state and methods for CreateTemplate
|
|
|
|
|
+ */
|
|
|
|
|
+import { ref, computed, reactive, nextTick } from 'vue'
|
|
|
|
|
+import { resolveMaterialUrl, getImageLoadUrl, toStoragePath } from '../utils.js'
|
|
|
|
|
+import { SHAPE_PRESETS, TEXT_PRESETS, RATIO_CONFIG } from '../constants.js'
|
|
|
|
|
+
|
|
|
|
|
+export function useCanvasLayers() {
|
|
|
|
|
+ const canvasRef = ref(null)
|
|
|
|
|
+ const canvasAreaRef = ref(null)
|
|
|
|
|
+ const layerListRef = ref(null)
|
|
|
|
|
+ const currentTool = ref('select')
|
|
|
|
|
+ const canvasWidth = ref(600)
|
|
|
|
|
+ const canvasHeight = ref(450)
|
|
|
|
|
+ const canvasRatio = ref('4:3')
|
|
|
|
|
+ const zoomLevel = ref(100)
|
|
|
|
|
+ const maintainAspectRatio = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+ let pageIdCounter = 0
|
|
|
|
|
+ const pages = ref([{ id: ++pageIdCounter, layers: [] }])
|
|
|
|
|
+ const currentPageIndex = ref(0)
|
|
|
|
|
+ const layers = ref(pages.value[0].layers)
|
|
|
|
|
+ const selectedLayerId = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+ const dragState = reactive({
|
|
|
|
|
+ draggingId: null,
|
|
|
|
|
+ dragOverId: null,
|
|
|
|
|
+ draggedLayer: null
|
|
|
|
|
+ })
|
|
|
|
|
+ const layerNameEditingId = ref(null)
|
|
|
|
|
+ const addTextViewMode = ref('list')
|
|
|
|
|
+ const addShapeViewMode = ref('list')
|
|
|
|
|
+ const textLayerCount = ref(0)
|
|
|
|
|
+
|
|
|
|
|
+ let layerIdCounter = 0
|
|
|
|
|
+
|
|
|
|
|
+ const selectedLayer = computed(() =>
|
|
|
|
|
+ layers.value.find(l => l.id === selectedLayerId.value) || null
|
|
|
|
|
+ )
|
|
|
|
|
+ const reversedLayers = computed(() => [...layers.value].reverse())
|
|
|
|
|
+ const selectedLayerIndex = computed(() =>
|
|
|
|
|
+ layers.value.findIndex(l => l.id === selectedLayerId.value)
|
|
|
|
|
+ )
|
|
|
|
|
+ const canMoveUp = computed(
|
|
|
|
|
+ () => selectedLayerId.value && selectedLayerIndex.value < layers.value.length - 1
|
|
|
|
|
+ )
|
|
|
|
|
+ const canMoveDown = computed(
|
|
|
|
|
+ () => selectedLayerId.value && selectedLayerIndex.value > 0
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ function mapRowToLayer(row) {
|
|
|
|
|
+ const width = Number(row.width ?? 100) || 100
|
|
|
|
|
+ const height = Number(row.height ?? 100) || 100
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: ++layerIdCounter,
|
|
|
|
|
+ name: row.layer_name,
|
|
|
|
|
+ type: row.layer_type === 'text' ? 'text' : row.layer_type === 'shape' ? 'shape' : 'image',
|
|
|
|
|
+ url: row.layer_type === 'shape' ? '' : resolveMaterialUrl(row.material_url || ''),
|
|
|
|
|
+ text: row.text_content || '',
|
|
|
|
|
+ x: Number(row.position_x ?? 0) || 0,
|
|
|
|
|
+ y: Number(row.position_y ?? 0) || 0,
|
|
|
|
|
+ width,
|
|
|
|
|
+ height,
|
|
|
|
|
+ rotation: Number(row.rotation ?? 0) || 0,
|
|
|
|
|
+ opacity: Number(row.opacity ?? 100) || 100,
|
|
|
|
|
+ visible: String(row.visible ?? '1') !== '0',
|
|
|
|
|
+ locked: String(row.locked ?? '0') === '1',
|
|
|
|
|
+ shapeType: row.shape_type || 'rect',
|
|
|
|
|
+ fillMode: row.fill_mode || 'solid',
|
|
|
|
|
+ fillColor: row.fill_color || '#e0e0e0',
|
|
|
|
|
+ strokeColor: row.stroke_color || '#606266',
|
|
|
|
|
+ strokeWidth: Number(row.stroke_width ?? 2) || 2,
|
|
|
|
|
+ fontFamily: row.font_family || 'Arial',
|
|
|
|
|
+ fontSize: Number(row.font_size || 16) || 16,
|
|
|
|
|
+ color: row.font_color || '#000000',
|
|
|
|
|
+ backgroundColor: row.background_color || 'transparent',
|
|
|
|
|
+ textAlign: row.text_align || 'left',
|
|
|
|
|
+ fontWeight: row.font_weight || 'normal',
|
|
|
|
|
+ fontStyle: row.font_style || 'normal',
|
|
|
|
|
+ textDecoration: row.font_underline === 'underline' ? 'underline' : 'none',
|
|
|
|
|
+ lineHeight: Number(row.line_height || 1.5) || 1.5,
|
|
|
|
|
+ letterSpacing: Number(row.letter_spacing || 0) || 0,
|
|
|
|
|
+ backgroundBorderRadius: Number(row.background_border_radius ?? 0) || 0,
|
|
|
|
|
+ originalWidth: width,
|
|
|
|
|
+ originalHeight: height,
|
|
|
|
|
+ zIndex: Number(row.z_index || 0) || 0,
|
|
|
|
|
+ materialId: row.material_id,
|
|
|
|
|
+ templateId: row.template_id
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const syncLayersToCurrentPage = () => {
|
|
|
|
|
+ const p = pages.value[currentPageIndex.value]
|
|
|
|
|
+ layers.value = p ? p.layers : []
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const addPage = () => {
|
|
|
|
|
+ pages.value.push({ id: ++pageIdCounter, layers: [] })
|
|
|
|
|
+ currentPageIndex.value = pages.value.length - 1
|
|
|
|
|
+ syncLayersToCurrentPage()
|
|
|
|
|
+ selectedLayerId.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const switchPage = (index) => {
|
|
|
|
|
+ if (index < 0 || index >= pages.value.length) return
|
|
|
|
|
+ currentPageIndex.value = index
|
|
|
|
|
+ syncLayersToCurrentPage()
|
|
|
|
|
+ const pageLayers = pages.value[index]?.layers ?? []
|
|
|
|
|
+ selectedLayerId.value = pageLayers.length
|
|
|
|
|
+ ? pageLayers[pageLayers.length - 1].id
|
|
|
|
|
+ : null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const deletePage = (index) => {
|
|
|
|
|
+ if (pages.value.length <= 1) return
|
|
|
|
|
+ pages.value.splice(index, 1)
|
|
|
|
|
+ if (currentPageIndex.value >= pages.value.length) {
|
|
|
|
|
+ currentPageIndex.value = Math.max(0, pages.value.length - 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ syncLayersToCurrentPage()
|
|
|
|
|
+ selectedLayerId.value = layers.value.length ? layers.value[layers.value.length - 1].id : null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const clearDesign = () => {
|
|
|
|
|
+ pageIdCounter = 0
|
|
|
|
|
+ pages.value = [{ id: ++pageIdCounter, layers: [] }]
|
|
|
|
|
+ currentPageIndex.value = 0
|
|
|
|
|
+ syncLayersToCurrentPage()
|
|
|
|
|
+ selectedLayerId.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseDataToPages(data) {
|
|
|
|
|
+ if (Array.isArray(data?.pages) && data.pages.length) {
|
|
|
|
|
+ pageIdCounter = 0
|
|
|
|
|
+ return data.pages.map(p => ({
|
|
|
|
|
+ id: ++pageIdCounter,
|
|
|
|
|
+ layers: (Array.isArray(p?.layers) ? p.layers : []).map(mapRowToLayer)
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ let rows = Array.isArray(data) ? data : (data?.layers ?? data?.data ?? [])
|
|
|
|
|
+ if (!Array.isArray(rows) || !rows.length) return null
|
|
|
|
|
+
|
|
|
|
|
+ const hasPageIndex = rows.some(r => r.page_index != null)
|
|
|
|
|
+ if (hasPageIndex) {
|
|
|
|
|
+ const byPage = new Map()
|
|
|
|
|
+ for (const row of rows) {
|
|
|
|
|
+ const idx = Number(row.page_index ?? 0) || 0
|
|
|
|
|
+ if (!byPage.has(idx)) byPage.set(idx, [])
|
|
|
|
|
+ byPage.get(idx).push(row)
|
|
|
|
|
+ }
|
|
|
|
|
+ const sorted = [...byPage.entries()].sort((a, b) => a[0] - b[0])
|
|
|
|
|
+ pageIdCounter = 0
|
|
|
|
|
+ return sorted.map(([, pRows]) => ({
|
|
|
|
|
+ id: ++pageIdCounter,
|
|
|
|
|
+ layers: pRows.map(mapRowToLayer)
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pageIdCounter = 0
|
|
|
|
|
+ return [{ id: ++pageIdCounter, layers: rows.map(mapRowToLayer) }]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const processUploadedFile = (file, category) => {
|
|
|
|
|
+ return new Promise(resolve => {
|
|
|
|
|
+ const reader = new FileReader()
|
|
|
|
|
+ reader.onload = (e) => {
|
|
|
|
|
+ const img = new Image()
|
|
|
|
|
+ img.onload = () => {
|
|
|
|
|
+ const maxSize = 300
|
|
|
|
|
+ let width = img.width
|
|
|
|
|
+ let height = img.height
|
|
|
|
|
+ if (width > maxSize || height > maxSize) {
|
|
|
|
|
+ const ratio = Math.min(maxSize / width, maxSize / height)
|
|
|
|
|
+ width = width * ratio
|
|
|
|
|
+ height = height * ratio
|
|
|
|
|
+ }
|
|
|
|
|
+ const newLayer = {
|
|
|
|
|
+ id: ++layerIdCounter,
|
|
|
|
|
+ name: file.name,
|
|
|
|
|
+ url: e.target.result,
|
|
|
|
|
+ materialType: category,
|
|
|
|
|
+ x: Math.round((canvasWidth.value - width) / 2),
|
|
|
|
|
+ y: Math.round((canvasHeight.value - height) / 2),
|
|
|
|
|
+ width: Math.round(width),
|
|
|
|
|
+ height: Math.round(height),
|
|
|
|
|
+ rotation: 0,
|
|
|
|
|
+ opacity: 100,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ locked: false,
|
|
|
|
|
+ originalWidth: img.width,
|
|
|
|
|
+ originalHeight: img.height,
|
|
|
|
|
+ fileSize: file.size
|
|
|
|
|
+ }
|
|
|
|
|
+ layers.value.push(newLayer)
|
|
|
|
|
+ selectedLayerId.value = newLayer.id
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ }
|
|
|
|
|
+ img.src = e.target.result
|
|
|
|
|
+ }
|
|
|
|
|
+ reader.readAsDataURL(file)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const addShapeLayer = (preset) => {
|
|
|
|
|
+ const def = SHAPE_PRESETS[preset] || SHAPE_PRESETS.rect
|
|
|
|
|
+ const newLayer = {
|
|
|
|
|
+ id: ++layerIdCounter,
|
|
|
|
|
+ name: `形状 ${layerIdCounter}`,
|
|
|
|
|
+ type: 'shape',
|
|
|
|
|
+ shapeType: def.shapeType,
|
|
|
|
|
+ x: Math.round((canvasWidth.value - def.width) / 2),
|
|
|
|
|
+ y: Math.round((canvasHeight.value - def.height) / 2),
|
|
|
|
|
+ width: def.width,
|
|
|
|
|
+ height: def.height,
|
|
|
|
|
+ rotation: 0,
|
|
|
|
|
+ opacity: 100,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ locked: false,
|
|
|
|
|
+ fillColor: '#e0e0e0',
|
|
|
|
|
+ fillMode: 'solid',
|
|
|
|
|
+ strokeColor: '#606266',
|
|
|
|
|
+ strokeWidth: 2
|
|
|
|
|
+ }
|
|
|
|
|
+ layers.value.push(newLayer)
|
|
|
|
|
+ selectedLayerId.value = newLayer.id
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const addTextLayer = (preset) => {
|
|
|
|
|
+ const def = preset ? TEXT_PRESETS[preset] : { fontSize: 16, fontWeight: 'normal', text: '双击编辑文字', name: '文字' }
|
|
|
|
|
+ const count = textLayerCount.value + 1
|
|
|
|
|
+ const name = preset ? `${def.name} ${count}` : '文字 ' + count
|
|
|
|
|
+ const newLayer = {
|
|
|
|
|
+ id: ++layerIdCounter,
|
|
|
|
|
+ name,
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: def.text,
|
|
|
|
|
+ x: canvasWidth.value / 2 - 50,
|
|
|
|
|
+ y: canvasHeight.value / 2 - 15,
|
|
|
|
|
+ width: 120,
|
|
|
|
|
+ height: Math.max(30, def.fontSize + 8),
|
|
|
|
|
+ rotation: 0,
|
|
|
|
|
+ opacity: 100,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ locked: false,
|
|
|
|
|
+ fontSize: def.fontSize,
|
|
|
|
|
+ fontFamily: 'Arial',
|
|
|
|
|
+ fontWeight: def.fontWeight || 'normal',
|
|
|
|
|
+ fontStyle: 'normal',
|
|
|
|
|
+ textDecoration: 'none',
|
|
|
|
|
+ color: '#000000',
|
|
|
|
|
+ textAlign: 'left',
|
|
|
|
|
+ backgroundColor: 'transparent',
|
|
|
|
|
+ backgroundBorderRadius: 0,
|
|
|
|
|
+ lineHeight: 1.5,
|
|
|
|
|
+ letterSpacing: 0
|
|
|
|
|
+ }
|
|
|
|
|
+ layers.value.push(newLayer)
|
|
|
|
|
+ selectedLayerId.value = newLayer.id
|
|
|
|
|
+ textLayerCount.value++
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const addMaterialToCanvas = (material) => {
|
|
|
|
|
+ const img = new Image()
|
|
|
|
|
+ img.onload = function () {
|
|
|
|
|
+ let width = img.width
|
|
|
|
|
+ let height = img.height
|
|
|
|
|
+ const maxSize = 200
|
|
|
|
|
+ if (width > maxSize || height > maxSize) {
|
|
|
|
|
+ const ratio = Math.min(maxSize / width, maxSize / height)
|
|
|
|
|
+ width = width * ratio
|
|
|
|
|
+ height = height * ratio
|
|
|
|
|
+ }
|
|
|
|
|
+ const newLayer = {
|
|
|
|
|
+ id: ++layerIdCounter,
|
|
|
|
|
+ name: `素材 ${layerIdCounter}`,
|
|
|
|
|
+ url: resolveMaterialUrl(material.material_url),
|
|
|
|
|
+ x: Math.round((canvasWidth.value - width) / 2),
|
|
|
|
|
+ y: Math.round((canvasHeight.value - height) / 2),
|
|
|
|
|
+ width: Math.round(width),
|
|
|
|
|
+ height: Math.round(height),
|
|
|
|
|
+ rotation: 0,
|
|
|
|
|
+ opacity: 100,
|
|
|
|
|
+ visible: true,
|
|
|
|
|
+ locked: false,
|
|
|
|
|
+ originalWidth: img.width,
|
|
|
|
|
+ originalHeight: img.height,
|
|
|
|
|
+ materialId: material.id ?? material.material_id ?? null
|
|
|
|
|
+ }
|
|
|
|
|
+ layers.value.push(newLayer)
|
|
|
|
|
+ selectedLayerId.value = newLayer.id
|
|
|
|
|
+ }
|
|
|
|
|
+ img.src = resolveMaterialUrl(material.material_url)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const getLayerStyle = (layer) => {
|
|
|
|
|
+ if (!layer.visible) return { display: 'none' }
|
|
|
|
|
+ return {
|
|
|
|
|
+ left: layer.x + 'px',
|
|
|
|
|
+ top: layer.y + 'px',
|
|
|
|
|
+ width: layer.width + 'px',
|
|
|
|
|
+ height: layer.height + 'px',
|
|
|
|
|
+ transform: `rotate(${layer.rotation}deg)`,
|
|
|
|
|
+ opacity: layer.opacity / 100
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const getShapeStyle = (layer) => {
|
|
|
|
|
+ const fillMode = layer.fillMode || 'solid'
|
|
|
|
|
+ const fill = fillMode === 'none' ? 'transparent' : (layer.fillColor || '#e0e0e0')
|
|
|
|
|
+ const stroke = layer.strokeColor || '#606266'
|
|
|
|
|
+ const sw = layer.strokeWidth ?? 2
|
|
|
|
|
+ const isLine = layer.shapeType === 'line'
|
|
|
|
|
+ let borderRadius = '0'
|
|
|
|
|
+ if (layer.shapeType === 'circle') borderRadius = '50%'
|
|
|
|
|
+ else if (layer.shapeType === 'ellipse') borderRadius = '50%'
|
|
|
|
|
+ return {
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ height: '100%',
|
|
|
|
|
+ backgroundColor: isLine ? stroke : fill,
|
|
|
|
|
+ border: isLine ? 'none' : `${sw}px solid ${stroke}`,
|
|
|
|
|
+ borderRadius,
|
|
|
|
|
+ boxSizing: 'border-box'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const getTextStyle = (layer) => {
|
|
|
|
|
+ return {
|
|
|
|
|
+ fontSize: layer.fontSize + 'px',
|
|
|
|
|
+ fontFamily: layer.fontFamily,
|
|
|
|
|
+ fontWeight: layer.fontWeight,
|
|
|
|
|
+ fontStyle: layer.fontStyle,
|
|
|
|
|
+ textDecoration: layer.textDecoration,
|
|
|
|
|
+ color: layer.color,
|
|
|
|
|
+ textAlign: layer.textAlign,
|
|
|
|
|
+ backgroundColor: layer.backgroundColor,
|
|
|
|
|
+ borderRadius: (layer.backgroundBorderRadius ?? 0) + 'px',
|
|
|
|
|
+ lineHeight: layer.lineHeight,
|
|
|
|
|
+ letterSpacing: layer.letterSpacing + 'px',
|
|
|
|
|
+ padding: '4px 8px',
|
|
|
|
|
+ whiteSpace: 'pre-wrap',
|
|
|
|
|
+ userSelect: 'none'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const selectLayer = (id) => { selectedLayerId.value = id }
|
|
|
|
|
+ const toggleLayerVisibility = (id) => {
|
|
|
|
|
+ const layer = layers.value.find(l => l.id === id)
|
|
|
|
|
+ if (layer) layer.visible = !layer.visible
|
|
|
|
|
+ }
|
|
|
|
|
+ const toggleLayerLock = (id) => {
|
|
|
|
|
+ const layer = layers.value.find(l => l.id === id)
|
|
|
|
|
+ if (layer) layer.locked = !layer.locked
|
|
|
|
|
+ }
|
|
|
|
|
+ const moveLayerUp = () => {
|
|
|
|
|
+ const index = selectedLayerIndex.value
|
|
|
|
|
+ if (index < layers.value.length - 1) {
|
|
|
|
|
+ ;[layers.value[index], layers.value[index + 1]] = [layers.value[index + 1], layers.value[index]]
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const moveLayerDown = () => {
|
|
|
|
|
+ const index = selectedLayerIndex.value
|
|
|
|
|
+ if (index > 0) {
|
|
|
|
|
+ ;[layers.value[index], layers.value[index - 1]] = [layers.value[index - 1], layers.value[index]]
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const deleteLayer = () => {
|
|
|
|
|
+ const index = selectedLayerIndex.value
|
|
|
|
|
+ if (index !== -1) {
|
|
|
|
|
+ layers.value.splice(index, 1)
|
|
|
|
|
+ selectedLayerId.value = layers.value.length > 0 ? layers.value[0].id : null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const deleteLayerById = (id) => {
|
|
|
|
|
+ const index = layers.value.findIndex(l => l.id === id)
|
|
|
|
|
+ if (index !== -1) {
|
|
|
|
|
+ layers.value.splice(index, 1)
|
|
|
|
|
+ selectedLayerId.value = selectedLayerId.value === id ? (layers.value.length > 0 ? layers.value[0].id : null) : selectedLayerId.value
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const openTextAddMore = () => { addTextViewMode.value = 'more' }
|
|
|
|
|
+ const backTextAddList = () => { addTextViewMode.value = 'list' }
|
|
|
|
|
+ const openShapeAddMore = () => { addShapeViewMode.value = 'more' }
|
|
|
|
|
+ const backShapeAddList = () => { addShapeViewMode.value = 'list' }
|
|
|
|
|
+
|
|
|
|
|
+ // Layer name edit
|
|
|
|
|
+ const layerNameInputRefs = new Map()
|
|
|
|
|
+ const setLayerNameInputRef = (id, el) => {
|
|
|
|
|
+ if (el) layerNameInputRefs.set(id, el)
|
|
|
|
|
+ else layerNameInputRefs.delete(id)
|
|
|
|
|
+ }
|
|
|
|
|
+ const setLayerNameEditingId = (id) => {
|
|
|
|
|
+ layerNameEditingId.value = id
|
|
|
|
|
+ }
|
|
|
|
|
+ const openLayerNameEdit = (layer) => {
|
|
|
|
|
+ layerNameEditingId.value = layer.id
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ const inputComp = layerNameInputRefs.get(layer.id)
|
|
|
|
|
+ inputComp?.focus?.()
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Drag/drop for layer list
|
|
|
|
|
+ const handleDragStart = (e, layer) => {
|
|
|
|
|
+ if (layerNameEditingId.value === layer.id) { e.preventDefault(); return }
|
|
|
|
|
+ if (layer.locked) { e.preventDefault(); return }
|
|
|
|
|
+ dragState.draggingId = layer.id
|
|
|
|
|
+ dragState.draggedLayer = layer
|
|
|
|
|
+ e.dataTransfer.effectAllowed = 'move'
|
|
|
|
|
+ e.dataTransfer.setData('text/plain', layer.id)
|
|
|
|
|
+ if (e.target?.style) e.target.style.opacity = '0.5'
|
|
|
|
|
+ }
|
|
|
|
|
+ const handleDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move' }
|
|
|
|
|
+ const handleDragEnter = (e, layer) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ if (dragState.draggingId !== layer.id) dragState.dragOverId = layer.id
|
|
|
|
|
+ }
|
|
|
|
|
+ const handleDragLeave = (e) => {
|
|
|
|
|
+ if (!e.currentTarget.contains(e.relatedTarget)) dragState.dragOverId = null
|
|
|
|
|
+ }
|
|
|
|
|
+ const handleDrop = (e, targetLayer) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ e.stopPropagation()
|
|
|
|
|
+ const draggedId = dragState.draggingId
|
|
|
|
|
+ if (draggedId && draggedId !== targetLayer.id) {
|
|
|
|
|
+ const draggedIndex = layers.value.findIndex(l => l.id === draggedId)
|
|
|
|
|
+ const targetIndex = layers.value.findIndex(l => l.id === targetLayer.id)
|
|
|
|
|
+ if (draggedIndex !== -1 && targetIndex !== -1) {
|
|
|
|
|
+ const [movedLayer] = layers.value.splice(draggedIndex, 1)
|
|
|
|
|
+ layers.value.splice(targetIndex, 0, movedLayer)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ dragState.dragOverId = null
|
|
|
|
|
+ }
|
|
|
|
|
+ const handleDragEnd = (e) => {
|
|
|
|
|
+ if (e.target?.style) e.target.style.opacity = '1'
|
|
|
|
|
+ dragState.draggingId = null
|
|
|
|
|
+ dragState.dragOverId = null
|
|
|
|
|
+ dragState.draggedLayer = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Canvas interaction
|
|
|
|
|
+ let isDragging = false, isResizing = false, isRotating = false
|
|
|
|
|
+ let dragStartX = 0, dragStartY = 0
|
|
|
|
|
+ let layerStartX = 0, layerStartY = 0
|
|
|
|
|
+ let resizeDirection = ''
|
|
|
|
|
+ let startWidth = 0, startHeight = 0
|
|
|
|
|
+ let centerX = 0, centerY = 0
|
|
|
|
|
+
|
|
|
|
|
+ const getCanvasScale = () => {
|
|
|
|
|
+ const el = canvasRef.value
|
|
|
|
|
+ if (!el) return { scaleX: 1, scaleY: 1 }
|
|
|
|
|
+ const rect = el.getBoundingClientRect()
|
|
|
|
|
+ return {
|
|
|
|
|
+ scaleX: canvasWidth.value / (rect.width || 1),
|
|
|
|
|
+ scaleY: canvasHeight.value / (rect.height || 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleLayerMouseDown = (e, layer) => {
|
|
|
|
|
+ if (currentTool.value !== 'select' && currentTool.value !== 'move') return
|
|
|
|
|
+ if (layer.locked) { selectedLayerId.value = layer.id; return }
|
|
|
|
|
+ selectedLayerId.value = layer.id
|
|
|
|
|
+ isDragging = true
|
|
|
|
|
+ dragStartX = e.clientX
|
|
|
|
|
+ dragStartY = e.clientY
|
|
|
|
|
+ layerStartX = layer.x
|
|
|
|
|
+ layerStartY = layer.y
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleCanvasMouseDown = (e) => {
|
|
|
|
|
+ if (canvasRef.value && e.target === canvasRef.value) selectedLayerId.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleCanvasMouseMove = (e) => {
|
|
|
|
|
+ if (!selectedLayer.value) return
|
|
|
|
|
+ const { scaleX, scaleY } = getCanvasScale()
|
|
|
|
|
+ if (isDragging) {
|
|
|
|
|
+ const deltaX = (e.clientX - dragStartX) * scaleX
|
|
|
|
|
+ const deltaY = (e.clientY - dragStartY) * scaleY
|
|
|
|
|
+ selectedLayer.value.x = Math.round(layerStartX + deltaX)
|
|
|
|
|
+ selectedLayer.value.y = Math.round(layerStartY + deltaY)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isResizing) {
|
|
|
|
|
+ const deltaX = (e.clientX - dragStartX) * scaleX
|
|
|
|
|
+ const deltaY = (e.clientY - dragStartY) * scaleY
|
|
|
|
|
+ const layer = selectedLayer.value
|
|
|
|
|
+ const aspectRatio = layer.originalWidth / layer.originalHeight
|
|
|
|
|
+ let newWidth = startWidth
|
|
|
|
|
+ let newHeight = startHeight
|
|
|
|
|
+ if (resizeDirection.includes('e')) newWidth = Math.max(20, startWidth + deltaX)
|
|
|
|
|
+ if (resizeDirection.includes('w')) newWidth = Math.max(20, startWidth - deltaX)
|
|
|
|
|
+ if (resizeDirection.includes('s')) newHeight = Math.max(20, startHeight + deltaY)
|
|
|
|
|
+ if (resizeDirection.includes('n')) newHeight = Math.max(20, startHeight - deltaY)
|
|
|
|
|
+ if (maintainAspectRatio.value) {
|
|
|
|
|
+ if (['se', 'nw', 'ne', 'sw'].includes(resizeDirection)) newHeight = newWidth / aspectRatio
|
|
|
|
|
+ else if (resizeDirection.includes('e') || resizeDirection.includes('w')) newHeight = newWidth / aspectRatio
|
|
|
|
|
+ else if (resizeDirection.includes('n') || resizeDirection.includes('s')) newWidth = newHeight * aspectRatio
|
|
|
|
|
+ }
|
|
|
|
|
+ if (resizeDirection.includes('w')) layer.x = Math.round(layerStartX + (startWidth - newWidth))
|
|
|
|
|
+ if (resizeDirection.includes('n')) layer.y = Math.round(layerStartY + (startHeight - newHeight))
|
|
|
|
|
+ layer.width = Math.round(newWidth)
|
|
|
|
|
+ layer.height = Math.round(newHeight)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isRotating) {
|
|
|
|
|
+ const rect = canvasRef.value?.getBoundingClientRect()
|
|
|
|
|
+ if (rect) {
|
|
|
|
|
+ const mouseX = (e.clientX - rect.left) * scaleX
|
|
|
|
|
+ const mouseY = (e.clientY - rect.top) * scaleY
|
|
|
|
|
+ const angle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI
|
|
|
|
|
+ selectedLayer.value.rotation = Math.round(angle + 90)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleCanvasMouseUp = () => {
|
|
|
|
|
+ isDragging = false
|
|
|
|
|
+ isResizing = false
|
|
|
|
|
+ isRotating = false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const startResize = (e, direction) => {
|
|
|
|
|
+ if (!selectedLayer.value) return
|
|
|
|
|
+ isResizing = true
|
|
|
|
|
+ resizeDirection = direction
|
|
|
|
|
+ dragStartX = e.clientX
|
|
|
|
|
+ dragStartY = e.clientY
|
|
|
|
|
+ startWidth = selectedLayer.value.width
|
|
|
|
|
+ startHeight = selectedLayer.value.height
|
|
|
|
|
+ layerStartX = selectedLayer.value.x
|
|
|
|
|
+ layerStartY = selectedLayer.value.y
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const startRotate = (e) => {
|
|
|
|
|
+ if (!selectedLayer.value) return
|
|
|
|
|
+ isRotating = true
|
|
|
|
|
+ centerX = selectedLayer.value.x + selectedLayer.value.width / 2
|
|
|
|
|
+ centerY = selectedLayer.value.y + selectedLayer.value.height / 2
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const editingTextLayer = ref(null)
|
|
|
|
|
+ const handleLayerDblClick = (e, layer) => {
|
|
|
|
|
+ if (layer.type === 'text' && !layer.locked) {
|
|
|
|
|
+ editingTextLayer.value = layer
|
|
|
|
|
+ const textarea = document.createElement('textarea')
|
|
|
|
|
+ textarea.value = layer.text
|
|
|
|
|
+ textarea.style.cssText = `
|
|
|
|
|
+ position: absolute; left: ${layer.x}px; top: ${layer.y}px;
|
|
|
|
|
+ font-size: ${layer.fontSize}px; font-family: ${layer.fontFamily};
|
|
|
|
|
+ font-weight: ${layer.fontWeight}; font-style: ${layer.fontStyle};
|
|
|
|
|
+ color: ${layer.color}; background: white; border: 2px solid #409eff;
|
|
|
|
|
+ padding: 4px 8px; min-width: 100px; min-height: 30px;
|
|
|
|
|
+ resize: none; z-index: 1000; outline: none;
|
|
|
|
|
+ `
|
|
|
|
|
+ const canvas = canvasRef.value
|
|
|
|
|
+ if (canvas) {
|
|
|
|
|
+ canvas.appendChild(textarea)
|
|
|
|
|
+ textarea.focus()
|
|
|
|
|
+ textarea.select()
|
|
|
|
|
+ const saveEdit = () => {
|
|
|
|
|
+ layer.text = textarea.value || '双击编辑文字'
|
|
|
|
|
+ canvas.removeChild(textarea)
|
|
|
|
|
+ editingTextLayer.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+ textarea.addEventListener('blur', saveEdit)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleCanvasRatioChange = (ratio) => {
|
|
|
|
|
+ if (ratio === 'custom') return
|
|
|
|
|
+ const config = RATIO_CONFIG[ratio]
|
|
|
|
|
+ if (config) {
|
|
|
|
|
+ canvasWidth.value = config.width
|
|
|
|
|
+ canvasHeight.value = config.height
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleCanvasSizeChange = () => {
|
|
|
|
|
+ if (canvasRatio.value !== 'custom') {
|
|
|
|
|
+ const config = RATIO_CONFIG[canvasRatio.value]
|
|
|
|
|
+ if (config) {
|
|
|
|
|
+ const currentRatio = canvasWidth.value / canvasHeight.value
|
|
|
|
|
+ const expectedRatio = config.ratio
|
|
|
|
|
+ if (Math.abs(currentRatio - expectedRatio) > 0.1) canvasRatio.value = 'custom'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleWidthChange = (newWidth) => {
|
|
|
|
|
+ if (!maintainAspectRatio.value || !selectedLayer.value) return
|
|
|
|
|
+ const layer = selectedLayer.value
|
|
|
|
|
+ const aspectRatio = layer.originalWidth / layer.originalHeight
|
|
|
|
|
+ layer.height = Math.round(newWidth / aspectRatio)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleHeightChange = (newHeight) => {
|
|
|
|
|
+ if (!maintainAspectRatio.value || !selectedLayer.value) return
|
|
|
|
|
+ const layer = selectedLayer.value
|
|
|
|
|
+ const aspectRatio = layer.originalWidth / layer.originalHeight
|
|
|
|
|
+ layer.width = Math.round(newHeight * aspectRatio)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleCanvasWheel = (e) => {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ const delta = e.deltaY > 0 ? -10 : 10
|
|
|
|
|
+ zoomLevel.value = Math.max(10, Math.min(200, zoomLevel.value + delta))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // generateCanvasPreview & layerToApiShape
|
|
|
|
|
+ const generateCanvasPreview = async (layersOverride) => {
|
|
|
|
|
+ const targetLayers = layersOverride ?? layers.value
|
|
|
|
|
+ if (!canvasWidth.value || !canvasHeight.value) return null
|
|
|
|
|
+ const exportCanvas = document.createElement('canvas')
|
|
|
|
|
+ exportCanvas.width = canvasWidth.value
|
|
|
|
|
+ exportCanvas.height = canvasHeight.value
|
|
|
|
|
+ const ctx = exportCanvas.getContext('2d')
|
|
|
|
|
+ if (!ctx) return null
|
|
|
|
|
+
|
|
|
|
|
+ ctx.fillStyle = '#ffffff'
|
|
|
|
|
+ ctx.fillRect(0, 0, exportCanvas.width, exportCanvas.height)
|
|
|
|
|
+
|
|
|
|
|
+ const imageLayers = targetLayers.filter(l => (l.type || 'image') !== 'text' && l.url)
|
|
|
|
|
+ for (const layer of imageLayers) {
|
|
|
|
|
+ const url = layer.url
|
|
|
|
|
+ if (url.startsWith('data:')) {
|
|
|
|
|
+ const img = new Image()
|
|
|
|
|
+ await new Promise(r => { img.onload = () => r(); img.onerror = () => r(); img.src = url })
|
|
|
|
|
+ layer._previewImg = img.complete && img.naturalWidth ? img : null
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ const fetchUrl = getImageLoadUrl(url)
|
|
|
|
|
+ let img = null
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(fetchUrl)
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ const blob = await res.blob()
|
|
|
|
|
+ const objUrl = URL.createObjectURL(blob)
|
|
|
|
|
+ img = new Image()
|
|
|
|
|
+ await new Promise((r, reject) => { img.onload = () => r(); img.onerror = () => reject(); img.src = objUrl })
|
|
|
|
|
+ URL.revokeObjectURL(objUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ img = new Image()
|
|
|
|
|
+ img.crossOrigin = 'anonymous'
|
|
|
|
|
+ await new Promise(r => { img.onload = () => r(); img.onerror = () => r(); img.src = fetchUrl })
|
|
|
|
|
+ if (!img.complete || !img.naturalWidth) img = null
|
|
|
|
|
+ }
|
|
|
|
|
+ layer._previewImg = img
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const layersToDraw = [...targetLayers]
|
|
|
|
|
+ for (const layer of layersToDraw) {
|
|
|
|
|
+ if (!layer.visible) continue
|
|
|
|
|
+ ctx.save()
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.globalAlpha = (layer.opacity ?? 100) / 100
|
|
|
|
|
+ const w = layer.width || 0
|
|
|
|
|
+ const h = layer.height || 0
|
|
|
|
|
+ const cx = (layer.x || 0) + w / 2
|
|
|
|
|
+ const cy = (layer.y || 0) + h / 2
|
|
|
|
|
+ ctx.translate(cx, cy)
|
|
|
|
|
+ ctx.rotate(((layer.rotation || 0) * Math.PI) / 180)
|
|
|
|
|
+
|
|
|
|
|
+ if ((layer.type || 'image') === 'text') {
|
|
|
|
|
+ const fontSize = layer.fontSize || 16
|
|
|
|
|
+ const fontFamily = layer.fontFamily || 'Arial'
|
|
|
|
|
+ const fontWeight = layer.fontWeight || 'normal'
|
|
|
|
|
+ const fontStyle = layer.fontStyle || 'normal'
|
|
|
|
|
+ const paddingTop = 4
|
|
|
|
|
+ const paddingLeft = 8
|
|
|
|
|
+ const paddingRight = 8
|
|
|
|
|
+ const bgColor = layer.backgroundColor
|
|
|
|
|
+ if (bgColor && bgColor !== 'transparent') {
|
|
|
|
|
+ ctx.fillStyle = bgColor
|
|
|
|
|
+ const br = layer.backgroundBorderRadius ?? 0
|
|
|
|
|
+ if (br > 0 && typeof ctx.roundRect === 'function') {
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.roundRect(-w / 2, -h / 2, w, h, br)
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ } else ctx.fillRect(-w / 2, -h / 2, w, h)
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`
|
|
|
|
|
+ ctx.textAlign = (layer.textAlign || 'left') === 'justify' ? 'left' : (layer.textAlign || 'left')
|
|
|
|
|
+ ctx.textBaseline = 'top'
|
|
|
|
|
+ ctx.fillStyle = layer.color || '#000000'
|
|
|
|
|
+ const lineHeightPx = (layer.lineHeight || 1.5) * fontSize
|
|
|
|
|
+ const lines = (layer.text || '').split('\n')
|
|
|
|
|
+ let startX = 0
|
|
|
|
|
+ if (ctx.textAlign === 'left') startX = -w / 2 + paddingLeft
|
|
|
|
|
+ else if (ctx.textAlign === 'right') startX = w / 2 - paddingRight
|
|
|
|
|
+ let y = -h / 2 + paddingTop
|
|
|
|
|
+ for (const line of lines) {
|
|
|
|
|
+ ctx.fillText(line, startX, y)
|
|
|
|
|
+ y += lineHeightPx
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (layer.type === 'shape') {
|
|
|
|
|
+ const fillMode = layer.fillMode || 'solid'
|
|
|
|
|
+ const fill = fillMode === 'none' ? 'transparent' : (layer.fillColor || '#e0e0e0')
|
|
|
|
|
+ const stroke = layer.strokeColor || '#606266'
|
|
|
|
|
+ const sw = layer.strokeWidth ?? 2
|
|
|
|
|
+ const isLine = layer.shapeType === 'line'
|
|
|
|
|
+ const x = -w / 2
|
|
|
|
|
+ const y = -h / 2
|
|
|
|
|
+ ctx.fillStyle = isLine ? stroke : fill
|
|
|
|
|
+ ctx.strokeStyle = stroke
|
|
|
|
|
+ ctx.lineWidth = isLine ? Math.max(1, Math.min(w, h)) : sw
|
|
|
|
|
+ if (layer.shapeType === 'circle') {
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.arc(0, 0, Math.min(w, h) / 2, 0, Math.PI * 2)
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ if (!isLine) ctx.stroke()
|
|
|
|
|
+ } else if (layer.shapeType === 'ellipse') {
|
|
|
|
|
+ ctx.beginPath()
|
|
|
|
|
+ ctx.ellipse(0, 0, w / 2, h / 2, 0, 0, Math.PI * 2)
|
|
|
|
|
+ ctx.fill()
|
|
|
|
|
+ if (!isLine) ctx.stroke()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ctx.fillRect(x, y, w, h)
|
|
|
|
|
+ if (!isLine) ctx.strokeRect(x, y, w, h)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const img = layer._previewImg
|
|
|
|
|
+ if (img) ctx.drawImage(img, -w / 2, -h / 2, w, h)
|
|
|
|
|
+ }
|
|
|
|
|
+ ctx.restore()
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const layer of imageLayers) delete layer._previewImg
|
|
|
|
|
+ try {
|
|
|
|
|
+ return exportCanvas.toDataURL('image/png')
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('画布预览图生成失败:', e)
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const layerToApiShape = (layer, pageIndex) => ({
|
|
|
|
|
+ id: layer.id,
|
|
|
|
|
+ name: layer.name,
|
|
|
|
|
+ type: layer.type || 'image',
|
|
|
|
|
+ url: layer.url ? toStoragePath(layer.url) : '',
|
|
|
|
|
+ text: layer.text,
|
|
|
|
|
+ x: layer.x,
|
|
|
|
|
+ y: layer.y,
|
|
|
|
|
+ width: layer.width,
|
|
|
|
|
+ height: layer.height,
|
|
|
|
|
+ rotation: layer.rotation,
|
|
|
|
|
+ opacity: layer.opacity,
|
|
|
|
|
+ visible: layer.visible,
|
|
|
|
|
+ locked: layer.locked,
|
|
|
|
|
+ material_id: layer.materialId ?? layer.material_id ?? '',
|
|
|
|
|
+ material_type: layer.materialType || (layer.url && String(layer.url).startsWith('data:') ? '其他' : ''),
|
|
|
|
|
+ fontFamily: layer.fontFamily,
|
|
|
|
|
+ fontSize: layer.fontSize,
|
|
|
|
|
+ color: layer.color,
|
|
|
|
|
+ backgroundColor: layer.backgroundColor,
|
|
|
|
|
+ fontWeight: layer.fontWeight,
|
|
|
|
|
+ fontStyle: layer.fontStyle,
|
|
|
|
|
+ textDecoration: layer.textDecoration,
|
|
|
|
|
+ lineHeight: layer.lineHeight,
|
|
|
|
|
+ letterSpacing: layer.letterSpacing,
|
|
|
|
|
+ textAlign: layer.textAlign,
|
|
|
|
|
+ background_border_radius: layer.backgroundBorderRadius ?? 0,
|
|
|
|
|
+ originalWidth: layer.originalWidth,
|
|
|
|
|
+ originalHeight: layer.originalHeight,
|
|
|
|
|
+ shape_type: layer.type === 'shape' ? (layer.shapeType || '') : '',
|
|
|
|
|
+ fill_mode: layer.type === 'shape' ? (layer.fillMode || 'solid') : '',
|
|
|
|
|
+ fill_color: layer.type === 'shape' ? (layer.fillColor || '') : '',
|
|
|
|
|
+ stroke_color: layer.type === 'shape' ? (layer.strokeColor || '') : '',
|
|
|
|
|
+ stroke_width: layer.type === 'shape' ? (layer.strokeWidth ?? 2) : '',
|
|
|
|
|
+ page_index: pageIndex
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ canvasRef,
|
|
|
|
|
+ canvasAreaRef,
|
|
|
|
|
+ layerListRef,
|
|
|
|
|
+ currentTool,
|
|
|
|
|
+ canvasWidth,
|
|
|
|
|
+ canvasHeight,
|
|
|
|
|
+ canvasRatio,
|
|
|
|
|
+ zoomLevel,
|
|
|
|
|
+ maintainAspectRatio,
|
|
|
|
|
+ pages,
|
|
|
|
|
+ currentPageIndex,
|
|
|
|
|
+ layers,
|
|
|
|
|
+ selectedLayerId,
|
|
|
|
|
+ dragState,
|
|
|
|
|
+ layerNameEditingId,
|
|
|
|
|
+ addTextViewMode,
|
|
|
|
|
+ addShapeViewMode,
|
|
|
|
|
+ textLayerCount,
|
|
|
|
|
+ selectedLayer,
|
|
|
|
|
+ reversedLayers,
|
|
|
|
|
+ selectedLayerIndex,
|
|
|
|
|
+ canMoveUp,
|
|
|
|
|
+ canMoveDown,
|
|
|
|
|
+ mapRowToLayer,
|
|
|
|
|
+ syncLayersToCurrentPage,
|
|
|
|
|
+ addPage,
|
|
|
|
|
+ switchPage,
|
|
|
|
|
+ deletePage,
|
|
|
|
|
+ clearDesign,
|
|
|
|
|
+ parseDataToPages,
|
|
|
|
|
+ processUploadedFile,
|
|
|
|
|
+ addShapeLayer,
|
|
|
|
|
+ addTextLayer,
|
|
|
|
|
+ addMaterialToCanvas,
|
|
|
|
|
+ getLayerStyle,
|
|
|
|
|
+ getShapeStyle,
|
|
|
|
|
+ getTextStyle,
|
|
|
|
|
+ getImageLoadUrl,
|
|
|
|
|
+ selectLayer,
|
|
|
|
|
+ toggleLayerVisibility,
|
|
|
|
|
+ toggleLayerLock,
|
|
|
|
|
+ moveLayerUp,
|
|
|
|
|
+ moveLayerDown,
|
|
|
|
|
+ deleteLayer,
|
|
|
|
|
+ deleteLayerById,
|
|
|
|
|
+ openTextAddMore,
|
|
|
|
|
+ backTextAddList,
|
|
|
|
|
+ openShapeAddMore,
|
|
|
|
|
+ backShapeAddList,
|
|
|
|
|
+ toStoragePath,
|
|
|
|
|
+ RATIO_CONFIG,
|
|
|
|
|
+ setLayerNameInputRef,
|
|
|
|
|
+ setLayerNameEditingId,
|
|
|
|
|
+ openLayerNameEdit,
|
|
|
|
|
+ handleDragStart,
|
|
|
|
|
+ handleDragOver,
|
|
|
|
|
+ handleDrop,
|
|
|
|
|
+ handleDragEnd,
|
|
|
|
|
+ handleDragEnter,
|
|
|
|
|
+ handleDragLeave,
|
|
|
|
|
+ handleLayerMouseDown,
|
|
|
|
|
+ handleCanvasMouseDown,
|
|
|
|
|
+ handleCanvasMouseMove,
|
|
|
|
|
+ handleCanvasMouseUp,
|
|
|
|
|
+ handleCanvasWheel,
|
|
|
|
|
+ startResize,
|
|
|
|
|
+ startRotate,
|
|
|
|
|
+ handleLayerDblClick,
|
|
|
|
|
+ handleCanvasRatioChange,
|
|
|
|
|
+ handleCanvasSizeChange,
|
|
|
|
|
+ handleWidthChange,
|
|
|
|
|
+ handleHeightChange,
|
|
|
|
|
+ generateCanvasPreview,
|
|
|
|
|
+ layerToApiShape
|
|
|
|
|
+ }
|
|
|
|
|
+}
|