Kaynağa Gözat

pref: 调整组件属性结构

liaojiaxing 10 ay önce
ebeveyn
işleme
ddb9a678f9

+ 3 - 1
README.md

@@ -33,4 +33,6 @@
 ### 图表组件开发思路
 - 图表的option参数归一,提供统一的配置,便于后期做主题配置。
 - 不同的图表提供可以配置的参数,后期通过归一化处理
-- 配置的实现方式两种方式: 1、每个组件提供配置面板,2、根据配置项统一生成配置面板,通过配置项配置组件。
+- 组件配置:组件内容、组件样式
+-- 组件样式为容器样式,包括:组件属性(如宽高、xy坐标、整体透明度等)、样式属性(如背景、边框、阴影等)
+-- 组件内容为每个组件单独提供的配置组件,通常为数据源,数据样式等

+ 66 - 61
src/components/Charts/Bar/BasicBar/src/props.ts

@@ -1,5 +1,5 @@
-import { type PropType } from 'vue';
-import { EChartsOption } from 'echarts';
+import { type PropType } from "vue";
+import { EChartsOption } from "echarts";
 
 export const basicLineProps = {
   width: {
@@ -12,11 +12,11 @@ export const basicLineProps = {
   },
   // 标题
   title: {
-    type: Object as PropType<EChartsOption['title']>,
+    type: Object as PropType<EChartsOption["title"]>,
   },
   // 图例
   legend: {
-    type: Object as PropType<EChartsOption['legend']>
+    type: Object as PropType<EChartsOption["legend"]>,
   },
   // 背景
   backgroundColor: {
@@ -24,83 +24,88 @@ export const basicLineProps = {
   },
   // 边框
   grid: {
-    type: Object as PropType<EChartsOption['grid']>,
+    type: Object as PropType<EChartsOption["grid"]>,
   },
   // 提示框
   tooltip: {
-    type: Object as PropType<EChartsOption['tooltip']>,
+    type: Object as PropType<EChartsOption["tooltip"]>,
   },
   // x轴数据
   xAxis: {
-    type: Object as PropType<EChartsOption['xAxis']>,
+    type: Object as PropType<EChartsOption["xAxis"]>,
   },
   // y轴数据
   yAxis: {
-    type: Object as PropType<EChartsOption['yAxis']>,
+    type: Object as PropType<EChartsOption["yAxis"]>,
   },
   // 折线
   series: {
-    type: Array as PropType<EChartsOption['series']>,
+    type: Array as PropType<EChartsOption["series"]>,
   },
   // 数据集
   dataset: {
-    type: Object as PropType<EChartsOption['dataset']>,
+    type: Object as PropType<EChartsOption["dataset"]>,
   },
 };
 
 export const defaultPropsValue: EChartsOption = {
-  width: 400,
-  height: 260,
-  title: {
-    text: '柱状图标题',
-    left: 'center',
-    top: 8,
-    textStyle: {
-      color: '#fff',
-      fontSize: 16
-    }
-  },
-  legend: {
-    textStyle: {
-      color: '#fff',
+  container: {
+    props: {
+      width: 400,
+      height: 260,
     },
-    top: 32
-  },
-  grid: {
-    bottom: 34,
-    right: 20,
-    top: 60
   },
-  backgroundColor: 'rgba(0,0,0,0.1)',
-  tooltip: {},
-  xAxis: {
-    type: 'category',
-    axisLabel: {
-      color: '#9fadbf'
-    }
-  },
-  yAxis: {
-    axisLabel: {
-      color: '#9fadbf'
+  props: {
+    title: {
+      text: "柱状图标题",
+      left: "center",
+      top: 8,
+      textStyle: {
+        color: "#fff",
+        fontSize: 16,
+      },
     },
-    splitLine: {
-      lineStyle: {
-        type: 'dashed',
-        color: '#36485f'
-      }
-    }
-  },
-  series: [
-    { type: 'bar', itemStyle: { color: '#1890ff' }},
-    { type: 'bar', itemStyle: { color: '#2fc25b' }}
-  ],
-  dataset: {
-    dimensions: ['serie', '系列1', '系列2'],
-    source: [
-      { serie: '轴标签A', '系列1': 43.3, '系列2': 85.8 },
-      { serie: '轴标签B', '系列1': 43.3, '系列2': 85.8 },
-      { serie: '轴标签C', '系列1': 43.3, '系列2': 85.8 },
-      { serie: '轴标签D', '系列1': 43.3, '系列2': 85.8 },
+    legend: {
+      textStyle: {
+        color: "#fff",
+      },
+      top: 32,
+    },
+    grid: {
+      bottom: 34,
+      right: 20,
+      top: 60,
+    },
+    tooltip: {},
+    xAxis: {
+      type: "category",
+      axisLabel: {
+        color: "#9fadbf",
+      },
+    },
+    yAxis: {
+      axisLabel: {
+        color: "#9fadbf",
+      },
+      splitLine: {
+        lineStyle: {
+          type: "dashed",
+          color: "#36485f",
+        },
+      },
+    },
+    series: [
+      { type: "bar", itemStyle: { color: "#1890ff" } },
+      { type: "bar", itemStyle: { color: "#2fc25b" } },
     ],
-  }
-}
+    dataset: {
+      dimensions: ["serie", "系列1", "系列2"],
+      source: [
+        { serie: "轴标签A", 系列1: 43.3, 系列2: 85.8 },
+        { serie: "轴标签B", 系列1: 43.3, 系列2: 85.8 },
+        { serie: "轴标签C", 系列1: 43.3, 系列2: 85.8 },
+        { serie: "轴标签D", 系列1: 43.3, 系列2: 85.8 },
+      ],
+    },
+  },
+};

+ 2 - 12
src/components/Charts/Echarts.vue

@@ -1,16 +1,14 @@
 <!-- echarts基础组件 -->
 <template>
-  <div ref="chartRef" :style="style"></div>
+  <div ref="chartRef" style="width: 100%; height: 100%;"></div>
 </template>
 
 <script setup lang="ts">
-import { ref, Ref, defineProps, watch, nextTick, computed } from "vue";
+import { ref, Ref, defineProps, watch, nextTick } from "vue";
 import { useEcharts } from "@/hooks/useEcharts";
 import type { EChartsOption } from "echarts";
 import { throttle } from "lodash";
-import { useProjectStore } from "@/store/modules/project";
 
-const projectStore = useProjectStore();
 const props = defineProps<{
   echartsOptions: EChartsOption;
   width: number;
@@ -19,14 +17,6 @@ const props = defineProps<{
 const chartRef: Ref<null | HTMLDivElement> = ref(null);
 const { setOptions, resize } = useEcharts(chartRef as Ref<HTMLDivElement>);
 
-const style = computed(() => {
-  const { width, height } = props;
-  return {
-    width: `${width}px`,
-    height: `${height}px`,
-  };
-});
-
 watch(
   () => [
     props.width,

+ 82 - 61
src/components/Charts/Line/BasicLine/src/props.ts

@@ -1,5 +1,5 @@
-import { type PropType } from 'vue';
-import { EChartsOption } from 'echarts';
+import { type PropType } from "vue";
+import { EChartsOption } from "echarts";
 
 export const basicLineProps = {
   width: {
@@ -12,11 +12,11 @@ export const basicLineProps = {
   },
   // 标题
   title: {
-    type: Object as PropType<EChartsOption['title']>,
+    type: Object as PropType<EChartsOption["title"]>,
   },
   // 图例
   legend: {
-    type: Object as PropType<EChartsOption['legend']>
+    type: Object as PropType<EChartsOption["legend"]>,
   },
   // 背景
   backgroundColor: {
@@ -24,83 +24,104 @@ export const basicLineProps = {
   },
   // 边框
   grid: {
-    type: Object as PropType<EChartsOption['grid']>,
+    type: Object as PropType<EChartsOption["grid"]>,
   },
   // 提示框
   tooltip: {
-    type: Object as PropType<EChartsOption['tooltip']>,
+    type: Object as PropType<EChartsOption["tooltip"]>,
   },
   // x轴数据
   xAxis: {
-    type: Object as PropType<EChartsOption['xAxis']>,
+    type: Object as PropType<EChartsOption["xAxis"]>,
   },
   // y轴数据
   yAxis: {
-    type: Object as PropType<EChartsOption['yAxis']>,
+    type: Object as PropType<EChartsOption["yAxis"]>,
   },
   // 折线
   series: {
-    type: Array as PropType<EChartsOption['series']>,
+    type: Array as PropType<EChartsOption["series"]>,
   },
   // 数据集
   dataset: {
-    type: Object as PropType<EChartsOption['dataset']>,
+    type: Object as PropType<EChartsOption["dataset"]>,
   },
 };
 
 export const defaultPropsValue: EChartsOption = {
-  width: 400,
-  height: 260,
-  title: {
-    text: '折线图标题',
-    left: 'center',
-    top: 8,
-    textStyle: {
-      color: '#fff',
-      fontSize: 16
-    }
-  },
-  legend: {
-    textStyle: {
-      color: '#fff',
+  container: {
+    props: {
+      width: 400,
+      height: 260,
     },
-    top: 32
-  },
-  grid: {
-    bottom: 34,
-    right: 20,
-    top: 60
   },
-  backgroundColor: 'rgba(0,0,0,0.1)',
-  tooltip: {},
-  xAxis: {
-    type: 'category',
-    axisLabel: {
-      color: '#9fadbf'
-    }
-  },
-  yAxis: {
-    axisLabel: {
-      color: '#9fadbf'
+  props: {
+    title: {
+      text: "折线图标题",
+      left: "center",
+      top: 8,
+      textStyle: {
+        color: "#fff",
+        fontSize: 16,
+      },
     },
-    splitLine: {
-      lineStyle: {
-        type: 'dashed',
-        color: '#36485f'
-      }
-    }
-  },
-  series: [
-    { type: 'line', itemStyle: { color: '#1890ff' }},
-    { type: 'line', itemStyle: { color: '#2fc25b' }}
-  ],
-  dataset: {
-    dimensions: ['month', 'city', 'city2'],
-    source: [
-      { month: '纵轴A', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '纵轴B', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '纵轴C', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
-      { month: '纵轴D', city: Math.floor(Math.random() * 100), city2: Math.floor(Math.random() * 100) },
+    legend: {
+      textStyle: {
+        color: "#fff",
+      },
+      top: 32,
+    },
+    grid: {
+      bottom: 34,
+      right: 20,
+      top: 60,
+    },
+    tooltip: {},
+    xAxis: {
+      type: "category",
+      axisLabel: {
+        color: "#9fadbf",
+      },
+    },
+    yAxis: {
+      axisLabel: {
+        color: "#9fadbf",
+      },
+      splitLine: {
+        lineStyle: {
+          type: "dashed",
+          color: "#36485f",
+        },
+      },
+    },
+    series: [
+      { type: "line", itemStyle: { color: "#1890ff" } },
+      { type: "line", itemStyle: { color: "#2fc25b" } },
     ],
-  }
-}
+    dataset: {
+      dimensions: ["month", "city", "city2"],
+      source: [
+        {
+          month: "纵轴A",
+          city: Math.floor(Math.random() * 100),
+          city2: Math.floor(Math.random() * 100),
+        },
+        {
+          month: "纵轴B",
+          city: Math.floor(Math.random() * 100),
+          city2: Math.floor(Math.random() * 100),
+        },
+        {
+          month: "纵轴C",
+          city: Math.floor(Math.random() * 100),
+          city2: Math.floor(Math.random() * 100),
+        },
+        {
+          month: "纵轴D",
+          city: Math.floor(Math.random() * 100),
+          city2: Math.floor(Math.random() * 100),
+        },
+      ],
+    },
+  },
+};

+ 0 - 0
src/components/CusForm/BackgroundSelect.vue


+ 12 - 8
src/components/Text/Title/src/index.vue

@@ -1,21 +1,25 @@
 <template>
   <div class="cus-title" :style="style">
-    {{ title }}
+    {{ text }}
   </div>
 </template>
 
 <script setup lang="ts">
 import { defineProps, computed } from 'vue';
 import { titleProps } from './props';
+import { transformStyle } from '@/utils/style';
 
 const props = defineProps(titleProps);
-const style = computed(() => ({
-  ...props,
-  width: props.width + 'px',
-  height: props.height + 'px',
-  fontSize: props.fontSize + 'px',
-  lineHeight: props.height + 'px',
-}));
+const style = computed(() => {
+  const style = transformStyle(props);
+
+  return {
+    ...style,
+    width: '100%',
+    height: '100%',
+    lineHeight: props.height + 'px'
+  }
+});
 </script>
 
 <style lang="less" scoped>

+ 20 - 7
src/components/Text/Title/src/props.ts

@@ -1,7 +1,7 @@
 import { PropType } from 'vue';
 
 export const titleProps = {
-  title: {
+  text: {
     type: String,
     required: true
   },
@@ -32,10 +32,23 @@ export const titleProps = {
 }
 
 export const defaultPropsValue = {
-  title: '标题',
-  width: 300,
-  height: 80,
-  fontSize: 24,
-  color: '#fff',
-  backgroundColor: 'none',
+  container: {
+    style: {
+      background: {
+        type: 'none'
+      }
+    },
+    props: {
+      width: 300,
+      height: 80,
+    }
+  },
+  props: {
+    text: '标题内容',
+    fontSize: 24,
+    color: '#fff',
+    fontWeight: 'bold',
+    textAlign: 'left',
+    direction: 'horizontal'
+  }
 }

+ 1 - 1
src/components/index.ts

@@ -1,4 +1,4 @@
-// 导出components文件夹下所有组件
+// 暴露大屏组件资源
 const allComponents = {
   Title: () => import("@/components/Text/Title"),
   BasicLine: () => import("@/components/Charts/Line/BasicLine"),

+ 76 - 45
src/store/modules/project.ts

@@ -1,9 +1,10 @@
-import { defineStore } from "pinia";
 import type { ProjectInfo, Page, ReferLine, CustomElement } from "#/project";
 import type { ComponentType } from "@/components";
-import componentAll from '@/components';
+import { defineStore } from "pinia";
+import componentAll from "@/components";
 import { ScreenFillEnum } from "@/enum/screenFillEnum";
-import { update } from "lodash";
+import { update, defaultsDeep } from "lodash";
+import { getNormalizedContainer } from "@/utils/common";
 
 type ProjectState = {
   projectInfo: ProjectInfo;
@@ -12,48 +13,48 @@ type ProjectState = {
     key: number;
     name: string;
     componentType: ComponentType;
-    position: {
-      x: number;
-      y: number;
+    container: {
+      style: Record<string, any>;
+      props: Record<string, any>;
     };
   } | null;
-  mode: 'edit' | 'preview';
+  mode: "edit" | "preview";
   selectedElementKeys: number[];
-}
+};
 const defaultPage: Page = {
-  name: '页面1',
+  name: "页面1",
   background: {
-    type: 'color',
-    color: '#0b074b',
-    image: '',
-    fillType: ''
+    type: "color",
+    color: "#0b074b",
+    image: "",
+    fillType: "",
   },
   elements: [],
-  referLines: []
-}
-const CURRENT_PROJECT = 'currentProject';
+  referLines: [],
+};
+const CURRENT_PROJECT = "currentProject";
 
 export const useProjectStore = defineStore({
-  id: 'project',
+  id: "project",
   state: (): ProjectState => ({
     // 项目信息
     projectInfo: {
-      name: '',
-      description: '',
-      sizeType: '',
+      name: "",
+      description: "",
+      sizeType: "",
       width: 0,
       height: 0,
       fillType: ScreenFillEnum.Auto,
-      pages: [{ ...defaultPage}]
+      pages: [{ ...defaultPage }],
     },
     // 当前编辑页面索引
     activePageIndex: 0,
     // 添加组件临时数据
     addCompData: null,
     // 视图模式
-    mode: 'edit',
+    mode: "edit",
     // 选中的元素
-    selectedElementKeys: []
+    selectedElementKeys: [],
   }),
   getters: {
     referLines(state) {
@@ -64,7 +65,7 @@ export const useProjectStore = defineStore({
     },
     currentPage(state) {
       return state.projectInfo.pages[state.activePageIndex];
-    }
+    },
   },
   actions: {
     setProjectInfo(info: any) {
@@ -72,7 +73,7 @@ export const useProjectStore = defineStore({
       localStorage.setItem(CURRENT_PROJECT, JSON.stringify(info));
     },
     getCurrentProjectInfo(): ProjectInfo | undefined {
-      const info = JSON.parse(localStorage.getItem(CURRENT_PROJECT) || '');
+      const info = JSON.parse(localStorage.getItem(CURRENT_PROJECT) || "");
       this.setProjectInfo(info as unknown as ProjectInfo);
       return info;
     },
@@ -81,7 +82,11 @@ export const useProjectStore = defineStore({
     },
     removeReferLine(key: number) {
       const index = this.referLines.findIndex((line) => line.key === key);
-      index !== -1 && this.projectInfo.pages[this.activePageIndex].referLines.splice(index, 1);
+      index !== -1 &&
+        this.projectInfo.pages[this.activePageIndex].referLines.splice(
+          index,
+          1
+        );
     },
     updateReferLine(line: ReferLine) {
       const index = this.referLines.findIndex((l) => l.key === line.key);
@@ -90,37 +95,61 @@ export const useProjectStore = defineStore({
       }
     },
     // 添加组件
-    async addElement(element: CustomElement) {
+    async addElement(element: any) {
       this.addCompData = null;
-      if(!element) return;
+      if (!element) return;
 
       const elements = this.projectInfo.pages[this.activePageIndex].elements;
-      // 获取组件默认属性
-      const { defaultPropsValue } = await componentAll[element.componentType]?.() || {};
-      const index = elements.filter(item => item.componentType === element.componentType).length + 1;
-      const width = defaultPropsValue?.width || 400;
-      const height = defaultPropsValue?.height || 260;
+      // 获取每个自定义组件暴露出来的属性
+      const { defaultPropsValue } =
+        (await componentAll[element.componentType as ComponentType]?.()) || {};
+      const { width = 400, height = 260 } = defaultPropsValue?.container?.props  || {};
+      const { props: containerProps = {}, style = {} } = defaultPropsValue?.container || {};
 
+      const index =
+        elements.filter((item) => item.componentType === element.componentType)
+          .length + 1;
+
+      const { x, y } = element.container.props;
+      const container = getNormalizedContainer({
+        style,
+        props: {
+          ...containerProps,
+          width,
+          height,
+          x: x - width / 2,
+          y: y - height / 2,
+        },
+      });
+      // 添加组件
       this.projectInfo.pages[this.activePageIndex].elements.push({
-        ...element,
+        ...defaultsDeep(element, defaultPropsValue),
         name: element.name + index,
         zIndex: elements.length + 1,
-        props: defaultPropsValue,
         visible: true,
         locked: false,
-        position: {
-          x: element.position.x - (width / 2),
-          y: element.position.y - (height / 2)
-        }
+        container,
       });
 
       this.selectedElementKeys = [element.key];
     },
     // 更新组件
     updateElement(key: number, path: string, payload: any) {
-      const element = this.projectInfo.pages[this.activePageIndex].elements.find((item) => item.key === key);
+      const element = this.projectInfo.pages[
+        this.activePageIndex
+      ].elements.find((item) => item.key === key);
       // 如果是锁定状态不能修改宽高 位置
-      if(element && element.locked && ['props.width', 'props.height', 'position'].includes(path)) return;
+      if (
+        element &&
+        element.locked &&
+        [
+          "container.props.width",
+          "container.props.height",
+          "container.props.x",
+          "container.props.y",
+        ].includes(path)
+      )
+        return;
 
       if (element) {
         update(element, path, () => payload);
@@ -128,7 +157,9 @@ export const useProjectStore = defineStore({
     },
     // 删除组件
     removeElement(key: number) {
-      const index = this.projectInfo.pages[this.activePageIndex].elements.findIndex((item) => item.key === key);
+      const index = this.projectInfo.pages[
+        this.activePageIndex
+      ].elements.findIndex((item) => item.key === key);
       if (index !== -1) {
         this.projectInfo.pages[this.activePageIndex].elements.splice(index, 1);
       }
@@ -141,7 +172,7 @@ export const useProjectStore = defineStore({
     clearAddCompData() {
       this.addCompData = null;
     },
-    setMode(mode: 'edit' | 'preview') {
+    setMode(mode: "edit" | "preview") {
       this.mode = mode;
     },
     // 设置选中的元素
@@ -152,5 +183,5 @@ export const useProjectStore = defineStore({
     clearAllSelectedElement() {
       this.selectedElementKeys = [];
     },
-  }
-});
+  },
+});

+ 66 - 0
src/utils/common.ts

@@ -0,0 +1,66 @@
+import { defaultsDeep } from "lodash";
+
+const containerSetting = {
+  /* ===================================== 通用容器样式 ============================================ */
+  style: {
+    background: {
+      type: "color", // none, color, image
+      color: "rgba(0, 0, 0, 0.1)",
+      image: "",
+      fillType: "",
+    },
+    border: {
+      type: "none",
+      color: "#000",
+      width: 0,
+    },
+    borderRadius: {
+      type: "all", // all, custom
+      value: 2, // 整体圆角值
+      unit: "px", // 单位
+      units: ["px", "%"], // 单位列表
+      topLeft: 0,
+      topRight: 0,
+      bottomLeft: 0,
+      bottomRight: 0,
+    },
+    boxShadow: {
+      enabled: false,
+      color: "#000",
+      offsetX: 0,
+      offsetY: 0,
+      blurRadius: 0,
+      spreadRadius: 0,
+      inset: false,
+    },
+    backdropFilter: {
+      enabled: false,
+      blur: 0,
+    }
+  },
+  /* ===================================== 通用容器属性 ============================================ */
+  props: {
+    width: 0,
+    height: 0,
+    x: 0,
+    y: 0,
+    paddingLeft: 0,
+    paddingRight: 0,
+    paddingTop: 0,
+    paddingBottom: 0,
+    rotateX: 0,
+    rotateY: 0,
+    rotateZ: 0,
+    opacity: 1,
+  },
+};
+
+/**
+ * 获取归一化的容器组件属性
+ *
+ *  @param {Record<string, any>} config - 传入属性
+ *  @returns {Record<string, any>} - 返回归一化后的属性
+ */
+export function getNormalizedContainer(config: Record<string, any>) {
+  return defaultsDeep(config, containerSetting);
+}

+ 48 - 0
src/utils/style.ts

@@ -0,0 +1,48 @@
+/**
+ * 转换样式对象
+ * @param style 
+ */
+export function transformStyle(style: Record<string, any>) {
+  const styleObj: Record<string, any> = {};
+  for (const key in style) {
+    if(typeof style[key] === 'object') {
+      switch(key) {
+        // 背景色对象
+          case 'background':
+            if(style[key].type === 'none') styleObj[key] = 'none';
+            if(style[key].type === 'color') styleObj[key] = style[key].color;
+            if(style[key].type === 'image') {
+              styleObj[key+'-image'] = `url(${style[key].image}) no-repeat center center`;
+              styleObj[key+'-size'] = style[key].fillType;
+            };
+            break;
+        // 颜色对象
+        case 'border':
+          if(style[key].type === 'none') styleObj[key] = 'none';
+          else styleObj[key] = `${style[key].width}px ${style[key].type} ${style[key].color}`;
+          break;
+        // 边框对象
+        case 'borderRadius':
+          if(style[key].type === 'all') styleObj[key] = `${style[key].value}${style[key].unit}`;
+          else styleObj[key] = `${style[key].topLeft}${style[key].unit} ${style[key].topRight}${style[key].unit} ${style[key].bottomLeft}${style[key].unit} ${style[key].bottomRight}${style[key].unit}`;
+          break;
+        // 阴影对象
+        case 'boxShadow':
+          if(style[key].enabled) {
+            styleObj[key] = `${style[key].offsetX}px ${style[key].offsetY}px ${style[key].blurRadius}px ${style[key].spreadRadius}px ${style[key].color} ${style[key].inset ? 'inset' : ''}`;
+          }
+          break;
+        // 毛玻璃对象
+        case 'backdropFilter':
+          if(style[key].enabled) styleObj[key] = `blur(${style[key].blur}px)`;
+          break;
+      }
+    } else if(typeof style[key] === 'number') {
+      styleObj[key] = style[key] + 'px';
+    } else if(typeof style[key] === 'string') {
+      styleObj[key] = style[key];
+    }
+  }
+
+  return styleObj;
+}

+ 11 - 7
src/views/designer/component/ComponentLibary.vue

@@ -133,10 +133,12 @@ const handleAddComp = (item: CompItem) => {
     key: Date.now(),
     name: item.name,
     componentType: item.componetName,
-    position: {
-      x: stageStore.width / 2,
-      y: stageStore.height / 2,
-    },
+    container: {
+      props: {
+        x: stageStore.width / 2,
+        y: stageStore.height / 2,
+      }
+    }
   };
 
   projectStore.addElement(compData);
@@ -153,9 +155,11 @@ const handleDragStart = (item: CompItem) => {
     key: Date.now(),
     name: item.name,
     componentType: item.componetName,
-    positon: {
-      x: 0,
-      y: 0,
+    container: {
+      props: {
+        x: 0,
+        y: 0,
+      }
     }
   })
 };

+ 58 - 25
src/views/designer/component/ComponentWrapper.vue

@@ -5,7 +5,12 @@
     :style="warpperStyle"
   >
     <div class="component-content" @click="handleSelectComponent">
-      <component :is="component" v-bind="componentData.props" />
+      <component
+        :is="component"
+        v-bind="componentData.props"
+        :width="componentData.container.props.width"
+        :height="componentData.container.props.height"
+      />
     </div>
     <div v-if="showEditBox" class="edit-box" :style="editWapperStyle">
       <span class="name-tip">{{ getTip }}</span>
@@ -16,16 +21,21 @@
         @start="handleDragStart"
         @end="handleDragEnd"
       >
-        <span v-if="!componentData.locked" class="edit-box-point" :class="item"></span>
+        <span
+          v-if="!componentData.locked"
+          class="edit-box-point"
+          :class="item"
+        ></span>
       </UseDraggable>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
+import type { CustomElement } from "#/project";
 import { defineProps, defineAsyncComponent, computed, ref } from "vue";
 import componentAll from "@/components";
-import type { CustomElement } from "#/project";
+import { transformStyle } from "@/utils/style";
 
 import { useStageStore } from "@/store/modules/stage";
 import { useProjectStore } from "@/store/modules/project";
@@ -41,7 +51,8 @@ const componentWrapperRef = ref<HTMLElement | null>(null);
 const stageStore = useStageStore();
 const projectStore = useProjectStore();
 const editWapperStyle = computed(() => {
-  const { width = 400, height = 260 } = componentData.props || {};
+  const { width = 400, height = 260 } = componentData.container.props || {};
+
   return {
     transform: `scale(${1 / stageStore.scale})`,
     transformOrigin: "50% 50%",
@@ -52,14 +63,26 @@ const editWapperStyle = computed(() => {
     top: (height / 2) * (1 - stageStore.scale) + "px",
   };
 });
+
 const warpperStyle = computed(() => {
-  const { width = 400, height = 260 } = componentData.props || {};
-  const { position } = componentData || {};
+  const {
+    width = 400,
+    height = 260,
+    x,
+    y,
+    paddingLeft = 0,
+    paddingRight = 0,
+    paddingTop = 0,
+    paddingBottm = 0,
+  } = componentData.container.props || {};
+  const style = transformStyle(componentData.container?.style || {});
+  
   return {
+    ...style,
     width: `${width}px`,
     height: `${height}px`,
-    left: position.x + "px",
-    top: position.y + "px",
+    left: x + "px",
+    top: y + "px",
   };
 });
 // 是否显示编辑框
@@ -71,9 +94,10 @@ const showEditBox = computed(() => {
 });
 // 获取提示信息
 const getTip = computed(() => {
-  return showNameTip.value 
-  ? componentData.name
-  : `x: ${Math.round(componentData.position.x)} y: ${Math.round(componentData.position.y)}`;
+  const { x, y } = componentData.container.props || {};
+  return showNameTip.value
+    ? componentData.name
+    : `x: ${Math.round(x)} y: ${Math.round(y)}`;
 });
 
 let isPointDragFlag = false;
@@ -85,13 +109,20 @@ useDraggable(componentWrapperRef, {
 
     const originPosition = componentWrapperRef.value!.getBoundingClientRect();
     // 计算移动的距离
-    const x = position.x - originPosition.left;
-    const y = position.y - originPosition.top;
+    const xMoveLength = position.x - originPosition.left;
+    const yMoveLentgh = position.y - originPosition.top;
+    const { x, y } = componentData.container.props || {};
 
-    projectStore.updateElement(componentData.key, "position", {
-      x: componentData.position.x + x,
-      y: componentData.position.y + y,
-    });
+    projectStore.updateElement(
+      componentData.key,
+      "container.props.x",
+      x + xMoveLength
+    );
+    projectStore.updateElement(
+      componentData.key,
+      "container.props.y",
+      y + yMoveLentgh
+    );
   },
   onStart: () => {
     projectStore.setSelectedElementKeys([componentData.key]);
@@ -99,7 +130,7 @@ useDraggable(componentWrapperRef, {
   },
   onEnd: () => {
     showNameTip.value = true;
-  }
+  },
 });
 const handleSelectComponent = () => {
   projectStore.setSelectedElementKeys([componentData.key]);
@@ -126,10 +157,7 @@ const handleDragPoint = (type: string, e: PointerEvent) => {
   const moveX = (e.x - startPoint.x) / stageStore.scale;
   const moveY = (e.y - startPoint.y) / stageStore.scale;
 
-  let width = componentData.props.width;
-  let height = componentData.props.height;
-  let x = componentData.position.x;
-  let y = componentData.position.y;
+  let { x, y, width, height } = componentData.container.props || {};
 
   switch (type) {
     case "top-left":
@@ -173,9 +201,14 @@ const handleDragPoint = (type: string, e: PointerEvent) => {
 
   if (width < 10 || height < 10) return;
 
-  projectStore.updateElement(componentData.key, "position", { x, y });
-  projectStore.updateElement(componentData.key, "props.width", width);
-  projectStore.updateElement(componentData.key, "props.height", height);
+  projectStore.updateElement(componentData.key, "container.props.x", x);
+  projectStore.updateElement(componentData.key, "container.props.y", y);
+  projectStore.updateElement(componentData.key, "container.props.width", width);
+  projectStore.updateElement(
+    componentData.key,
+    "container.props.height",
+    height
+  );
 };
 // 拖拽点开始
 const handleDragStart = (_, e: PointerEvent) => {

+ 19 - 6
src/views/designer/component/Configurator.vue

@@ -1,18 +1,31 @@
 <template>
   <div>
     <Tabs type="card">
-      <TabPane key="1" tab="组件">
-        <div>基本配置</div>
-      </TabPane>
-      <TabPane key="2" tab="样式">
-        <div>样式配置</div>
-      </TabPane>
+      <!-- 页面设置 -->
+       <template v-if="projectStore.selectedElementKeys.length === 0">
+        <TabPane key="0" tab="页面">
+          <div>页面设置</div>
+        </TabPane>
+       </template>
+      <!-- 组件设置 -->
+      <template v-else>
+        <TabPane key="1" tab="组件">
+          <div>基本配置</div>
+        </TabPane>
+        <TabPane key="2" tab="样式">
+          <div>样式配置</div>
+        </TabPane>
+      </template>
     </Tabs>
   </div>
 </template>
 
 <script setup lang="ts">
 import { Tabs, TabPane } from 'ant-design-vue';
+import { useProjectStore } from '@/store/modules/project';
+
+const projectStore = useProjectStore();
+
 </script>
 
 <style scoped>

+ 7 - 5
src/views/designer/component/Stage.vue

@@ -156,16 +156,18 @@ const handleDrop = (e: DragEvent) => {
       key: Date.now(),
       name: projectStore.addCompData.name,
       componentType: projectStore.addCompData.componentType,
-      position: {
-        x: offsetX,
-        y: offsetY,
-      },
+      container: {
+        props: {
+          x: offsetX,
+          y: offsetY,
+        }
+      }
     };
     projectStore.addElement(compData); 
   }
 };
 
-// 点击画布,处理组件选中状态
+// 点击画布,移除所有组件选中状态
 const handleCanvasClick = (e: MouseEvent) => {
   if(
     !e?.target?.closest(".edit-box") 

+ 2 - 2
src/views/designer/component/Workspace.vue

@@ -7,7 +7,7 @@
     <Flex class="workspace-bottom" justify="space-between" align="center">
       <div class="bottom-left">
         <span style="margin-right: 12px"
-          >画布尺寸:{{ projectStore.width }}*{{ projectStore.height }}px</span
+          >画布尺寸:{{ projectStore.projectInfo.width }}*{{ projectStore.projectInfo.height }}px</span
         >
         <span>画布自适应:<Select size="small" style="width: 120px" /></span>
       </div>
@@ -61,7 +61,7 @@ const sizeOptions = [
   { value: 0, label: "适应大小" }
 ];
 
-const handleSizeChange = (val: string | number) => {
+const handleSizeChange = (val: any) => {
   if(Number.isFinite(val)) {
     stageStore.setScale(val as number);
   } 

+ 9 - 9
types/project.d.ts

@@ -17,25 +17,25 @@ declare interface CustomElement {
   name: string;
   // 组件类型
   componentType: ComponentType;
-  // 元素位置
-  position: {
-    x: number;
-    y: number;
-  };
   // 元素层级
   zIndex: number;
   // 是否可见
   visible: boolean;
   // 是否锁定
   locked: boolean;
-  // 元素属性 -- 包含样式,组件属性
-  props: Record<string, any>;
+  // 容器样式 -- 包含样式,组件属性
+  container: {
+    // 元素样式
+    style: Record<string, any>;
+    // 元素属性
+    props: Record<string, any>;
+  };
   // 元素交互
   events: Record<string, any>;
   // 元素动画
   animations: Record<string, any>;
-  // 元素内容 -- 数据源
-  content: Record<string, any>;
+  // 组件内容 -- 数据源, 数据样式等
+  props: Record<string, any>;
 }
 
 declare export interface ReferLine {