Veronique 1 anno fa
parent
commit
18827ece50

+ 101 - 99
src/configs/canvas.ts

@@ -5,11 +5,11 @@ export const WorkSpaceMaskType = 'WorkSpaceMaskType'
 export const WorkSpaceLineType = 'WorkSpaceLineType'
 
 export const WorkSpaceCommonType = [
-  WorkSpaceDrawType, WorkSpaceClipType, WorkSpaceSafeType, WorkSpaceMaskType, WorkSpaceLineType
+    WorkSpaceDrawType, WorkSpaceClipType, WorkSpaceSafeType, WorkSpaceMaskType, WorkSpaceLineType
 ]
 
 export const WorkSpaceThumbType = [
-  WorkSpaceClipType, WorkSpaceSafeType, WorkSpaceMaskType, WorkSpaceLineType
+    WorkSpaceClipType, WorkSpaceSafeType, WorkSpaceMaskType, WorkSpaceLineType
 ]
 
 // 分割服
@@ -41,107 +41,109 @@ export const WorkSpaceSafeColor = 'yellow'
 
 // 画布公共参数
 export const WorkSpaceCommonOption = {
-  selectable: false,
-  transparentCorners: false,
-  evented: false,
-  excludeFromExport: true,
-  hasControls: false,
-  hasBorders: false,
-  perPixelTargetFind: false,
-  // absolutePositioned: true,
-  lockMovementX: true,
-  lockMovementY: true,
-  lockRotation: true,
-  lockScalingX: true,
-  lockScalingY: true,
-  lockUniScaling: true,
-  hoverCursor: 'default',
-  name: WorkSpaceName,
+    selectable: false,
+    transparentCorners: false,
+    evented: false,
+    excludeFromExport: true,
+    hasControls: false,
+    hasBorders: false,
+    perPixelTargetFind: false,
+    // absolutePositioned: true,
+    lockMovementX: true,
+    lockMovementY: true,
+    lockRotation: true,
+    lockScalingX: true,
+    lockScalingY: true,
+    lockUniScaling: true,
+    hoverCursor: 'default',
+    name: WorkSpaceName,
 }
 
 export const propertiesToInclude = [
-  'id', 
-  'name', 
-  'layer',
-  'isShow',
-  'editable',
-  'color', 
-  'axis',
-  'mask',
-  'padding',
-  'cropKey', 
-  'cropPath', 
-  'cropSize', 
-  'fill',
-  'selectable',
-  'evented',
-  'fillType', 
-  'fillURL', 
-  'fillRepeat', 
-  'lockMovementX', 
-  'lockMovementY', 
-  'objectCaching',
-  'transparentCorners',
-  'codeOption',
-  'codeContent',
-  'background',
-  'hasBorders',
-  'originSrc',
-  'radius',
-  'curvature',
-  'effect',
-  'reverse',
-  'startStyle',
-  'endStyle',
-  'effects',
-  'mask',
-  'originSrc',
-  'originWidth',
-  'originHeight',
-  'globalCompositeOperation',
+    'id',
+    'name',
+    'layer',
+    'isShow',
+    'editable',
+    'color',
+    'axis',
+    'mask',
+    'padding',
+    'cropKey',
+    'cropPath',
+    'cropSize',
+    'fill',
+    'selectable',
+    'evented',
+    'fillType',
+    'fillURL',
+    'fillRepeat',
+    'lockMovementX',
+    'lockMovementY',
+    'objectCaching',
+    'transparentCorners',
+    'codeOption',
+    'codeContent',
+    'background',
+    'hasBorders',
+    'originSrc',
+    'radius',
+    'curvature',
+    'effect',
+    'reverse',
+    'startStyle',
+    'endStyle',
+    'effects',
+    'mask',
+    'originSrc',
+    'originWidth',
+    'originHeight',
+    'globalCompositeOperation',
+    'permissionsConfig',
+    'itemName'
 ]
 
 export const WorkSpaceDrawData = {
-  "rx": 0,
-  "ry": 0,
-  "id": "WorkSpaceDrawType",
-  "name": "rect",
-  "fill": "#fff",
-  "selectable": false,
-  "evented": false,
-  "lockMovementX": false,
-  "lockMovementY": false,
-  "objectCaching": true,
-  "transparentCorners": false,
-  "hasBorders": true,
-  "type": "Rect",
-  "version": "6.0.0-beta9",
-  "originX": "left",
-  "originY": "top",
-  "left": 0,
-  "top": 0,
-  "width": 1070.5512,
-  "height": 645.3543,
-  "stroke": "rgba(255,255,255,1)",
-  "strokeWidth": 1,
-  "strokeDashArray": null,
-  "strokeLineCap": "butt",
-  "strokeDashOffset": 0,
-  "strokeLineJoin": "miter",
-  "strokeUniform": false,
-  "strokeMiterLimit": 4,
-  "scaleX": 1,
-  "scaleY": 1,
-  "angle": 0,
-  "flipX": false,
-  "flipY": false,
-  "opacity": 1,
-  "shadow": null,
-  "visible": true,
-  "backgroundColor": "",
-  "fillRule": "nonzero",
-  "paintFirst": "fill",
-  "globalCompositeOperation": "source-over",
-  "skewX": 0,
-  "skewY": 0
+    "rx": 0,
+    "ry": 0,
+    "id": "WorkSpaceDrawType",
+    "name": "rect",
+    "fill": "#fff",
+    "selectable": false,
+    "evented": false,
+    "lockMovementX": false,
+    "lockMovementY": false,
+    "objectCaching": true,
+    "transparentCorners": false,
+    "hasBorders": true,
+    "type": "Rect",
+    "version": "6.0.0-beta9",
+    "originX": "left",
+    "originY": "top",
+    "left": 0,
+    "top": 0,
+    "width": 1070.5512,
+    "height": 645.3543,
+    "stroke": "rgba(255,255,255,1)",
+    "strokeWidth": 1,
+    "strokeDashArray": null,
+    "strokeLineCap": "butt",
+    "strokeDashOffset": 0,
+    "strokeLineJoin": "miter",
+    "strokeUniform": false,
+    "strokeMiterLimit": 4,
+    "scaleX": 1,
+    "scaleY": 1,
+    "angle": 0,
+    "flipX": false,
+    "flipY": false,
+    "opacity": 1,
+    "shadow": null,
+    "visible": true,
+    "backgroundColor": "",
+    "fillRule": "nonzero",
+    "paintFirst": "fill",
+    "globalCompositeOperation": "source-over",
+    "skewX": 0,
+    "skewY": 0
 }

+ 1 - 0
src/hooks/useCanvasExport.ts

@@ -145,6 +145,7 @@ export default () => {
 
     const getJSONData = () => {
         const [canvas] = useCanvas()
+        console.log(canvas)
         const serializer = canvas.toObject(propertiesToInclude)
         serializer.workSpace = currentTemplate.value.workSpace
         serializer.zoom = currentTemplate.value.zoom

+ 70 - 68
src/mocks/templates.ts

@@ -1,73 +1,75 @@
 // @ts-nocheck
-import { Template } from "@/types/canvas"
+import {Template} from "@/types/canvas"
 
 export const Templates: Template[] = [
-  {
-    "version": "6.4.3",
-    "id": "APxCmQX_hz",
-    "background": "rgba(255,255,255,0)",
-    "objects": [
-      {
-        "rx": 0,
-        "ry": 0,
-        "id": "WorkSpaceDrawType",
-        "name": "rect",
-        "color": "#ffffff",
-        "padding": 0,
-        "fill": "#ffffff",
-        "selectable": false,
-        "evented": false,
-        "fillType": 0,
-        "lockMovementX": false,
-        "lockMovementY": false,
-        "objectCaching": true,
-        "transparentCorners": false,
-        "hasBorders": true,
-        "globalCompositeOperation": "source-over",
-        "type": "Rect",
+    {
         "version": "6.4.3",
-        "originX": "left",
-        "originY": "top",
-        "left": 0,
-        "top": 0,
-        "width": 5905.5118,
-        "height": 9448.8189,
-        "stroke": "",
-        "strokeWidth": 0,
-        "strokeDashArray": null,
-        "strokeLineCap": "butt",
-        "strokeDashOffset": 0,
-        "strokeLineJoin": "miter",
-        "strokeUniform": false,
-        "strokeMiterLimit": 4,
-        "scaleX": 1,
-        "scaleY": 1,
-        "angle": 0,
-        "flipX": false,
-        "flipY": false,
-        "opacity": 1,
-        "shadow": null,
-        "visible": true,
-        "backgroundColor": "rgba(0,0,0,0)",
-        "fillRule": "nonzero",
-        "paintFirst": "fill",
-        "skewX": 0,
-        "skewY": 0
-      }
-    ],
-    "workSpace": {
-      "fillType": 0,
-      "left": 0,
-      "top": 0,
-      "angle": 0,
-      "scaleX": 1,
-      "scaleY": 1,
-      "color": "#ffffff",
-      "fill": "#ffffff",
-      "backgroundColor": "rgba(0,0,0,0)"
-    },
-    "zoom": 0.044084670517326584,
-    "width": 260.34254242515703,
-    "height": 416.54806788025115
-  }
+        "id": "APxCmQX_hz",
+        "background": "rgba(255,255,255,0)",
+        "objects": [
+            {
+                "rx": 0,
+                "ry": 0,
+                "id": "WorkSpaceDrawType",
+                "name": "rect",
+                "color": "#ffffff",
+                "padding": 0,
+                "fill": "#ffffff",
+                "selectable": false,
+                "evented": false,
+                "fillType": 0,
+                "lockMovementX": false,
+                "lockMovementY": false,
+                "objectCaching": true,
+                "transparentCorners": false,
+                "hasBorders": true,
+                "globalCompositeOperation": "source-over",
+                "type": "Rect",
+                "version": "6.4.3",
+                "originX": "left",
+                "originY": "top",
+                "left": 0,
+                "top": 0,
+                "width": 5905.5118,
+                "height": 9448.8189,
+                "stroke": "",
+                "strokeWidth": 0,
+                "strokeDashArray": null,
+                "strokeLineCap": "butt",
+                "strokeDashOffset": 0,
+                "strokeLineJoin": "miter",
+                "strokeUniform": false,
+                "strokeMiterLimit": 4,
+                "scaleX": 1,
+                "scaleY": 1,
+                "angle": 0,
+                "flipX": false,
+                "flipY": false,
+                "opacity": 1,
+                "shadow": null,
+                "visible": true,
+                "backgroundColor": "rgba(0,0,0,0)",
+                "fillRule": "nonzero",
+                "paintFirst": "fill",
+                "skewX": 0,
+                "skewY": 0,
+                "permissionsConfig": [],
+                "itemName": "背景"
+            }
+        ],
+        "workSpace": {
+            "fillType": 0,
+            "left": 0,
+            "top": 0,
+            "angle": 0,
+            "scaleX": 1,
+            "scaleY": 1,
+            "color": "#ffffff",
+            "fill": "#ffffff",
+            "backgroundColor": "rgba(0,0,0,0)"
+        },
+        "zoom": 0.044084670517326584,
+        "width": 260.34254242515703,
+        "height": 416.54806788025115
+    }
 ]

+ 301 - 301
src/store/modules/template.ts

@@ -1,12 +1,12 @@
-import { defineStore } from 'pinia'
-import { Templates } from '@/mocks/templates'
-import { Template, CanvasElement, ImageElement, GroupElement, RectElement } from '@/types/canvas'
-import { FabricObject, SerializedImageProps, FabricImage, Group, StaticCanvas } from 'fabric'
-import { WorkSpaceDrawType, propertiesToInclude } from '@/configs/canvas'
-import { useMainStore } from './main'
-import { ElementNames } from '@/types/elements'
-import { ElLoading } from 'element-plus'
-import { Snapshot, SnapshotType } from '@/types/history'
+import {defineStore} from 'pinia'
+import {Templates} from '@/mocks/templates'
+import {Template, CanvasElement, ImageElement, GroupElement, RectElement} from '@/types/canvas'
+import {FabricObject, SerializedImageProps, FabricImage, Group, StaticCanvas} from 'fabric'
+import {WorkSpaceDrawType, propertiesToInclude} from '@/configs/canvas'
+import {useMainStore} from './main'
+import {ElementNames} from '@/types/elements'
+import {ElLoading} from 'element-plus'
+import {Snapshot, SnapshotType} from '@/types/history'
 import useCanvasScale from '@/hooks/useCanvasScale'
 import useCanvas from '@/views/Canvas/useCanvas'
 import useHistorySnapshot from '@/hooks/useHistorySnapshot'
@@ -15,306 +15,306 @@ import usePixi from '@/views/Canvas/usePixi'
 
 
 interface UpdateElementData {
-  id: string | string[]
-  left?: number
-  top?: number
-  props: Partial<CanvasElement>
+    id: string | string[]
+    left?: number
+    top?: number
+    props: Partial<CanvasElement>
 }
 
 export interface TemplatesState {
-  templateId: string
-  templateName: string
-  templates: Template[]
-  templateIndex: number
-  templateCanvas: Map<string, StaticCanvas>
+    templateId: string
+    templateName: string
+    templates: Template[]
+    templateIndex: number
+    templateCanvas: Map<string, StaticCanvas>
 }
 
 export const useTemplatesStore = defineStore('Templates', {
-  state: (): TemplatesState => ({
-    // theme: theme, // 主题样式
-    templateId: '',
-    templateName: '空白模板',
-    templates: Templates, // 页面页面数据
-    templateIndex: 0, // 当前页面索引
-    templateCanvas: new Map()
-    // fixedRatio: false, // 固定比例
-    // slideUnit: 'mm', // 尺寸单位
-    // slideName: '', // 模板名称
-    // slideId: '', // 模板id
-  }),
-
-  getters: {
-    currentTemplate(state) {
-      return state.templates[state.templateIndex] as Template
-    },
-
-    currentTemplateWidth(state) {
-      const currentTemplate = state.templates[state.templateIndex]
-      return currentTemplate.width / currentTemplate.zoom
-    },
-
-    currentTemplateHeight(state) {
-      const currentTemplate = state.templates[state.templateIndex]
-      return currentTemplate.height / currentTemplate.zoom
-    },
-
-    currentTemplateElement(state) {
-      const currentTemplate = state.templates[state.templateIndex]
-      const [ canvas ] = useCanvas()
-      const activeObject = canvas.getActiveObject() as CanvasElement
-      return currentTemplate.objects.filter(ele => ele.id === activeObject.id)[0]
-    }
-  },
-
-  actions: {
-    async renderTemplate() {
-      const [ canvas ] = useCanvas()
-      const { initCommon } = useCommon()
-      const { setCanvasSize } = useCanvasScale()
-      await canvas.loadFromJSON(this.currentTemplate)
-      this.setObjectFilter(this.currentTemplate.objects as CanvasElement[])
-      setCanvasSize()
-      initCommon()
-    },
-
-    async renderElement() {
-      const [ canvas ] = useCanvas()
-      const { initCommon } = useCommon()
-      const { setCanvasSize } = useCanvasScale()
-      const mainStore = useMainStore()
-      canvas.discardActiveObject()
-      mainStore.setCanvasObject(undefined)
-      await canvas.loadFromJSON(this.currentTemplate)
-      setCanvasSize()
-      initCommon()
-    },
-
-    modifedElement(target: FabricObject, options: Record<string, any>,) {
-      const [ canvas ] = useCanvas()
-      const { addHistorySnapshot } = useHistorySnapshot()
-      const index = canvas._objects.findIndex(item => item.id === target.id)
-      const data: Snapshot = {
-        type: SnapshotType.MODIFY,
-        index,
-        target: target.toObject(propertiesToInclude),
-        tid: this.templateId
-      }
-      addHistorySnapshot(data)
-      target.set({...options});
-      if (options.filters) {
-        (target as FabricImage).applyFilters();
-      }
-      canvas.setActiveObject(target)
-      canvas.renderAll()
-    },
-
-    addElement(target: FabricObject) {
-      const [ canvas ] = useCanvas()
-      const { addHistorySnapshot } = useHistorySnapshot()
-      const data: Snapshot = {
-        type: SnapshotType.ADD,
-        index: canvas._objects.indexOf(target),
-        target: target.toObject(propertiesToInclude),
-        tid: this.templateId
-      }
-      addHistorySnapshot(data)
-    },
-
-    groupElement(target: FabricObject, objects: FabricObject[]) {
-      const [ canvas ] = useCanvas()
-      const { addHistorySnapshot } = useHistorySnapshot()
-      const data: Snapshot = {
-        type: SnapshotType.GROUP,
-        index: canvas._objects.indexOf(target),
-        target: target.toObject(propertiesToInclude),
-        objects: objects.map(item => item.toObject(propertiesToInclude)),
-        tid: this.templateId
-      }
-      addHistorySnapshot(data)
-    },
-
-    ungroupElement() {
-
-    },
-
-    deleteElement(target: FabricObject) {
-      const [ canvas ] = useCanvas()
-      const { addHistorySnapshot } = useHistorySnapshot()
-      const data: Snapshot = {
-        type: SnapshotType.DELETE,
-        index: canvas._objects.indexOf(target),
-        target: target.toObject(propertiesToInclude),
-        tid: this.templateId
-      }
-      canvas.remove(target)
-      canvas.renderAll()
-      addHistorySnapshot(data)
-    },
-
-    setClip(clip: number) {
-      const { addHistorySnapshot } = useHistorySnapshot()
-      this.templates.forEach(template => {
-        template.clip = clip
-      })
-      // addHistorySnapshot()
-    },
-
-    setSize(width: number, height: number, zoom: number) {
-      const { initCommon } = useCommon()
-      const { addHistorySnapshot } = useHistorySnapshot()
-      this.templates.forEach(template => {
-        template.width = width
-        template.height = height
-        template.zoom = zoom
-        template.objects.filter(item => item.id === WorkSpaceDrawType).map(ele => {
-          ele.width = width / zoom
-          ele.height = height / zoom
-        })
-      })
-      initCommon()
-      // addHistorySnapshot()
-    },
-
-    setObjectFilter(objects: CanvasElement[]) {
-      objects.forEach(ele => {
-        if (ele.type.toLowerCase() === ElementNames.IMAGE) {
-          this.setImageFilter(ele as ImageElement)
-          // this.setImageMask(ele as ImageElement)
-        }
-        if (ele.type.toLowerCase() === ElementNames.GROUP) {
-          this.setObjectFilter(((ele as GroupElement).objects) as CanvasElement[])
+    state: (): TemplatesState => ({
+        // theme: theme, // 主题样式
+        templateId: '',
+        templateName: '空白模板',
+        templates: Templates, // 页面页面数据
+        templateIndex: 0, // 当前页面索引
+        templateCanvas: new Map()
+        // fixedRatio: false, // 固定比例
+        // slideUnit: 'mm', // 尺寸单位
+        // slideName: '', // 模板名称
+        // slideId: '', // 模板id
+    }),
+
+    getters: {
+        currentTemplate(state) {
+            return state.templates[state.templateIndex] as Template
+        },
+
+        currentTemplateWidth(state) {
+            const currentTemplate = state.templates[state.templateIndex]
+            return currentTemplate.width / currentTemplate.zoom
+        },
+
+        currentTemplateHeight(state) {
+            const currentTemplate = state.templates[state.templateIndex]
+            return currentTemplate.height / currentTemplate.zoom
+        },
+
+        currentTemplateElement(state) {
+            const currentTemplate = state.templates[state.templateIndex]
+            const [canvas] = useCanvas()
+            const activeObject = canvas.getActiveObject() as CanvasElement
+            return currentTemplate.objects.filter(ele => ele.id === activeObject.id)[0]
         }
-      })
     },
 
-    setImageFilter(image: ImageElement) {
-      if (!image.pixiFilters) return
-      const [ pixi ] = usePixi()
-      pixi.postMessage({
-        id: image.id,
-        type: "filter", 
-        src: image.src, 
-        pixiFilters: JSON.stringify(image.pixiFilters), 
-        width: image.width, 
-        height: image.height
-      });
-    },
+    actions: {
+        async renderTemplate() {
+            const [canvas] = useCanvas()
+            const {initCommon} = useCommon()
+            const {setCanvasSize} = useCanvasScale()
+            await canvas.loadFromJSON(this.currentTemplate)
+            this.setObjectFilter(this.currentTemplate.objects as CanvasElement[])
+            setCanvasSize()
+            initCommon()
+        },
+
+        async renderElement() {
+            const [canvas] = useCanvas()
+            const {initCommon} = useCommon()
+            const {setCanvasSize} = useCanvasScale()
+            const mainStore = useMainStore()
+            canvas.discardActiveObject()
+            mainStore.setCanvasObject(undefined)
+            await canvas.loadFromJSON(this.currentTemplate)
+            setCanvasSize()
+            initCommon()
+        },
+
+        modifedElement(target: FabricObject, options: Record<string, any>,) {
+            const [canvas] = useCanvas()
+            const {addHistorySnapshot} = useHistorySnapshot()
+            const index = canvas._objects.findIndex(item => item.id === target.id)
+            const data: Snapshot = {
+                type: SnapshotType.MODIFY,
+                index,
+                target: target.toObject(propertiesToInclude),
+                tid: this.templateId
+            }
+            addHistorySnapshot(data)
+            target.set({...options});
+            if (options.filters) {
+                (target as FabricImage).applyFilters();
+            }
+            canvas.setActiveObject(target)
+            canvas.renderAll()
+        },
+
+        addElement(target: FabricObject) {
+            const [canvas] = useCanvas()
+            const {addHistorySnapshot} = useHistorySnapshot()
+            const data: Snapshot = {
+                type: SnapshotType.ADD,
+                index: canvas._objects.indexOf(target),
+                target: target.toObject(propertiesToInclude),
+                tid: this.templateId
+            }
+            addHistorySnapshot(data)
+        },
+
+        groupElement(target: FabricObject, objects: FabricObject[]) {
+            const [canvas] = useCanvas()
+            const {addHistorySnapshot} = useHistorySnapshot()
+            const data: Snapshot = {
+                type: SnapshotType.GROUP,
+                index: canvas._objects.indexOf(target),
+                target: target.toObject(propertiesToInclude),
+                objects: objects.map(item => item.toObject(propertiesToInclude)),
+                tid: this.templateId
+            }
+            addHistorySnapshot(data)
+        },
+
+        ungroupElement() {
+
+        },
+
+        deleteElement(target: FabricObject) {
+            const [canvas] = useCanvas()
+            const {addHistorySnapshot} = useHistorySnapshot()
+            const data: Snapshot = {
+                type: SnapshotType.DELETE,
+                index: canvas._objects.indexOf(target),
+                target: target.toObject(propertiesToInclude),
+                tid: this.templateId
+            }
+            canvas.remove(target)
+            canvas.renderAll()
+            addHistorySnapshot(data)
+        },
+
+        setClip(clip: number) {
+            const {addHistorySnapshot} = useHistorySnapshot()
+            this.templates.forEach(template => {
+                template.clip = clip
+            })
+            // addHistorySnapshot()
+        },
+
+        setSize(width: number, height: number, zoom: number) {
+            const {initCommon} = useCommon()
+            const {addHistorySnapshot} = useHistorySnapshot()
+            this.templates.forEach(template => {
+                template.width = width
+                template.height = height
+                template.zoom = zoom
+                template.objects.filter(item => item.id === WorkSpaceDrawType).map(ele => {
+                    ele.width = width / zoom
+                    ele.height = height / zoom
+                })
+            })
+            initCommon()
+            // addHistorySnapshot()
+        },
+
+        setObjectFilter(objects: CanvasElement[]) {
+            objects.forEach(ele => {
+                if (ele.type.toLowerCase() === ElementNames.IMAGE) {
+                    this.setImageFilter(ele as ImageElement)
+                    // this.setImageMask(ele as ImageElement)
+                }
+                if (ele.type.toLowerCase() === ElementNames.GROUP) {
+                    this.setObjectFilter(((ele as GroupElement).objects) as CanvasElement[])
+                }
+            })
+        },
+
+        setImageFilter(image: ImageElement) {
+            if (!image.pixiFilters) return
+            const [pixi] = usePixi()
+            pixi.postMessage({
+                id: image.id,
+                type: "filter",
+                src: image.src,
+                pixiFilters: JSON.stringify(image.pixiFilters),
+                width: image.width,
+                height: image.height
+            });
+        },
+
+        setImageMask(image: ImageElement) {
+            if (!image.mask) return
+            const [pixi] = usePixi()
+            pixi.postMessage({
+                id: image.id,
+                type: "mask",
+                src: image.src,
+                mask: JSON.stringify(image.mask),
+                width: image.width,
+                height: image.height
+            });
+        },
+
+        async changeTemplate(template: Template | Template[]) {
+            const {setCanvasTransform} = useCanvasScale()
+            const templates = Array.isArray(template) ? template : [template]
+            this.templates = templates
+            this.templateIndex = 0
+            await this.renderTemplate()
+            setCanvasTransform()
+        },
+
+        setTemplates(templates: Template[]) {
+            this.templates = templates
+        },
+
+        setTemplateId(templateId: string) {
+            this.templateId = templateId
+        },
+
+        setTemplateName(templateName: string) {
+            this.templateName = templateName
+        },
+
+        setTemplateIndex(index: number) {
+            this.templateIndex = index
+        },
+
+        async addTemplate(template: Template | Template[]) {
+            const templates = Array.isArray(template) ? template : [template]
+            const addIndex = this.templateIndex + 1
+            this.templates.splice(addIndex, 0, ...templates)
+            this.templateIndex = addIndex
+            await this.renderTemplate()
+        },
+
+        updateTemplate(props: Partial<Template>) {
+            const {addHistorySnapshot} = useHistorySnapshot()
+            const templateIndex = this.templateIndex
+            this.templates[templateIndex] = {...this.templates[templateIndex], ...props}
+            // addHistorySnapshot()
+        },
+
+        deleteTemplate(templateId: string | string[]) {
+            const {addHistorySnapshot} = useHistorySnapshot()
+            const templateIds = Array.isArray(templateId) ? templateId : [templateId]
+
+            const deleteTemplatesIndex = []
+            for (let i = 0; i < templateIds.length; i++) {
+                const index = this.templates.findIndex(item => item.id === templateIds[i])
+                deleteTemplatesIndex.push(index)
+            }
+            let newIndex = Math.min(...deleteTemplatesIndex)
+
+            const maxIndex = this.templates.length - templateIds.length - 1
+            if (newIndex > maxIndex) newIndex = maxIndex
+
+            this.templateIndex = newIndex
+            this.templates = this.templates.filter(item => !templateIds.includes(item.id))
+            // addHistorySnapshot()
+        },
+
+        clearTemplate() {
+            const objects = this.templates[this.templateIndex].objects.filter(item => item.id === WorkSpaceDrawType)
+            this.templates[this.templateIndex].objects = objects
+            this.renderTemplate()
+        },
+
+        updateWorkSpace(props: Partial<Template>) {
+            const templateIndex = this.templateIndex
+            this.templates[templateIndex] = {...this.templates[templateIndex], ...props}
+        },
+
+        updateElement(data: UpdateElementData) {
+            const {addHistorySnapshot} = useHistorySnapshot()
+            const {id, props} = data
+            const elementIds = typeof id === 'string' ? [id] : id
+            if (!elementIds) return
+            const template = this.templates[this.templateIndex]
+            const elements = template.objects.map(el => elementIds.includes(el.id) ? {...el, ...props} : el)
+            this.templates[this.templateIndex].objects = elements as FabricObject[]
+            // addHistorySnapshot()
+        },
+
+        // addElement(element: FabricObject | FabricObject[]) {
+        //   const { addHistorySnapshot } = useHistorySnapshot()
+        //   const elements = Array.isArray(element) ? element : [element]
+        //   const currentTemplateElements = this.templates[this.templateIndex].objects
+        //   const newElements = [...currentTemplateElements, ...elements]
+        //   this.templates[this.templateIndex].objects = newElements as FabricObject[]
+        //   addHistorySnapshot()
+        // },
+
+        // deleteElement(elementId: string | string[]) {
+        //   const { addHistorySnapshot } = useHistorySnapshot()
+        //   const elementIds = Array.isArray(elementId) ? elementId : [elementId]
+        //   const currentTemplateElements = this.templates[this.templateIndex].objects
+        //   const newElements = currentTemplateElements.filter(item => !elementIds.includes(item.id))
+        //   this.templates[this.templateIndex].objects = newElements
+        //   addHistorySnapshot()
+        // },
+
+        setBackgroundImage(props: SerializedImageProps) {
+            this.currentTemplate.backgroundImage = props
+        },
 
-    setImageMask(image: ImageElement) {
-      if (!image.mask) return
-      const [ pixi ] = usePixi()
-      pixi.postMessage({
-        id: image.id,
-        type: "mask", 
-        src: image.src,
-        mask: JSON.stringify(image.mask), 
-        width: image.width, 
-        height: image.height
-      });
-    },
-
-    async changeTemplate(template: Template | Template[]) {
-      const { setCanvasTransform } = useCanvasScale()
-      const templates = Array.isArray(template) ? template : [template]
-      this.templates = templates
-      this.templateIndex = 0
-      await this.renderTemplate()
-      setCanvasTransform()
-    },
-
-    setTemplates(templates: Template[]) {
-      this.templates = templates
-    },
-
-    setTemplateId(templateId: string) {
-      this.templateId = templateId
-    },
-
-    setTemplateName(templateName:string){
-      this.templateName = templateName
-    },
-
-    setTemplateIndex(index: number) {
-      this.templateIndex = index
-    },
-
-    async addTemplate(template: Template | Template[]) {
-      const templates = Array.isArray(template) ? template : [template]
-      const addIndex = this.templateIndex + 1
-      this.templates.splice(addIndex, 0, ...templates)
-      this.templateIndex = addIndex
-      await this.renderTemplate()
-    },
-
-    updateTemplate(props: Partial<Template>) {
-      const { addHistorySnapshot } = useHistorySnapshot()
-      const templateIndex = this.templateIndex
-      this.templates[templateIndex] = { ...this.templates[templateIndex], ...props }
-      // addHistorySnapshot()
-    },
-
-    deleteTemplate(templateId: string | string[]) {
-      const { addHistorySnapshot } = useHistorySnapshot()
-      const templateIds = Array.isArray(templateId) ? templateId : [templateId]
-  
-      const deleteTemplatesIndex = []
-      for (let i = 0; i < templateIds.length; i++) {
-        const index = this.templates.findIndex(item => item.id === templateIds[i])
-        deleteTemplatesIndex.push(index)
-      }
-      let newIndex = Math.min(...deleteTemplatesIndex)
-  
-      const maxIndex = this.templates.length - templateIds.length - 1
-      if (newIndex > maxIndex) newIndex = maxIndex
-  
-      this.templateIndex = newIndex
-      this.templates = this.templates.filter(item => !templateIds.includes(item.id))
-      // addHistorySnapshot()
-    },
-
-    clearTemplate() {
-      const objects = this.templates[this.templateIndex].objects.filter(item => item.id === WorkSpaceDrawType)
-      this.templates[this.templateIndex].objects = objects
-      this.renderTemplate()
-    },
-
-    updateWorkSpace(props: Partial<Template>) {
-      const templateIndex = this.templateIndex
-      this.templates[templateIndex] = { ...this.templates[templateIndex], ...props }
-    },
-
-    updateElement(data: UpdateElementData) {
-      const { addHistorySnapshot } = useHistorySnapshot()
-      const { id, props } = data
-      const elementIds = typeof id === 'string' ? [id] : id
-      if (!elementIds) return
-      const template = this.templates[this.templateIndex]
-      const elements = template.objects.map(el => elementIds.includes(el.id) ? { ...el, ...props }: el)
-      this.templates[this.templateIndex].objects = elements as FabricObject[]
-      // addHistorySnapshot()
-    },
-
-    // addElement(element: FabricObject | FabricObject[]) {
-    //   const { addHistorySnapshot } = useHistorySnapshot()
-    //   const elements = Array.isArray(element) ? element : [element]
-    //   const currentTemplateElements = this.templates[this.templateIndex].objects
-    //   const newElements = [...currentTemplateElements, ...elements]
-    //   this.templates[this.templateIndex].objects = newElements as FabricObject[]
-    //   addHistorySnapshot()
-    // },
-
-    // deleteElement(elementId: string | string[]) {
-    //   const { addHistorySnapshot } = useHistorySnapshot()
-    //   const elementIds = Array.isArray(elementId) ? elementId : [elementId]
-    //   const currentTemplateElements = this.templates[this.templateIndex].objects
-    //   const newElements = currentTemplateElements.filter(item => !elementIds.includes(item.id))
-    //   this.templates[this.templateIndex].objects = newElements
-    //   addHistorySnapshot()
-    // },
-
-    setBackgroundImage(props: SerializedImageProps) {
-      this.currentTemplate.backgroundImage = props
-    },
-
-  }
+    }
 })

+ 4 - 0
src/types/canvas.ts

@@ -21,6 +21,8 @@ export interface CommenElement {
   fillType: number
   background?: BackgroundElement
   isRotate?: boolean
+  permissionsConfig?: []
+  itemName?: string
 }
 export interface GradientElement extends Gradient<'linear' | 'radial'> {
   gradientName: string
@@ -61,6 +63,7 @@ export interface WorkSpaceElement {
   gradientColor?: ColorStop[]
   gradientRotate?: number
   backgroundColor?: string
+  permissionsConfig?: []
 }
 
 export interface BackgroundElement {
@@ -80,6 +83,7 @@ export interface BackgroundElement {
   gradientOffsetX?: number
   gradientOffsetY?: number
   backgroundColor?: string
+  permissionsConfig?: []
 }
 
 export interface TextboxElement extends Textbox, CommenElement {

+ 1 - 1
src/types/common.ts

@@ -1,5 +1,5 @@
 export type ExportTypes = 'image' | 'pdf' | 'psd' | 'json' | 'svg' | ''
-export type PoolType = 'editor' | 'template' | 'material' | 'text' | 'image' | 'illustration' | 'layer' | 'code' | 'toolkit' | 'help' | 'chatgpt'
+export type PoolType = 'editor' | 'template' | 'material' | 'text' | 'image' | 'illustration' | 'layer' | 'code' | 'toolkit' | 'help' | 'chatgpt' | 'frontEditor'
 export type SystemFont = {
   label: string
   value: string

+ 4 - 6
src/types/components.d.ts

@@ -14,7 +14,6 @@ declare module 'vue' {
     Contextmenu: typeof import('./../components/Contextmenu/index.vue')['default']
     EditableInput: typeof import('./../components/ColorPicker/EditableInput.vue')['default']
     ElAffix: typeof import('element-plus/es')['ElAffix']
-    ElAside: typeof import('element-plus/es')['ElAside']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
     ElCarousel: typeof import('element-plus/es')['ElCarousel']
@@ -23,8 +22,9 @@ declare module 'vue' {
     ElCheckboxButton: typeof import('element-plus/es')['ElCheckboxButton']
     ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
+    ElCollapse: typeof import('element-plus/es')['ElCollapse']
+    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
-    ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
@@ -33,14 +33,11 @@ declare module 'vue' {
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
-    ElHeader: typeof import('element-plus/es')['ElHeader']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
-    ElMain: typeof import('element-plus/es')['ElMain']
-    ElMenu: typeof import('element-plus/es')['ElMenu']
-    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElLabel: typeof import('element-plus/es')['ElLabel']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
     ElPopover: typeof import('element-plus/es')['ElPopover']
@@ -48,6 +45,7 @@ declare module 'vue' {
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
+    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSlider: typeof import('element-plus/es')['ElSlider']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']

+ 79 - 30
src/views/Editor/CanvasLeft/Label/index.vue

@@ -2,55 +2,64 @@
   <div>
     <div class="left-top-tabs" id="left-top-tabs">
       <div class="top-tab">
-<!--        <el-tooltip placement="top" :hide-after="0" content="首页">-->
-<!--          <IconHome class="handler-item" @click="goHome"/>-->
-<!--        </el-tooltip>-->
+        <!--        <el-tooltip placement="top" :hide-after="0" content="首页">-->
+        <!--          <IconHome class="handler-item" @click="goHome"/>-->
+        <!--        </el-tooltip>-->
       </div>
     </div>
     <div class="left-bottom-tabs">
       <div class="center-tabs">
-        <div class="center-tab" :class="{ 'left-active': tab.key === poolType }" v-for="tab in topTabs" :key="tab.key" @click="setPoolType(tab.key)">
+        <div class="center-tab" :class="{ 'left-active': tab.key === poolType }" v-for="tab in topTabsRef"
+             :key="tab.key"
+             @click="setPoolType(tab.key)">
           <div class="flex justify-center items-center flex-col" :id="`left-tabs-${tab.key}`">
-            <SvgIcon :icon-class="tab.icon" className="svg-size" />
+            <SvgIcon :icon-class="tab.icon" className="svg-size"/>
             <div class="left-name">{{ $t(tab.label) }}</div>
           </div>
         </div>
       </div>
-      <div class="bottom-tabs">
-        <!-- <div class="bottom-tab" :class="{ 'left-active': 'layer' === poolType }" @click="setPoolType('layer')">
-          <div :id="`left-tabs-layer`">
-            <div><SvgIcon icon-class="layer" className="svg-size" /></div>
-            <div class="left-name">{{ $t("message.layer") }}</div>
-          </div>
-        </div> -->
-        <div class="bottom-tab" :class="{ 'left-active': 'help' === poolType }" ref="helpRef" @click="setPoolType('help')">
-          <div :id="`left-tabs-help`">
-            <div><SvgIcon icon-class="help" className="svg-size" /></div>
-            <div class="left-name">{{ $t("message.help") }}</div>
-          </div>
-        </div>
-        <HelpPopover :help-ref="helpRef" :help-popover-ref="helpPopoverRef" />
-        <HotkeyDrawer :has-hotkey="hasHotkey" />
-      </div>
+      <!--      <div class="bottom-tabs">-->
+      <!--        &lt;!&ndash; <div class="bottom-tab" :class="{ 'left-active': 'layer' === poolType }" @click="setPoolType('layer')">-->
+      <!--          <div :id="`left-tabs-layer`">-->
+      <!--            <div><SvgIcon icon-class="layer" className="svg-size" /></div>-->
+      <!--            <div class="left-name">{{ $t("message.layer") }}</div>-->
+      <!--          </div>-->
+      <!--        </div> &ndash;&gt;-->
+      <!--        <div class="bottom-tab" :class="{ 'left-active': 'help' === poolType }" ref="helpRef"-->
+      <!--             @click="setPoolType('help')">-->
+      <!--          <div :id="`left-tabs-help`">-->
+      <!--            <div>-->
+      <!--              <SvgIcon icon-class="help" className="svg-size"/>-->
+      <!--            </div>-->
+      <!--            <div class="left-name">{{ $t("message.help") }}</div>-->
+      <!--          </div>-->
+      <!--        </div>-->
+      <!--        <HelpPopover :help-ref="helpRef" :help-popover-ref="helpPopoverRef"/>-->
+      <!--        <HotkeyDrawer :has-hotkey="hasHotkey"/>-->
+      <!--      </div>-->
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { useMainStore } from "@/store";
-import { PoolType } from "@/types/common";
-import { storeToRefs } from "pinia";
-import { useRouter } from 'vue-router'
+import {useMainStore} from "@/store";
+import {PoolType} from "@/types/common";
+import {storeToRefs} from "pinia";
+import {useRouter} from 'vue-router'
 import HotkeyDrawer from "./components/HotkeyDrawer.vue";
 import HelpPopover from "./components/HelpPopover.vue";
+import {onMounted} from "vue";
+
 const router = useRouter()
 const mainStore = useMainStore();
-const { poolType, poolShow } = storeToRefs(mainStore);
+const {poolType, poolShow} = storeToRefs(mainStore);
 
 const helpRef = ref();
 const helpPopoverRef = ref();
 const hasHotkey = ref(false);
 
+const runMode = ref(0);
+
 interface TabItem {
   key: PoolType;
   label: string;
@@ -58,16 +67,22 @@ interface TabItem {
   index: number;
 }
 
+const topTabsRef = ref([])
+
 const topTabs: TabItem[] = [
-  { key: "editor", label: "message.edit", icon: `editor`, index: 0 },
+  {key: "editor", label: "message.edit", icon: `editor`, index: 0},
   // { key: "template", label: "message.template", icon: `template`, index: 1 },
-  { key: "material", label: "message.material", icon: `material`, index: 2 },
-  { key: "text", label: "message.text", icon: "text", index: 3 },
-  { key: "image", label: "message.image", icon: "picture", index: 4 },
+  {key: "material", label: "message.material", icon: `material`, index: 2},
+  {key: "text", label: "message.text", icon: "text", index: 3},
+  {key: "image", label: "message.image", icon: "picture", index: 4},
   // { key: "toolkit", label: "message.tool", icon: "toolkit", index: 5 },
   // { key: "chatgpt", label: "message.chatgpt", icon: "chatgpt", index: 6 },
 ];
 
+const topTabsFront: TabItem[] = [
+  {key: "frontEditor", label: "message.edit", icon: `editor`, index: 0}
+];
+
 const setPoolType = (tab: PoolType) => {
   if (poolShow.value && tab === poolType.value) {
     poolShow.value = false;
@@ -80,6 +95,20 @@ const setPoolType = (tab: PoolType) => {
 const goHome = () => {
   window.open(router.resolve({path: `/home`}).href, '_blank');
 }
+
+onMounted(() => {
+  const query = router.currentRoute.value.query
+  if (query.runMode) {
+    runMode.value = query.runMode
+  }
+  if (runMode.value == 0) {
+    topTabsRef.value = topTabsFront;
+    setTimeout(() => {
+      setPoolType("frontEditor")
+    }, 300)
+
+  } else topTabsRef.value = topTabs;
+})
 </script>
 
 <style lang="scss" scoped>
@@ -94,6 +123,7 @@ const goHome = () => {
   justify-content: center;
   align-items: center;
   border-bottom: 1px solid $borderColor;
+
   .handler-item {
     width: 32px;
     height: 32px;
@@ -108,15 +138,18 @@ const goHome = () => {
     }
   }
 }
+
 .center-tabs {
   // overflow-y: scroll;
   overflow-x: hidden;
   height: calc(100vh - 100px);
+
   .center-tab:hover {
     background: #f1f1f1;
     border-radius: 5px;
   }
 }
+
 .center-tab {
   width: 100%;
   height: 60px;
@@ -129,16 +162,20 @@ const goHome = () => {
   justify-content: center;
   position: relative;
 }
+
 .left-active {
   color: $themeColor;
 }
+
 .left-name {
   font-size: 14px;
   line-height: 1.2;
 }
+
 .svg-size {
   font-size: 20px;
 }
+
 .left-active::before {
   background-color: $themeColor;
   border-radius: 4px;
@@ -150,6 +187,7 @@ const goHome = () => {
   width: 6px;
   z-index: 20;
 }
+
 .left-content {
   position: relative;
   width: 300px;
@@ -162,6 +200,7 @@ const goHome = () => {
   border-right: 1px solid $borderColor;
   transition: left 0.5s linear, right 0.5s linear;
 }
+
 .left-close {
   cursor: default;
   left: -320px;
@@ -169,6 +208,7 @@ const goHome = () => {
   top: 50%;
   // z-index: 1;
 }
+
 .layout-toggle {
   background: $themeColor;
   cursor: pointer;
@@ -188,6 +228,7 @@ const goHome = () => {
   border-bottom: 1px solid $borderColor;
   border-right: 1px solid $borderColor;
 }
+
 .bottom-tabs {
   position: absolute;
   bottom: 0;
@@ -195,6 +236,7 @@ const goHome = () => {
   z-index: 30;
   border-right: 1px solid #eee;
 }
+
 .bottom-tab {
   height: 60px;
   display: flex;
@@ -205,9 +247,11 @@ const goHome = () => {
   background: #fff;
   position: relative;
   border-radius: 5px;
+
   .help-handle {
     font-size: 20px;
   }
+
   #left-tabs-help,
   #left-tabs-layer {
     width: 100%;
@@ -216,17 +260,21 @@ const goHome = () => {
     justify-content: center;
     align-items: center;
   }
+
   &:hover {
     background: #f1f1f1;
   }
 }
+
 .has-help {
   color: $themeColor;
 }
+
 .help-pop-row {
   font-size: 15px;
   padding: 10px 25px;
   cursor: pointer;
+
   .help-pop-icon {
     font-size: 20px;
   }
@@ -235,6 +283,7 @@ const goHome = () => {
     padding-left: 10px;
   }
 }
+
 .help-pop-row:hover {
   background-color: $hoverColor;
 }

+ 468 - 0
src/views/Editor/CanvasLeft/Menu/components/FrontEditorPool.vue

@@ -0,0 +1,468 @@
+<template>
+  <div>
+    <el-scrollbar>
+      <el-collapse v-model="activeNames">
+        <template v-for="(object,index) in objects">
+          <el-collapse-item v-if="object.permissionsConfig && (object.permissionsConfig.length > 0)"
+                            :title="object.itemName" :name="object.id">
+            <el-form
+                :key="object.id"
+                label-width="auto"
+                label-position="right"
+                size="small"
+            >
+              <template v-for="(perm) in object.permissionsConfig">
+                <template v-if="object.id == 'WorkSpaceDrawType'">
+                  <!--              背景-->
+                  <el-form-item v-if="perm == '0'" label="背景颜色">
+                    <el-popover trigger="click" placement="right" :width="265">
+                      <template #reference>
+                        <ColorButton style="width: 100px" :color="background.color || '#fff'"/>
+                      </template>
+                      <ColorPicker
+                          :modelValue="background.color"
+                          @update:modelValue="(color: string) => updateBackground({color: color, fill: color})"
+                      />
+                    </el-popover>
+                  </el-form-item>
+                  <el-form-item v-else-if="perm == '1'" label="背景尺寸">
+                    <el-row>
+                      <el-col :span="11">
+                        <el-input
+                            v-model="canvasWidth"
+                            :value="pageSizeWidth"
+                            @change="changeTemplateWidth"
+                            oninput="value=value.replace(/[^\d.]/g,'')"
+                        >
+                          <template #prepend>宽</template>
+                        </el-input>
+                      </el-col>
+                      <el-col :span="1"/>
+                      <el-col :span="11">
+                        <el-input
+                            v-model="canvasHeight"
+                            :value="pageSizeHeight"
+                            @change="changeTemplateHeight"
+                            oninput="value=value.replace(/[^\d.]/g,'')"
+                        >
+                          <template #prepend>高</template>
+                        </el-input>
+                      </el-col>
+                    </el-row>
+                  </el-form-item>
+                  <el-form-item v-else-if="perm == '2'" label="上传背景">
+                    <!--                暂不实现-->
+                  </el-form-item>
+                </template>
+
+                <template v-else-if="object.name == 'textbox'">
+                  <!--             文字-->
+                  <el-form-item v-if="perm == '0'" label="内容">
+                    <el-tag type="primary">请点击右侧元素更改文字内容</el-tag>
+                  </el-form-item>
+                  <el-form-item v-if="perm == '1'" label="字号">
+                    <el-button-group class="full-group">
+                      <el-tooltip placement="top" content="增大字号" :hide-after="0">
+                        <el-button class="font-size" @click="handleElementFontsize(object.id,'+')">
+                          <IconFontSize/>
+                          +
+                        </el-button>
+                      </el-tooltip>
+
+                      <el-tooltip placement="top" content="减小字号" :hide-after="0">
+                        <el-button @click="handleElementFontsize(object.id,'-')">
+                          <IconFontSize/>
+                          -
+                        </el-button>
+                      </el-tooltip>
+                    </el-button-group>
+                  </el-form-item>
+                  <el-form-item v-if="perm == '2'" label="字体">
+                    <el-select v-model="object.fontFamily" placement="left" style="width: 180px"
+                               @change="handleElementFontFamily(object.id, $event)">
+                      <el-option-group v-for="group in fontOptionGroups" :key="group.label" :label="group.label">
+                        <template v-if="group.type == 0">
+                          <el-option v-for="item in group.options" :key="item" :value="item.value" :label="item.label"
+                                     :style="{fontFamily: item.value}"></el-option>
+                        </template>
+                        <template v-else>
+                          <el-option v-for="item in group.options" :key="item.id" :value="item.fontFamily"
+                                     :label="item.fontName">
+                            <el-image style="height: 18px; width: 90px" :src="item.fontThumbUrl"></el-image>
+                          </el-option>
+                        </template>
+                      </el-option-group>
+                    </el-select>
+                  </el-form-item>
+                  <el-form-item v-if="perm == '3'" label="颜色">
+                    <el-tooltip placement="top" content="文字颜色" :hide-after="0">
+                      <div @click.stop>
+                        <el-popover trigger="click" placement="right" :width="265" @click.stop>
+                          <template #reference>
+                            <el-button class="font-color">
+                              <TextColorButton :color="object.color">
+                                <IconText/>
+                              </TextColorButton>
+                            </el-button>
+                          </template>
+                          <ColorPicker :modelValue="object.color"
+                                       @update:modelValue="(color: string) => updateFontColor(object.id,color)"/>
+                        </el-popover>
+                      </div>
+                    </el-tooltip>
+                  </el-form-item>
+                  <el-form-item v-if="perm == '4'" label="字间距">
+                    <el-button-group class="full-group">
+                      <el-tooltip placement="top" content="减小缩进" :hide-after="0">
+                        <el-button @click="handleElementCharSpacing(object.id,'-')">
+                          <IconIndentLeft/>
+                        </el-button>
+                      </el-tooltip>
+                      <el-tooltip placement="top" content="增大缩进" :hide-after="0">
+                        <el-button @click="handleElementCharSpacing(object.id,'+')">
+                          <IconIndentRight/>
+                        </el-button>
+                      </el-tooltip>
+                    </el-button-group>
+                  </el-form-item>
+                  <el-form-item v-if="perm == '5'" label="尺寸">
+                    <el-tag type="primary">请拖拽右侧元素更改元素尺寸</el-tag>
+                  </el-form-item>
+                  <el-form-item v-if="perm == '6'" label="位置">
+                    <el-tag type="primary">请拖拽右侧元素更改元素位置</el-tag>
+                  </el-form-item>
+                </template>
+
+                <template v-else-if="object.name == 'image'">
+                  <!--             图片-->
+                  <el-form-item v-if="perm == '0'" label="上传图片">
+                    <FileInput class="full-width-btn" @change="(files: FileList) => replaceImage(object.id,files)">
+                      <el-button class="full-btn">
+                        <IconTransform class="btn-icon"/>
+                      </el-button>
+                    </FileInput>
+                  </el-form-item>
+                  <el-form-item v-if="perm == '1'" label="尺寸">
+                    <el-tag type="primary">请拖拽右侧元素更改元素尺寸</el-tag>
+                  </el-form-item>
+                  <el-form-item v-if="perm == '2'" label="位置">
+                    <el-tag type="primary">请拖拽右侧元素更改元素位置</el-tag>
+                  </el-form-item>
+                </template>
+
+              </template>
+            </el-form>
+          </el-collapse-item>
+        </template>
+      </el-collapse>
+    </el-scrollbar>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import useCanvas from "@/views/Canvas/useCanvas";
+import {computed, ref} from "vue";
+import {MaxSize, MinSize, TransparentFill} from "@/configs/background";
+import {WorkSpaceElement} from "@/types/canvas";
+import {storeToRefs} from "pinia";
+import {useFabricStore, useMainStore, useTemplatesStore} from "@/store";
+import {propertiesToInclude, WorkSpaceDrawType} from "@/configs/canvas";
+import {Gradient, Pattern, Image, util, Textbox} from "fabric";
+import useHandleBackground from "@/hooks/useHandleBackground";
+import {mm2px, px2mm} from "@/utils/image";
+import {ElMessage} from "element-plus";
+import useI18n from "@/hooks/useI18n";
+import {ArcText} from "@/extension/object/ArcText";
+import {FontGroupOption} from "@/types/elements";
+
+const {t} = useI18n();
+const mainStore = useMainStore();
+const {sizeMode, unitMode} = storeToRefs(mainStore);
+const {canvasObject, systemFonts, onlineFonts} = storeToRefs(mainStore)
+const [canvas] = useCanvas();
+const templatesStore = useTemplatesStore();
+const {setBackgroudImage} = useHandleBackground();
+const {currentTemplate} = storeToRefs(templatesStore);
+const fabricStore = useFabricStore();
+const {clip, safe, zoom, opacity} = storeToRefs(fabricStore);
+const objects = canvas.getObjects().filter((object) => !["WorkSpaceMaskType", "WorkSpaceClipType", "WorkSpaceSafeType", "WorkSpaceClipType"].includes(object.id));
+
+
+const activeNames = ref([])
+
+const pageSizeWidth = computed(() => {
+  return Math.round(templateWidth.value * 100) / 100
+})
+const pageSizeHeight = computed(() => {
+  return Math.round(templateHeight.value * 100) / 100
+})
+const templateWidth = computed(() => {
+  const workWidth = currentTemplate.value.width / currentTemplate.value.zoom;
+  return unitMode.value === 0 ? px2mm(workWidth) : workWidth;
+});
+
+const templateHeight = computed(() => {
+  const workHeight = currentTemplate.value.height / currentTemplate.value.zoom;
+  return unitMode.value === 0 ? px2mm(workHeight) : workHeight;
+});
+
+const canvasWidth = ref<number>(templateWidth.value);
+const canvasHeight = ref<number>(templateHeight.value);
+
+const background = computed(() => {
+  if (!currentTemplate.value) {
+    return {
+      fillType: 0,
+      fill: TransparentFill,
+      backgroundColor: "#fff",
+    } as WorkSpaceElement;
+  }
+  if (!currentTemplate.value.workSpace) {
+    return {
+      fillType: 0,
+      fill: TransparentFill,
+      backgroundColor: "#fff",
+    } as WorkSpaceElement;
+  }
+  return currentTemplate.value.workSpace;
+});
+
+const fontOptionGroups = ref<FontGroupOption[]>([
+  {
+    type: 0,
+    label: '系统字体',
+    options: systemFonts.value
+  },
+  {
+    type: 1,
+    label: '在线字体',
+    options: onlineFonts.value
+  }
+])
+
+
+// 固定宽高
+const isFixed = ref(false);
+// 设置背景
+const updateBackground = (props: Partial<WorkSpaceElement>) => {
+  const [canvas] = useCanvas();
+  const workSpaceDraw = canvas.getObjects().filter((item) => item.id === WorkSpaceDrawType)[0];
+  if (!workSpaceDraw) return;
+  workSpaceDraw.set({...props});
+  if (props.fill instanceof Pattern) {
+    props.fill = props.fill.toObject() as Pattern
+  }
+  templatesStore.updateWorkSpace({workSpace: {...background.value, ...props}});
+  const workProps = workSpaceDraw.toObject(propertiesToInclude as any[]);
+  templatesStore.updateElement({id: workSpaceDraw.id, props: {...workProps, ...props}});
+  canvas.renderAll();
+};
+
+// 修改上传背景
+const changeBackgroundImage = async (imageURL: string) => {
+  if (background.value.imageSize === "repeat") {
+    const backgroundImage = await util.loadImage(imageURL);
+    const workSpacePattern = new Pattern({
+      source: backgroundImage,
+      repeat: "repeat",
+    });
+    updateBackground({fill: workSpacePattern, imageURL});
+  } else {
+    setBackgroudImage(imageURL);
+    updateBackground({fill: TransparentFill, imageURL});
+  }
+};
+
+// 获取画布尺寸
+const getCanvasSize = () => {
+  let width =
+      unitMode.value === 0 ? mm2px(canvasWidth.value) : canvasWidth.value;
+  let height =
+      unitMode.value === 0 ? mm2px(canvasHeight.value) : canvasHeight.value;
+  width = width * zoom.value;
+  height = height * zoom.value;
+  return {width, height};
+};
+
+// 修改画布宽度
+const changeTemplateWidth = () => {
+  const [canvas] = useCanvas();
+  const workSpaceDraw = canvas
+      .getObjects()
+      .filter((item) => item.id === WorkSpaceDrawType)[0];
+  if (!workSpaceDraw) return;
+  const ratio = currentTemplate.value.height / currentTemplate.value.width;
+  let {width, height} = getCanvasSize();
+  if (width / zoom.value < mm2px(MinSize)) {
+    ElMessage({
+      message: t("style.minimumSizeLimit") + MinSize,
+      type: "warning",
+    });
+    width = mm2px(MinSize) * zoom.value;
+  }
+  if (width / zoom.value > mm2px(MaxSize)) {
+    ElMessage({
+      message: t("style.maximumSizeLimit") + MaxSize,
+      type: "warning",
+    });
+    width = mm2px(MaxSize) * zoom.value;
+  }
+  height = isFixed.value ? width * ratio : height;
+  workSpaceDraw.set({width: width / zoom.value, height: height / zoom.value});
+  templatesStore.setSize(width, height, zoom.value);
+  sizeMode.value = 2;
+  canvas.renderAll();
+  // resetCanvas()
+  // addHistorySnapshot();
+};
+
+// 修改画布高度
+const changeTemplateHeight = () => {
+  const [canvas] = useCanvas();
+  const workSpaceDraw = canvas
+      .getObjects()
+      .filter((item) => item.id === WorkSpaceDrawType)[0];
+  if (!workSpaceDraw) return;
+  const ratio = currentTemplate.value.height / currentTemplate.value.width;
+  let {width, height} = getCanvasSize();
+  if (height / zoom.value < mm2px(MinSize)) {
+    ElMessage({
+      message: t("style.minimumSizeLimit") + MinSize,
+      type: "warning",
+    });
+    height = mm2px(MinSize) * zoom.value;
+  }
+  if (height / zoom.value > mm2px(MaxSize)) {
+    ElMessage({
+      message: t("style.maximumSizeLimit") + MaxSize,
+      type: "warning",
+    });
+    height = mm2px(MaxSize) * zoom.value;
+  }
+  width = isFixed.value ? height / ratio : width;
+  workSpaceDraw.set({width: width / zoom.value, height: height / zoom.value});
+  templatesStore.setSize(width, height, zoom.value);
+  sizeMode.value = 2;
+  canvas.renderAll();
+  // resetCanvas()
+  // addHistorySnapshot();
+};
+
+//查找目标元素
+const findObject = (objId: string) => {
+  return objects.find((item) => item.id === objId);
+}
+
+// 修改字体大小
+const handleElementFontsize = (objId: string, mode: string) => {
+  const handleElement = findObject(objId) as Textbox | ArcText;
+
+  if (handleElement.fontSize <= 6) return
+  const fontSize = mode === '+' ? handleElement.fontSize + 1 : handleElement.fontSize - 1
+  if (handleElement.isEditing) {
+    handleElement.setSelectionStyles({fontSize})
+  } else {
+    templatesStore.modifedElement(handleElement, {fontSize})
+  }
+  canvas.renderAll()
+}
+
+// 修改字体族
+const handleElementFontFamily = (objId: string, fontFamily: string) => {
+  const handleElement = findObject(objId) as Textbox | ArcText;
+
+  if (handleElement.isEditing) {
+    handleElement.setSelectionStyles({fontFamily})
+  } else {
+    templatesStore.modifedElement(handleElement, {fontFamily})
+  }
+  canvas.renderAll()
+}
+
+// 修改字体颜色
+const updateFontColor = (objId: string, fill: string) => {
+  const handleElement = findObject(objId) as Textbox | ArcText;
+
+  if (handleElement.isEditing) {
+    handleElement.setSelectionStyles({fill})
+  } else {
+    templatesStore.modifedElement(handleElement, {fill, color: fill})
+  }
+}
+
+// 修改缩进
+const handleElementCharSpacing = (objId: string, mode: '+' | '-') => {
+  const handleElement = findObject(objId) as Textbox | ArcText;
+
+  const handleCharSpacing = handleElement.charSpacing
+  const charSpacing = mode === '+' ? handleCharSpacing + 10 : handleCharSpacing - 10
+  templatesStore.modifedElement(handleElement, {charSpacing})
+}
+
+// 替换图片(保持当前的样式)
+const replaceImage = (objId: string, files: FileList) => {
+  const handleElement = findObject(objId) as Image;
+
+  const props = {src: files.fileLink};
+  handleElement.setSrc(files.fileLink); //红就红吧type硬赋值了
+  templatesStore.updateElement({id: handleElement.id, props});
+};
+
+onMounted(() => {
+  console.log(objects)
+  activeNames.value = objects.map(x => x.id)
+})
+
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-tabs__item) {
+  padding: 0;
+}
+
+.layout-search {
+  margin: 0 auto;
+  width: 80%;
+  padding: 20px 10px 10px;
+}
+
+.layout-upload {
+  justify-content: center;
+}
+
+.layout-tabs {
+  width: 90%;
+  margin: 0 auto;
+}
+
+.layout-templates {
+  display: flex;
+  flex-wrap: wrap;
+  padding: 2px;
+
+  .thumbnail {
+    display: flex;
+    width: 124px;
+    margin: 2px;
+  }
+
+  .thumbnail img {
+    outline: 1px solid $borderColor;
+    margin: 0 5px;
+    cursor: pointer;
+
+    &:hover {
+      outline-color: $themeColor;
+    }
+  }
+}
+
+.col-img {
+  height: 100px;
+
+  img {
+    max-height: 100%;
+  }
+}
+
+</style>

+ 11 - 6
src/views/Editor/CanvasLeft/Menu/index.vue

@@ -3,7 +3,7 @@
     <div class="menu-content" :class="{ 'menu-close': poolShow !== true }">
       <component :is="currentComponent" class="menu-pool"></component>
       <div class="layout-toggle" @click="leftToggle" v-show="currentComponent">
-        <IconLeft  class="toggle-icon" v-if="poolShow"/>
+        <IconLeft class="toggle-icon" v-if="poolShow"/>
         <IconRight class="toggle-icon" v-else/>
       </div>
     </div>
@@ -11,9 +11,9 @@
 </template>
 
 <script lang="ts" setup>
-import { useMainStore } from '@/store'
-import { storeToRefs } from 'pinia'
-import { computed } from 'vue'
+import {useMainStore} from '@/store'
+import {storeToRefs} from 'pinia'
+import {computed} from 'vue'
 import EditorPool from './components/EditorPool.vue'
 import TemplatePool from './components/TemplatePool.vue'
 import MaterialPool from './components/MaterialPool.vue'
@@ -23,9 +23,10 @@ import ToolkitPool from './components/ToolkitPool.vue'
 import ChatgptPool from './components/ChatgptPool.vue'
 import LayerPool from './components/LayerPool.vue'
 import CodePool from './components/CodePool.vue'
+import FrontEditorPool from "./components/FrontEditorPool.vue";
 
 const mainStore = useMainStore()
-const { lastHelp, lastEdit, poolType, poolShow } = storeToRefs(mainStore)
+const {lastHelp, lastEdit, poolType, poolShow} = storeToRefs(mainStore)
 
 const leftMap = {
   'editor': EditorPool,
@@ -39,6 +40,7 @@ const leftMap = {
   'layer': LayerPool,
   'chatgpt': ChatgptPool,
   'help': null,
+  'frontEditor': FrontEditorPool,
 }
 const currentComponent = computed(() => {
   if (poolType.value === 'help') return leftMap[lastHelp.value]
@@ -47,7 +49,6 @@ const currentComponent = computed(() => {
 })
 
 
-
 const leftToggle = () => {
   // if (poolType.value === 'editor' && !poolShow.value) return
   poolShow.value = !poolShow.value
@@ -68,12 +69,14 @@ const leftToggle = () => {
   border-bottom: 1px solid $borderColor;
   transition: left 0.5s linear, right 0.5s linear;
 }
+
 .menu-pool {
   width: 300px;
   height: 100vh;
   transition: left .3s linear;
   border-bottom: 1px solid $borderColor;
 }
+
 .menu-close {
   cursor: default;
   left: -251px;
@@ -82,6 +85,7 @@ const leftToggle = () => {
   position: absolute;;
   // z-index: 1;
 }
+
 .layout-toggle {
   background: $themeColor;
   cursor: pointer;
@@ -101,6 +105,7 @@ const leftToggle = () => {
   border-bottom: 1px solid $borderColor;
   border-right: 1px solid $borderColor;
 }
+
 .toggle-icon {
   color: #fff;
 }

+ 165 - 118
src/views/Editor/CanvasRight/CanvasStylePanel/index.vue

@@ -1,6 +1,20 @@
 <template>
   <div class="canvas-design-panel">
     <div class="mb-10">
+      <b>客户可编辑项</b>
+    </div>
+    <div class="mb-10">
+      <el-checkbox-group v-model="backgroundPerms" size="small">
+        <el-checkbox v-for="perm in selectablePerms" :key="perm.code" :value="perm.code"
+                     @change="backgroundPermChange">
+          {{ perm.name }}
+        </el-checkbox>
+      </el-checkbox-group>
+    </div>
+
+    <el-divider style="margin: 12px 0"/>
+
+    <div class="mb-10">
       <b>{{ t("style.canvasSize") }}</b>
     </div>
     <div class="mb-10">
@@ -45,135 +59,135 @@
         </el-col>
       </el-row>
     </div>
-    <div class="mb-10">
-      <el-row>
-        <el-col :span="11">
-          <el-input
-              v-model="clip"
-              @change="changeTemplateClip"
-              oninput="value=value.replace(/[^\d]/g,'')"
-              :disabled="unitMode === 1"
-          >
-            <template #prepend>
-              <el-tooltip
-                  placement="top"
-                  :hide-after="0"
-                  :content="t('style.bleedingLine')"
-              >
-                <IconCuttingOne/>
-              </el-tooltip>
-            </template>
-          </el-input>
-        </el-col>
-<!--        <el-col :span="2" class="fixed-ratio">-->
-<!--          <el-tooltip-->
-<!--              effect="dark"-->
-<!--              placement="top"-->
-<!--              :content="t('style.fillet')"-->
-<!--              v-if="isRound"-->
-<!--          >-->
-<!--            <IconRound class="icon-btn" @click="changeWorkRound(false)"/>-->
-<!--          </el-tooltip>-->
-<!--          <el-tooltip-->
-<!--              effect="dark"-->
-<!--              placement="top"-->
-<!--              :content="t('style.rightAngle')"-->
-<!--              v-else-->
-<!--          >-->
-<!--            <IconRightAngle class="icon-btn" @click="changeWorkRound(true)"/>-->
-<!--          </el-tooltip>-->
-<!--        </el-col>-->
-<!--        <el-col :span="11">-->
-<!--          <el-input-->
-<!--              v-model="safe"-->
-<!--              @change="changeTemplateSafe"-->
-<!--              oninput="value=value.replace(/[^\d]/g,'')"-->
-<!--              :disabled="unitMode === 1"-->
-<!--          >-->
-<!--            <template #prepend>-->
-<!--              <el-tooltip-->
-<!--                  placement="top"-->
-<!--                  :hide-after="0"-->
-<!--                  :content="t('style.safetyLine')"-->
-<!--              >-->
-<!--                <IconShield/>-->
-<!--              </el-tooltip>-->
-<!--            </template>-->
-<!--          </el-input>-->
-<!--        </el-col>-->
-      </el-row>
-    </div>
-<!--    <div class="mt-10">-->
-<!--      <el-row>-->
-<!--        <el-col :span="11">-->
-<!--          <el-select v-model="unitMode" @change="changeUnitMode">-->
-<!--            <template #prefix>-->
-<!--              <el-tooltip-->
-<!--                  placement="top"-->
-<!--                  :hide-after="0"-->
-<!--                  :content="t('style.unit')"-->
-<!--              >-->
-<!--                <IconRuler/>-->
-<!--              </el-tooltip>-->
-<!--            </template>-->
-<!--            <el-option-->
-<!--                v-for="item in DesignUnitMode"-->
-<!--                :key="item.id"-->
-<!--                :label="item.name"-->
-<!--                :value="item.id"-->
-<!--            ></el-option>-->
-<!--          </el-select>-->
-<!--        </el-col>-->
-<!--        <el-col :span="2"></el-col>-->
-<!--        &lt;!&ndash;        <el-col :span="11">&ndash;&gt;-->
-<!--        &lt;!&ndash;          <el-select v-model="sizeMode">&ndash;&gt;-->
-<!--        &lt;!&ndash;            <template #prefix>&ndash;&gt;-->
-<!--        &lt;!&ndash;              <el-tooltip&ndash;&gt;-->
-<!--        &lt;!&ndash;                placement="top"&ndash;&gt;-->
-<!--        &lt;!&ndash;                :hide-after="0"&ndash;&gt;-->
-<!--        &lt;!&ndash;                :content="t('style.template')"&ndash;&gt;-->
-<!--        &lt;!&ndash;              >&ndash;&gt;-->
-<!--        &lt;!&ndash;                <IconIdCard />&ndash;&gt;-->
-<!--        &lt;!&ndash;              </el-tooltip>&ndash;&gt;-->
-<!--        &lt;!&ndash;            </template>&ndash;&gt;-->
-<!--        &lt;!&ndash;            <el-option&ndash;&gt;-->
-<!--        &lt;!&ndash;              v-for="item in DesignSizeMode"&ndash;&gt;-->
-<!--        &lt;!&ndash;              :key="item.id"&ndash;&gt;-->
-<!--        &lt;!&ndash;              :label="item.name"&ndash;&gt;-->
-<!--        &lt;!&ndash;              :value="item.id"&ndash;&gt;-->
-<!--        &lt;!&ndash;              :disabled="item.disabled"&ndash;&gt;-->
-<!--        &lt;!&ndash;            ></el-option>&ndash;&gt;-->
-<!--        &lt;!&ndash;          </el-select>&ndash;&gt;-->
-<!--        &lt;!&ndash;        </el-col>&ndash;&gt;-->
-<!--      </el-row>-->
-<!--    </div>-->
+    <!--    <div class="mb-10">-->
+    <!--      <el-row>-->
+    <!--        <el-col :span="11">-->
+    <!--          <el-input-->
+    <!--              v-model="clip"-->
+    <!--              @change="changeTemplateClip"-->
+    <!--              oninput="value=value.replace(/[^\d]/g,'')"-->
+    <!--              :disabled="unitMode === 1"-->
+    <!--          >-->
+    <!--            <template #prepend>-->
+    <!--              <el-tooltip-->
+    <!--                  placement="top"-->
+    <!--                  :hide-after="0"-->
+    <!--                  :content="t('style.bleedingLine')"-->
+    <!--              >-->
+    <!--                <IconCuttingOne/>-->
+    <!--              </el-tooltip>-->
+    <!--            </template>-->
+    <!--          </el-input>-->
+    <!--        </el-col>-->
+    <!--        <el-col :span="2" class="fixed-ratio">-->
+    <!--          <el-tooltip-->
+    <!--              effect="dark"-->
+    <!--              placement="top"-->
+    <!--              :content="t('style.fillet')"-->
+    <!--              v-if="isRound"-->
+    <!--          >-->
+    <!--            <IconRound class="icon-btn" @click="changeWorkRound(false)"/>-->
+    <!--          </el-tooltip>-->
+    <!--          <el-tooltip-->
+    <!--              effect="dark"-->
+    <!--              placement="top"-->
+    <!--              :content="t('style.rightAngle')"-->
+    <!--              v-else-->
+    <!--          >-->
+    <!--            <IconRightAngle class="icon-btn" @click="changeWorkRound(true)"/>-->
+    <!--          </el-tooltip>-->
+    <!--        </el-col>-->
+    <!--        <el-col :span="11">-->
+    <!--          <el-input-->
+    <!--              v-model="safe"-->
+    <!--              @change="changeTemplateSafe"-->
+    <!--              oninput="value=value.replace(/[^\d]/g,'')"-->
+    <!--              :disabled="unitMode === 1"-->
+    <!--          >-->
+    <!--            <template #prepend>-->
+    <!--              <el-tooltip-->
+    <!--                  placement="top"-->
+    <!--                  :hide-after="0"-->
+    <!--                  :content="t('style.safetyLine')"-->
+    <!--              >-->
+    <!--                <IconShield/>-->
+    <!--              </el-tooltip>-->
+    <!--            </template>-->
+    <!--          </el-input>-->
+    <!--        </el-col>-->
+    <!--      </el-row>-->
+    <!--    </div>-->
+    <!--    <div class="mt-10">-->
+    <!--      <el-row>-->
+    <!--        <el-col :span="11">-->
+    <!--          <el-select v-model="unitMode" @change="changeUnitMode">-->
+    <!--            <template #prefix>-->
+    <!--              <el-tooltip-->
+    <!--                  placement="top"-->
+    <!--                  :hide-after="0"-->
+    <!--                  :content="t('style.unit')"-->
+    <!--              >-->
+    <!--                <IconRuler/>-->
+    <!--              </el-tooltip>-->
+    <!--            </template>-->
+    <!--            <el-option-->
+    <!--                v-for="item in DesignUnitMode"-->
+    <!--                :key="item.id"-->
+    <!--                :label="item.name"-->
+    <!--                :value="item.id"-->
+    <!--            ></el-option>-->
+    <!--          </el-select>-->
+    <!--        </el-col>-->
+    <!--        <el-col :span="2"></el-col>-->
+    <!--        &lt;!&ndash;        <el-col :span="11">&ndash;&gt;-->
+    <!--        &lt;!&ndash;          <el-select v-model="sizeMode">&ndash;&gt;-->
+    <!--        &lt;!&ndash;            <template #prefix>&ndash;&gt;-->
+    <!--        &lt;!&ndash;              <el-tooltip&ndash;&gt;-->
+    <!--        &lt;!&ndash;                placement="top"&ndash;&gt;-->
+    <!--        &lt;!&ndash;                :hide-after="0"&ndash;&gt;-->
+    <!--        &lt;!&ndash;                :content="t('style.template')"&ndash;&gt;-->
+    <!--        &lt;!&ndash;              >&ndash;&gt;-->
+    <!--        &lt;!&ndash;                <IconIdCard />&ndash;&gt;-->
+    <!--        &lt;!&ndash;              </el-tooltip>&ndash;&gt;-->
+    <!--        &lt;!&ndash;            </template>&ndash;&gt;-->
+    <!--        &lt;!&ndash;            <el-option&ndash;&gt;-->
+    <!--        &lt;!&ndash;              v-for="item in DesignSizeMode"&ndash;&gt;-->
+    <!--        &lt;!&ndash;              :key="item.id"&ndash;&gt;-->
+    <!--        &lt;!&ndash;              :label="item.name"&ndash;&gt;-->
+    <!--        &lt;!&ndash;              :value="item.id"&ndash;&gt;-->
+    <!--        &lt;!&ndash;              :disabled="item.disabled"&ndash;&gt;-->
+    <!--        &lt;!&ndash;            ></el-option>&ndash;&gt;-->
+    <!--        &lt;!&ndash;          </el-select>&ndash;&gt;-->
+    <!--        &lt;!&ndash;        </el-col>&ndash;&gt;-->
+    <!--      </el-row>-->
+    <!--    </div>-->
 
     <el-divider style="margin: 12px 0"/>
 
     <div class="title">
       <b>{{ t("style.canvasFill") }}</b>
     </div>
-<!--    <div class="row">-->
-<!--      <el-button class="full-row" @click="changeAllBackgroud">{{ t("style.applyCanvasToAll") }}</el-button>-->
-<!--    </div>-->
+    <!--    <div class="row">-->
+    <!--      <el-button class="full-row" @click="changeAllBackgroud">{{ t("style.applyCanvasToAll") }}</el-button>-->
+    <!--    </div>-->
     <Backgrounds/>
 
     <el-divider style="margin: 12px 0"/>
 
-<!--    <Watermark/>-->
+    <!--    <Watermark/>-->
 
-<!--    <el-divider style="margin: 12px 0"/>-->
+    <!--    <el-divider style="margin: 12px 0"/>-->
 
-<!--    <div class="title">-->
-<!--      <b>{{ t("style.canvasMask") }}</b>-->
-<!--    </div>-->
-<!--    <el-row>-->
-<!--      <el-col :span="7" class="slider-name">{{ t("style.opacity") }}:</el-col>-->
-<!--      <el-col :span="13">-->
-<!--        <el-slider :min="0.1" :max="1" :step="0.01" v-model="opacity" @change="changeMaskOpacity"></el-slider>-->
-<!--      </el-col>-->
-<!--      <el-col :span="4" class="slider-num">{{ opacity }}</el-col>-->
-<!--    </el-row>-->
+    <!--    <div class="title">-->
+    <!--      <b>{{ t("style.canvasMask") }}</b>-->
+    <!--    </div>-->
+    <!--    <el-row>-->
+    <!--      <el-col :span="7" class="slider-name">{{ t("style.opacity") }}:</el-col>-->
+    <!--      <el-col :span="13">-->
+    <!--        <el-slider :min="0.1" :max="1" :step="0.01" v-model="opacity" @change="changeMaskOpacity"></el-slider>-->
+    <!--      </el-col>-->
+    <!--      <el-col :span="4" class="slider-num">{{ opacity }}</el-col>-->
+    <!--    </el-row>-->
   </div>
 </template>
 
@@ -181,7 +195,7 @@
 import {Rect} from "fabric";
 import {storeToRefs} from "pinia";
 import {ElMessage} from "element-plus";
-import {ref, watch, onMounted, computed} from "vue";
+import {ref, watch, onMounted, computed, nextTick} from "vue";
 import {mm2px, px2mm} from "@/utils/image";
 import useI18n from "@/hooks/useI18n";
 import {useFabricStore, useMainStore, useTemplatesStore} from "@/store";
@@ -240,6 +254,22 @@ const templateHeight = computed(() => {
 const canvasWidth = ref<number>(templateWidth.value);
 const canvasHeight = ref<number>(templateHeight.value);
 
+const backgroundPerms = ref([]);
+const selectablePerms = [
+  {
+    "name": "背景颜色",
+    "code": "0"
+  },
+  {
+    "name": "背景尺寸",
+    "code": "1"
+  },
+  // {
+  //   "name": "上传背景",
+  //   "code": "2"
+  // }
+]
+
 // 固定宽高
 const isFixed = ref(false);
 
@@ -250,6 +280,15 @@ const isRound = ref(false);
 const RECENT_GRIDS = "RECENT_GRIDS";
 const gridColorRecent = ref<[string[]]>([[]]);
 
+//修改背景的可编辑参数
+const backgroundPermChange = () => {
+  const [canvas] = useCanvas();
+  canvas.getObjects()
+      .filter((item) => item.id === WorkSpaceDrawType).forEach(workSpaceDraw => {
+    workSpaceDraw.set({permissionsConfig: backgroundPerms.value})
+  })
+}
+
 // 获取画布尺寸
 const getCanvasSize = () => {
   let width =
@@ -393,6 +432,14 @@ const changeAllBackgroud = () => {
 onMounted(() => {
   const recentGridCache = localStorage.getItem(RECENT_GRIDS);
   if (recentGridCache) gridColorRecent.value = JSON.parse(recentGridCache);
+
+  const [canvas] = useCanvas();
+  const workSpaceDraw = canvas
+      .getObjects()
+      .filter((item) => item.id === WorkSpaceDrawType)[0];
+  if (!workSpaceDraw) return;
+  if (workSpaceDraw.get('permissionsConfig'))
+    backgroundPerms.value = workSpaceDraw.get('permissionsConfig')
 });
 
 // 保存缓存最近添加的网格

+ 137 - 0
src/views/Editor/CanvasRight/Components/ElementPermission.vue

@@ -0,0 +1,137 @@
+<template>
+  <div class="element-permission">
+    <div class="mb-2">
+      <b>客户可编辑项</b>
+    </div>
+    <div class="mb-2">
+      <el-checkbox-group v-if="eleType == 'text'" v-model="handleElement.permissionsConfig" size="small">
+        <el-checkbox v-for="perm in textPerms" :key="perm.code" :value="perm.code"
+                     @change="eleChange">
+          {{ perm.name }}
+        </el-checkbox>
+      </el-checkbox-group>
+      <el-checkbox-group v-if="eleType == 'image'" v-model="handleElement.permissionsConfig" size="small">
+        <el-checkbox v-for="perm in imagePerms" :key="perm.code" :value="perm.code"
+                     @change="eleChange">
+          {{ perm.name }}
+        </el-checkbox>
+      </el-checkbox-group>
+    </div>
+    <el-divider style="margin: 12px 0"/>
+
+    <el-row>
+      <el-col :span="7" class="slider-name"
+      >元素别名:
+      </el-col
+      >
+      <el-col :span="3"></el-col>
+      <el-col :span="14">
+        <el-input v-model="handleElement.itemName" @change="eleChange">
+        </el-input>
+      </el-col>
+    </el-row>
+    <el-divider style="margin: 12px 0"/>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {computed, ref, watch} from "vue";
+import {storeToRefs} from "pinia";
+import {useMainStore} from "@/store";
+import * as fabric from "fabric";
+import useCanvas from "@/views/Canvas/useCanvas";
+import {CanvasElement} from "@/types/canvas";
+
+const props = defineProps({
+  eleType: {
+    type: String,
+    required: true,
+  },
+});
+
+const eleName = ref("")
+const textPerms = [
+  {
+    "name": "文字内容",
+    "code": "0"
+  },
+  {
+    "name": "字号",
+    "code": "1"
+  },
+  {
+    "name": "字体",
+    "code": "2"
+  },
+  {
+    "name": "颜色",
+    "code": "3"
+  },
+  {
+    "name": "字间距",
+    "code": "4"
+  },
+  {
+    "name": "元素尺寸",
+    "code": "5"
+  },
+  {
+    "name": "元素位置",
+    "code": "6"
+  }
+]
+
+const imagePerms = [
+  {
+    "name": "上传图片",
+    "code": "0"
+  },
+  {
+    "name": "图片尺寸",
+    "code": "1"
+  },
+  {
+    "name": "图片位置",
+    "code": "2"
+  }
+]
+
+const [canvas] = useCanvas();
+const {canvasObject} = storeToRefs(useMainStore());
+
+const handleElement = computed(() => canvasObject.value as CanvasElement);
+
+const eleChange = () => {
+  if (!handleElement.value) return;
+  canvas.renderAll();
+}
+
+</script>
+
+<style lang="scss" scoped>
+.row {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.switch-wrapper {
+  text-align: right;
+}
+
+.slider {
+  flex: 3;
+}
+
+.slider-name {
+  display: flex;
+  align-items: center;
+}
+
+.slider-num {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 77 - 48
src/views/Editor/CanvasRight/ElementStylePanel/ImageStylePanel.vue

@@ -1,11 +1,12 @@
 <template>
   <div class="image-style-panel">
-    <ElementPosition />
-    <el-divider style="margin: 12px 0" />
+    <ElementPermission ele-type="image"/>
+    <ElementPosition/>
+    <el-divider style="margin: 12px 0"/>
 
     <div
-      class="origin-image"
-      :style="{
+        class="origin-image"
+        :style="{
         backgroundImage: `url(${
           handleElement.originSrc
             ? handleElement.originSrc
@@ -14,17 +15,20 @@
       }"
     ></div>
 
-    <ElementFlip />
+    <ElementFlip/>
 
     <el-row class="mt-10">
       <el-col :span="12">
         <el-button-group class="clip-image">
           <el-button class="clip-button" @click="clipImage">
-            <IconTailoring class="btn-icon" /> {{ $t("style.cropImage") }}
+            <IconTailoring class="btn-icon"/>
+            {{ $t("style.cropImage") }}
           </el-button>
           <el-popover trigger="click" width="284">
             <template #reference>
-              <el-button class="clip-popover"><IconDown /></el-button>
+              <el-button class="clip-popover">
+                <IconDown/>
+              </el-button>
             </template>
             <div class="clip">
               <div class="title">{{ $t("style.byShape") }}:</div>
@@ -37,66 +41,74 @@
               <template v-for="type in ratioClipOptions" :key="type.label">
                 <div class="title" v-if="type.label">按{{ type.label }}:</div>
                 <el-button-group class="row">
-                  <el-button style="flex: 1" v-for="item in type.children" :key="item.key" @click="presetImageClip('rect', item.ratio)">{{ item.key }}</el-button>
+                  <el-button style="flex: 1" v-for="item in type.children" :key="item.key"
+                             @click="presetImageClip('rect', item.ratio)">{{ item.key }}
+                  </el-button>
                 </el-button-group>
               </template>
-            </div>    
+            </div>
           </el-popover>
         </el-button-group>
       </el-col>
       <el-col :span="12">
         <el-button class="matting-button" @click="openMatting">
-          <IconMagicWand class="btn-icon" /> 抠图
+          <IconMagicWand class="btn-icon"/>
+          抠图
         </el-button>
       </el-col>
     </el-row>
 
-    <ElementBlend />
-    <el-divider style="margin: 12px 0" />
-    <ElementEffects />
-    <el-divider style="margin: 12px 0" />
-    <ElementMask />
-    <el-divider style="margin: 12px 0" />
-    <ElementFilter />
-    <el-divider style="margin: 12px 0" />
-    <ElementOutline />
-    <el-divider style="margin: 12px 0" />
-    <ElementShadow :hasShadow="hasShadow" />
-    <el-divider style="margin: 12px 0" />
-
-    <el-divider style="margin: 12px 0" />
+    <ElementBlend/>
+    <el-divider style="margin: 12px 0"/>
+    <ElementEffects/>
+    <el-divider style="margin: 12px 0"/>
+    <ElementMask/>
+    <el-divider style="margin: 12px 0"/>
+    <ElementFilter/>
+    <el-divider style="margin: 12px 0"/>
+    <ElementOutline/>
+    <el-divider style="margin: 12px 0"/>
+    <ElementShadow :hasShadow="hasShadow"/>
+    <el-divider style="margin: 12px 0"/>
+
+    <el-divider style="margin: 12px 0"/>
 
     <el-row>
       <FileInput class="full-width-btn" @change="(files: FileList) => replaceImage(files)">
         <el-button class="full-btn">
-          <IconTransform class="btn-icon" />{{ $t("style.replaceimage") }}
+          <IconTransform class="btn-icon"/>
+          {{ $t("style.replaceimage") }}
         </el-button>
       </FileInput>
     </el-row>
     <el-row>
       <el-button class="full-width-btn" @click="resetImage()">
-        <IconUndo class="btn-icon" /> {{ $t("style.resetStyle") }}
-        </el-button>
+        <IconUndo class="btn-icon"/>
+        {{ $t("style.resetStyle") }}
+      </el-button>
     </el-row>
     <el-row>
       <el-button class="full-width-btn" @click="setBackgroundImage()">
-        <IconTheme class="btn-icon" /> {{ $t("style.setAsBg") }}
+        <IconTheme class="btn-icon"/>
+        {{ $t("style.setAsBg") }}
       </el-button>
     </el-row>
-    <ImageMatting :visible="dialogVisible" :image="handleElement.originSrc ? handleElement.originSrc : handleElement.getSrc()" @close="closeMatting" />
+    <ImageMatting :visible="dialogVisible"
+                  :image="handleElement.originSrc ? handleElement.originSrc : handleElement.getSrc()"
+                  @close="closeMatting"/>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed } from "vue";
-import { storeToRefs } from "pinia";
-import { useMainStore, useTemplatesStore } from "@/store";
-import { CLIPPATHS, ClipPathType } from "@/configs/images";
-import { ImageElement } from "@/types/canvas";
-import { ratioClipOptions } from "@/configs/images";
-import { getImageDataURL } from "@/utils/image";
-import { propertiesToInclude } from "@/configs/canvas";
-import { Image } from "fabric";
+import {ref, computed} from "vue";
+import {storeToRefs} from "pinia";
+import {useMainStore, useTemplatesStore} from "@/store";
+import {CLIPPATHS, ClipPathType} from "@/configs/images";
+import {ImageElement} from "@/types/canvas";
+import {ratioClipOptions} from "@/configs/images";
+import {getImageDataURL} from "@/utils/image";
+import {propertiesToInclude} from "@/configs/canvas";
+import {Image} from "fabric";
 import ElementPosition from "../Components/ElementPosition.vue";
 import ElementOutline from "../Components/ElementOutline.vue";
 import ElementShadow from "../Components/ElementShadow.vue";
@@ -106,11 +118,12 @@ import ElementMask from "../Components/ElementMask.vue";
 import ElementBlend from "../Components/ElementBlend.vue";
 import ElementEffects from "../Components/ElementEffects.vue";
 import useCanvas from "@/views/Canvas/useCanvas";
+import ElementPermission from "@/views/Editor/CanvasRight/Components/ElementPermission.vue";
 
 const mainStore = useMainStore();
 const templatesStore = useTemplatesStore();
 const [canvas] = useCanvas();
-const { canvasObject } = storeToRefs(mainStore);
+const {canvasObject} = storeToRefs(mainStore);
 const handleElement = computed(() => canvasObject.value as Image);
 const hasShadow = computed(() => (handleElement.value.shadow ? true : false));
 const dialogVisible = ref(false);
@@ -155,7 +168,7 @@ const presetImageClip = (key: ClipPathType, ratio = 0) => {
   // 形状裁剪(保持当前裁剪范围)
   else {
     // const path = CLIPPATHS[key].createPath(200, 200)
-    handleElement.value.set({ __isCropping: true, _cropKey: key });
+    handleElement.value.set({__isCropping: true, _cropKey: key});
     canvas.renderAll();
   }
 };
@@ -170,13 +183,17 @@ const closeMatting = () => {
 
 // 替换图片(保持当前的样式)
 const replaceImage = (files: FileList) => {
-  const imageFile = files[0];
-  if (!imageFile) return;
-  getImageDataURL(imageFile).then((dataURL) => {
-    const props = { src: dataURL };
-    handleElement.value.setSrc(dataURL);
-    templatesStore.updateElement({ id: handleElement.value.id, props });
-  });
+  console.log(files);
+  // const imageFile = files[0];
+  // if (!imageFile) return;
+  // getImageDataURL(imageFile).then((dataURL) => {
+  //   const props = { src: dataURL };
+  //   handleElement.value.setSrc(dataURL);
+  //   templatesStore.updateElement({ id: handleElement.value.id, props });
+  // });
+  const props = {src: files.fileLink};
+  handleElement.value.setSrc(files.fileLink); //红就红吧type硬赋值了
+  templatesStore.updateElement({id: handleElement.value.id, props});
 };
 
 // 重置图片:清除全部样式
@@ -185,7 +202,7 @@ const resetImage = () => {
   handleElement.value.applyFilters();
   // @ts-ignore
   const props = handleElement.value.toObject(propertiesToInclude) as ImageElement;
-  templatesStore.updateElement({ id: props.id, props });
+  templatesStore.updateElement({id: props.id, props});
 };
 
 // 将图片设置为背景
@@ -209,9 +226,11 @@ const setBackgroundImage = () => {
   align-items: center;
   margin-bottom: 10px;
 }
+
 .switch-wrapper {
   text-align: right;
 }
+
 .origin-image {
   height: 100px;
   background-size: contain;
@@ -220,13 +239,16 @@ const setBackgroundImage = () => {
   background-color: $lightGray;
   margin-bottom: 10px;
 }
+
 .full-width-btn {
   width: 100%;
   margin-bottom: 10px;
 }
+
 .full-btn {
   width: 100%;
 }
+
 .btn-icon {
   margin-right: 3px;
 }
@@ -239,11 +261,13 @@ const setBackgroundImage = () => {
     margin-bottom: 5px;
   }
 }
+
 .shape-clip {
   margin-bottom: 10px;
 
   @include flex-grid-layout();
 }
+
 .shape-clip-item {
   display: flex;
   justify-content: center;
@@ -262,19 +286,24 @@ const setBackgroundImage = () => {
     background-color: #e1e1e1;
   }
 }
+
 .clip-image {
   display: flex;
   flex: 1;
+
   .clip-button {
     width: 70%;
   }
+
   .clip-popover {
     width: 30%;
   }
 }
+
 .matting-button {
   width: 100%;
 }
+
 .mt-10 {
   margin-top: 10px;
 }

+ 2 - 0
src/views/Editor/CanvasRight/ElementStylePanel/TextboxStylePanel.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="text-style-panel">
+    <ElementPermission ele-type="text"/>
     <ElementPosition/>
     <el-divider style="margin: 12px 0"/>
     <el-row>
@@ -269,6 +270,7 @@ import ElementEffects from '../Components/ElementEffects.vue'
 import ElementFill from '../Backgrounds/ElementFill.vue'
 import useHandleCreate from "@/hooks/useHandleCreate"
 import useCanvas from '@/views/Canvas/useCanvas'
+import ElementPermission from "@/views/Editor/CanvasRight/Components/ElementPermission.vue";
 
 
 const mainStore = useMainStore()

+ 6 - 6
src/views/Editor/CanvasRight/ElementStylePanel/index.vue

@@ -5,10 +5,10 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from 'vue'
-import { storeToRefs } from 'pinia'
-import { useMainStore } from '@/store'
-import { ElementNames } from '@/types/elements'
+import {computed} from 'vue'
+import {storeToRefs} from 'pinia'
+import {useMainStore} from '@/store'
+import {ElementNames} from '@/types/elements'
 
 import TextboxStylePanel from './TextboxStylePanel.vue'
 import ImageStylePanel from './ImageStylePanel.vue'
@@ -42,11 +42,11 @@ const panelMap = {
   [ElementNames.ACTIVE]: GroupStylePanel,
 }
 
-const { canvasObject } = storeToRefs(useMainStore())
+const {canvasObject} = storeToRefs(useMainStore())
 
 const currentPanelComponent = computed(() => {
   if (!canvasObject.value) return null
-  console.log('canvasObject:', canvasObject.value.id, canvasObject.value.type)
+  console.log('canvasObject:', canvasObject.value.id, canvasObject.value.type, canvasObject.value)
   const canvasType = canvasObject.value.name ? canvasObject.value.name : canvasObject.value.type
   return panelMap[canvasType.toLowerCase() as ElementNames.TEXT]
 })

+ 2 - 2
src/views/Editor/computer.vue

@@ -9,7 +9,7 @@
 <template>
   <div class="h-full" v-drop-image="{ url: 'UploadUrl', highlightStyle: { backgroundColor: 'lightblue' } }">
     <div :class="[runMode == 1 ? 'layout-content-design': 'layout-content-show', 'flex']">
-      <CanvasLeft v-show="runMode == 1"/>
+      <CanvasLeft/>
       <div :class="runMode == 1 ? 'layout-content-center-design': 'layout-content-center-show'">
         <CanvasHeader v-show="runMode == 1"
                       class="center-header relative flex justify-between py-[10px] text-[14px] select-none h-[39px]"/>
@@ -21,7 +21,7 @@
       <CanvasRight v-show="runMode == 1" class="layout-content-right h-full w-[260px] bg-[#fff] flex flex-col"/>
       <CanvasDom v-show="runMode == 1" class="absolute -z-[200] -left-[300px]"/>
     </div>
-<!--    <CanvasTour v-show="runMode == 1"/>-->
+    <!--    <CanvasTour v-show="runMode == 1"/>-->
   </div>
 </template>