Sfoglia il codice sorgente

feat: 添加表格控件

jiaxing.liao 1 giorno fa
parent
commit
52924c10c7

+ 2 - 1
src/renderer/src/locales/en_US.json

@@ -98,5 +98,6 @@
   "bar": "Bar",
   "slider": "Slider",
   "tabview": "Tabview",
-  "tileview": "Tileview"
+  "tileview": "Tileview",
+  "table": "Table"
 }

+ 2 - 1
src/renderer/src/locales/zh_CN.json

@@ -98,5 +98,6 @@
   "bar": "进度条",
   "slider": "滑动条",
   "tabview": "选项卡",
-  "tileview": "平铺视图"
+  "tileview": "平铺视图",
+  "table": "表格"
 }

+ 10 - 0
src/renderer/src/lvgl-widgets/assets/icon/icon_addcol.svg

@@ -0,0 +1,10 @@
+<svg width="80" height="52" viewBox="0 0 80 52" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="12" y="46" width="40" height="12" rx="2" transform="rotate(-90 12 46)" fill="#CACACA"/>
+<rect x="27" y="46" width="40" height="12" rx="2" transform="rotate(-90 27 46)" fill="#CACACA"/>
+<rect x="42" y="46" width="40" height="12" rx="2" transform="rotate(-90 42 46)" fill="#CACACA"/>
+<mask id="path-4-inside-1_107_71" fill="white">
+<rect x="57" y="46" width="40" height="12" rx="1" transform="rotate(-90 57 46)"/>
+</mask>
+<rect x="57" y="46" width="40" height="12" rx="1" transform="rotate(-90 57 46)" stroke="#CACACA" stroke-width="2.6" stroke-dasharray="2 2" mask="url(#path-4-inside-1_107_71)"/>
+<path d="M62.56 25.89H63.5V27.94H65.55V28.88H63.5V30.94H62.56V28.88H60.5V27.94H62.56V25.89Z" fill="#CACACA"/>
+</svg>

+ 9 - 0
src/renderer/src/lvgl-widgets/assets/icon/icon_addrow.svg

@@ -0,0 +1,9 @@
+<svg width="80" height="52" viewBox="0 0 80 52" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="11" y="7" width="57" height="10" rx="2" fill="#CACACA"/>
+<rect x="11" y="21" width="57" height="10" rx="2" fill="#CACACA"/>
+<mask id="path-3-inside-1_107_2" fill="white">
+<rect x="11" y="35" width="57" height="10" rx="1"/>
+</mask>
+<rect x="11" y="35" width="57" height="10" rx="1" stroke="#CACACA" stroke-width="2.6" stroke-dasharray="2 2" mask="url(#path-3-inside-1_107_2)"/>
+<path d="M38.56 37.89H39.5V39.94H41.55V40.88H39.5V42.94H38.56V40.88H36.5V39.94H38.56V37.89Z" fill="#CACACA"/>
+</svg>

+ 3 - 1
src/renderer/src/lvgl-widgets/index.ts

@@ -14,6 +14,7 @@ import Slider from './slider'
 import Container from './container'
 import Tabview from './tabview/index'
 import Tileview from './tileview/index'
+import Table from './table/index'
 
 import Page from './page'
 import { IComponentModelConfig } from './type'
@@ -36,7 +37,8 @@ export const ComponentArray = [
 
   Container,
   Tabview,
-  Tileview
+  Tileview,
+  Table
 ]
 
 const componentMap: { [key: string]: IComponentModelConfig } = ComponentArray.reduce((acc, cur) => {

+ 138 - 0
src/renderer/src/lvgl-widgets/table/Config.vue

@@ -0,0 +1,138 @@
+<template>
+  <el-card class="mb-12px" body-class="p-8px!">
+    <template #header>
+      <div class="flex items-center justify-between">
+        <span> 单元格 </span>
+        <span>
+          <el-tooltip content="添加一行">
+            <span>
+              <PiRowsPlusBottomLight class="cursor-pointer" size="16px" @click="handleAddRow" />
+            </span>
+          </el-tooltip>
+          <el-tooltip content="添加一列">
+            <span>
+              <PiColumnsPlusRightLight
+                class="cursor-pointer ml-4px"
+                size="16px"
+                @click="handleAddColumn"
+              />
+            </span>
+          </el-tooltip>
+        </span>
+      </div>
+    </template>
+    <el-scrollbar max-height="120px">
+      <div
+        class="flex items-center gap-4px box-border pr-12px mb-4px"
+        v-for="(row, rowIndex) in items"
+        :key="rowIndex"
+      >
+        <div class="w-full flex items-center gap-4px relative group/item">
+          <div v-for="(_, columnIndex) in row" :key="`${rowIndex}_${columnIndex}`" class="relative">
+            <el-input v-model="row[columnIndex]" />
+            <div
+              v-if="rowIndex === 0 && columnIndex !== 0"
+              class="absolute top--4px right-0 cursor-pointer"
+            >
+              <LuX size="14px" @click="handleDeleteColumn(columnIndex)" />
+            </div>
+            <div
+              v-if="columnIndex === 0 && rowIndex !== 0"
+              class="absolute left--1px top--4px cursor-pointer"
+            >
+              <LuX size="14px" @click="handleDeleteRow(rowIndex)" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </el-scrollbar>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { computed, type Ref } from 'vue'
+import { LuX } from 'vue-icons-plus/lu'
+import { PiColumnsPlusRightLight, PiRowsPlusBottomLight } from 'vue-icons-plus/pi'
+
+const props = defineProps<{
+  values: Ref<any>
+}>()
+
+// 子项
+const items = computed({
+  get() {
+    return (props.values?.value?.props.items || []) as any[]
+  },
+  set(list: any[]) {
+    if (props.values?.value?.props.items) {
+      props.values.value.props.items = list
+    }
+  }
+})
+
+// 列数
+const rows = computed({
+  get() {
+    return props.values?.value?.props.rows || 1
+  },
+  set(value: number) {
+    if (props.values?.value?.props.rows) {
+      props.values.value.props.rows = value
+    }
+  }
+})
+// 列数
+const cols = computed({
+  get() {
+    return props.values?.value?.props.cols || 1
+  },
+  set(value: number) {
+    if (props.values?.value?.props.cols) {
+      props.values.value.props.cols = value
+    }
+  }
+})
+
+/**
+ * 添加一行
+ */
+const handleAddRow = () => {
+  items.value.push(new Array(cols.value).fill('cell'))
+  rows.value++
+}
+
+/**
+ * 添加一列
+ */
+const handleAddColumn = () => {
+  items.value.forEach((row, index) => {
+    if (index === 0) {
+      row.push('header')
+    } else {
+      row.push('cell')
+    }
+  })
+  cols.value++
+}
+
+/**
+ * 删除一行
+ * @param index 索引
+ */
+const handleDeleteRow = (index: number) => {
+  items.value.splice(index, 1)
+  rows.value--
+}
+
+/**
+ * 删除一列
+ */
+const handleDeleteColumn = (index) => {
+  items.value.forEach((row) => {
+    row.splice(index, 1)
+  })
+  cols.value--
+}
+</script>
+
+<style scoped></style>

+ 59 - 0
src/renderer/src/lvgl-widgets/table/Table.vue

@@ -0,0 +1,59 @@
+<template>
+  <div :style="styleMap?.mainStyle" class="relative box-border overflow-hidden">
+    <table class="border-spacing-0">
+      <thead>
+        <tr>
+          <th
+            v-for="(header, index) in headers"
+            :key="index"
+            :style="styleMap?.itemsStyle"
+            class="min-w-130px min-h-30px box-border"
+          >
+            {{ header }}
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="(row, index) in tableData" :key="index">
+          <td
+            v-for="(cell, index) in row"
+            :key="index"
+            :style="styleMap?.itemsStyle"
+            class="min-h-18px box-border"
+          >
+            {{ cell }}
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
+
+const props = defineProps<{
+  styles: any
+  state: string
+  part: string
+  items?: string[][]
+}>()
+
+const styleMap = useWidgetStyle({
+  widget: 'lv_table',
+  props
+})
+
+// 表头
+const headers = computed(() => {
+  return props.items?.[0] || []
+})
+
+// 内容
+const tableData = computed(() => {
+  return props.items?.slice(1) || []
+})
+</script>
+
+<style scoped></style>

+ 233 - 0
src/renderer/src/lvgl-widgets/table/index.tsx

@@ -0,0 +1,233 @@
+import Table from './Table.vue'
+import icon from '../assets/icon/icon_15table.svg'
+import { flagOptions } from '@/constants'
+import type { IComponentModelConfig } from '../type'
+import i18n from '@/locales'
+import defaultStyle from './style.json'
+import Config from './Config.vue'
+
+export default {
+  label: i18n.global.t('table'),
+  icon,
+  component: Table,
+  key: 'lv_table',
+  group: i18n.global.t('container'),
+  sort: 1,
+  hasChildren: false,
+  defaultStyle,
+  parts: [
+    {
+      name: 'main',
+      stateList: ['default', 'focused', 'disabled']
+    },
+    {
+      name: 'items',
+      stateList: ['default', 'focused', 'disabled']
+    }
+  ],
+  defaultSchema: {
+    name: 'table',
+    props: {
+      x: 0,
+      y: 0,
+      addFlags: [],
+      removeFlags: [],
+      rows: 4,
+      cols: 2,
+      items: [
+        ['header', 'header'],
+        ['cell', 'cell'],
+        ['cell', 'cell'],
+        ['cell', 'cell']
+      ]
+    },
+    styles: [
+      {
+        part: {
+          name: 'main',
+          state: 'default'
+        },
+        background: {
+          color: '#ffffffff'
+        },
+        border: {
+          color: '#eeeeeeff',
+          width: 2,
+          radius: 0,
+          side: ['all']
+        },
+        padding: {
+          top: 0,
+          right: 0,
+          bottom: 0,
+          left: 0
+        },
+        shadow: {
+          color: '#2092f5ff',
+          x: 0,
+          y: 0,
+          spread: 0,
+          width: 0
+        }
+      },
+      {
+        part: {
+          name: 'items',
+          state: 'default'
+        },
+        background: {
+          color: '#ffffff00'
+        },
+        text: {
+          color: '#35383cff',
+          size: 12,
+          family: 'xx',
+          align: 'center',
+          weight: 'normal'
+        },
+        border: {
+          color: '#eeeeeeff',
+          width: 3,
+          radius: 0,
+          side: ['all']
+        },
+        padding: {
+          top: 10,
+          right: 10,
+          bottom: 10,
+          left: 10
+        }
+      }
+    ]
+  },
+  config: {
+    // 组件属性
+    props: [
+      {
+        label: '名称',
+        field: 'name',
+        valueType: 'text',
+        componentProps: {
+          placeholder: '请输入名称'
+        }
+      },
+      {
+        label: '位置/大小',
+        valueType: 'group',
+        children: [
+          {
+            label: '',
+            field: 'props.x',
+            valueType: 'number',
+            componentProps: {
+              span: 12
+            },
+            slots: { prefix: 'X' }
+          },
+          {
+            label: '',
+            field: 'props.y',
+            valueType: 'number',
+            componentProps: {
+              span: 12
+            },
+            slots: { prefix: 'Y' }
+          }
+        ]
+      },
+      {
+        label: '添加标识',
+        field: 'props.addFlags',
+        valueType: 'checkbox',
+        componentProps: {
+          options: flagOptions,
+          defaultCollapsed: true
+        }
+      },
+      {
+        label: '删除标识',
+        field: 'props.removeFlags',
+        valueType: 'checkbox',
+        componentProps: {
+          options: flagOptions,
+          defaultCollapsed: true
+        }
+      }
+    ],
+    coreProps: [
+      {
+        label: '属性',
+        field: '',
+        valueType: '',
+        render: (val) => {
+          return <Config values={val} />
+        }
+      }
+    ],
+    // 组件样式
+    styles: [
+      {
+        label: '模块状态',
+        field: 'part',
+        valueType: 'part'
+      },
+      {
+        valueType: 'dependency',
+        name: ['part'],
+        dependency: ({ part }) => {
+          return part?.name === 'main'
+            ? [
+                {
+                  label: '背景',
+                  field: 'background',
+                  valueType: 'background',
+                  componentProps: {
+                    onlyColor: true
+                  }
+                },
+                {
+                  label: '边框',
+                  field: 'border',
+                  valueType: 'border'
+                },
+                {
+                  label: '内边距',
+                  field: 'padding',
+                  valueType: 'padding'
+                },
+                {
+                  label: '阴影',
+                  field: 'shadow',
+                  valueType: 'shadow'
+                }
+              ]
+            : [
+                {
+                  label: '背景',
+                  field: 'background',
+                  valueType: 'background',
+                  componentProps: {
+                    onlyColor: true
+                  }
+                },
+                {
+                  label: '字体',
+                  field: 'text',
+                  valueType: 'font'
+                },
+                {
+                  label: '边框',
+                  field: 'border',
+                  valueType: 'border'
+                },
+                {
+                  label: '内边距',
+                  field: 'padding',
+                  valueType: 'padding'
+                }
+              ]
+        }
+      }
+    ]
+  }
+} as IComponentModelConfig

+ 178 - 0
src/renderer/src/lvgl-widgets/table/style.json

@@ -0,0 +1,178 @@
+{
+  "widget": "lv_table",
+  "styleName": "defualt",
+  "part": [
+    {
+      "partName": "main",
+      "state": [
+        {
+          "state": "default",
+          "style": {
+            "background": {
+              "color": "#ffffffff"
+            },
+            "border": {
+              "color": "#eeeeeeff",
+              "width": 2,
+              "radius": 0,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 0,
+              "right": 0,
+              "bottom": 0,
+              "left": 0
+            },
+            "shadow": {
+              "color": "#2092f5ff",
+              "x": 0,
+              "y": 0,
+              "spread": 0,
+              "width": 0
+            }
+          }
+        },
+        {
+          "state": "focused",
+          "style": {
+            "background": {
+              "color": "#ffffffff"
+            },
+            "border": {
+              "color": "#eeeeeeff",
+              "width": 2,
+              "radius": 0,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 0,
+              "right": 0,
+              "bottom": 0,
+              "left": 0
+            },
+            "shadow": {
+              "color": "#2092f5ff",
+              "x": 0,
+              "y": 0,
+              "spread": 0,
+              "width": 0
+            }
+          }
+        },
+        {
+          "state": "disabled",
+          "style": {
+            "background": {
+              "color": "#ffffffff"
+            },
+            "border": {
+              "color": "#eeeeeeff",
+              "width": 2,
+              "radius": 0,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 0,
+              "right": 0,
+              "bottom": 0,
+              "left": 0
+            },
+            "shadow": {
+              "color": "#2092f5ff",
+              "x": 0,
+              "y": 0,
+              "spread": 0,
+              "width": 0
+            }
+          }
+        }
+      ]
+    },
+    {
+      "partName": "items",
+      "state": [
+        {
+          "state": "default",
+          "style": {
+            "background": {
+              "color": "#ffffff00"
+            },
+            "text": {
+              "color": "#35383cff",
+              "size": 12,
+              "family": "xx",
+              "align": "center",
+              "weight": "normal"
+            },
+            "border": {
+              "color": "#eeeeeeff",
+              "width": 3,
+              "radius": 0,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 10,
+              "right": 10,
+              "bottom": 10,
+              "left": 10
+            }
+          }
+        },
+        {
+          "state": "focused",
+          "style": {
+            "background": {
+              "color": "#ffffff00"
+            },
+            "text": {
+              "color": "#35383cff",
+              "size": 12,
+              "family": "xx",
+              "align": "center",
+              "weight": "normal"
+            },
+            "border": {
+              "color": "#eeeeeeff",
+              "width": 3,
+              "radius": 0,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 10,
+              "right": 10,
+              "bottom": 10,
+              "left": 10
+            }
+          }
+        },
+        {
+          "state": "disabled",
+          "style": {
+            "background": {
+              "color": "#ffffff00"
+            },
+            "text": {
+              "color": "#35383cff",
+              "size": 12,
+              "family": "xx",
+              "align": "center",
+              "weight": "normal"
+            },
+            "border": {
+              "color": "#eeeeeeff",
+              "width": 3,
+              "radius": 0,
+              "side": ["all"]
+            },
+            "padding": {
+              "top": 10,
+              "right": 10,
+              "bottom": 10,
+              "left": 10
+            }
+          }
+        }
+      ]
+    }
+  ]
+}

+ 9 - 2
src/renderer/src/views/designer/sidebar/Libary.vue

@@ -40,6 +40,7 @@ import { getAddWidgetIndex } from '@/utils'
 import { useProjectStore } from '@/store/modules/project'
 
 import type { IComponentModelConfig } from '@/lvgl-widgets/type'
+import { klona } from 'klona'
 
 const search = ref('')
 const activeCollapse = ref<string[]>([])
@@ -69,10 +70,16 @@ ComponentArray.filter((item) => !item.hideLibary).forEach((item) => {
 
 // 根据搜索条件获取分组信息
 const getGroups = computed(() => {
-  const list = Object.values(groupMap.value).filter((item) =>
-    item.items.some((item) => item.label.includes(search.value))
+  const list = klona(
+    Object.values(groupMap.value).filter((item) =>
+      item.items.some((item) => item.label.includes(search.value))
+    )
   )
 
+  list.forEach((item) => {
+    item.items = item.items.filter((item) => item.label.includes(search.value))
+  })
+
   activeCollapse.value = list.map((item) => item.label)
 
   return list

+ 2 - 1
src/renderer/src/views/designer/workspace/stage/Moveable.vue

@@ -142,7 +142,8 @@ useMutationObserver(
 const zIndex = ref()
 
 const individualGroupableProps = (element: HTMLElement | SVGElement | null | undefined) => {
-  if (element?.getAttribute('widget-type') === 'lv_checkbox') {
+  const widgetType = element?.getAttribute('widget-type') || ''
+  if (['lv_checkbox', 'lv_table'].includes(widgetType)) {
     return { resizable: false }
   }
   if (element?.getAttribute('widget-type') === 'lv_image') {