jiaxing.liao 1 неделя назад
Родитель
Сommit
0fa9af465c

+ 2 - 0
src/renderer/components.d.ts

@@ -49,6 +49,7 @@ declare module 'vue' {
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
     ElSlider: typeof import('element-plus/es')['ElSlider']
+    ElSpace: typeof import('element-plus/es')['ElSpace']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
@@ -110,6 +111,7 @@ declare global {
   const ElSelect: typeof import('element-plus/es')['ElSelect']
   const ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
   const ElSlider: typeof import('element-plus/es')['ElSlider']
+  const ElSpace: typeof import('element-plus/es')['ElSpace']
   const ElSwitch: typeof import('element-plus/es')['ElSwitch']
   const ElTabPane: typeof import('element-plus/es')['ElTabPane']
   const ElTabs: typeof import('element-plus/es')['ElTabs']

+ 2 - 2
src/renderer/src/lvgl-widgets/button-matrix/Config.vue

@@ -48,14 +48,14 @@
       <el-form-item label="文本" prop="text">
         <el-input type="textarea" :rows="1" v-model="formData.text"></el-input>
       </el-form-item>
-      <el-form-item label="宽度">
+      <!-- <el-form-item label="宽度">
         <input-number
           v-model="formData.width"
           style="width: 100%"
           :min="1"
           :max="255"
         ></input-number>
-      </el-form-item>
+      </el-form-item> -->
     </el-form>
     <el-collapse v-if="isCustom" :model-value="['style']">
       <el-collapse-item name="style">

+ 5 - 5
src/renderer/src/lvgl-widgets/button-matrix/index.tsx

@@ -279,11 +279,11 @@ export default {
           step: 1
         }
       },
-      {
-        label: '自定义',
-        field: 'props.isCustom',
-        valueType: 'switch'
-      },
+      // {
+      //   label: '自定义',
+      //   field: 'props.isCustom',
+      //   valueType: 'switch'
+      // },
       {
         label: '属性',
         field: '',

+ 331 - 0
src/renderer/src/lvgl-widgets/chart/Chart.vue

@@ -0,0 +1,331 @@
+<template>
+  <div
+    :style="{
+      ...styleMap?.mainStyle
+    }"
+    class="relative w-full h-full box-border overflow-hidden"
+  >
+    <ImageBg
+      v-if="styleMap?.mainStyle?.imageSrc"
+      :src="styleMap?.mainStyle?.imageSrc"
+      :imageStyle="styleMap?.mainStyle?.imageStyle"
+    />
+
+    <div class="absolute inset-0 w-full h-full">
+      <svg
+        :viewBox="`0 0 ${width} ${height}`"
+        preserveAspectRatio="xMidYMid meet"
+        class="w-full h-full block"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <!-- 网格线 -->
+        <g v-if="hasGrid">
+          <line
+            v-for="(line, index) in verticalGridLines"
+            :key="'v-' + index"
+            :x1="line.x1"
+            :y1="line.y1"
+            :x2="line.x2"
+            :y2="line.y2"
+            :stroke="gridLineColor"
+            :stroke-width="gridLineWidth"
+            :stroke-dasharray="gridDashArray"
+            :opacity="gridLineOpacity"
+          />
+          <line
+            v-for="(line, index) in horizontalGridLines"
+            :key="'h-' + index"
+            :x1="line.x1"
+            :y1="line.y1"
+            :x2="line.x2"
+            :y2="line.y2"
+            :stroke="gridLineColor"
+            :stroke-width="gridLineWidth"
+            :stroke-dasharray="gridDashArray"
+            :opacity="gridLineOpacity"
+          />
+        </g>
+
+        <!-- 折线图 -->
+        <g v-if="chart_type === 'line'">
+          <path
+            v-for="serie in lineSeries"
+            :key="`line-${serie.key}`"
+            :d="serie.path"
+            fill="none"
+            :stroke="serie.color"
+            :stroke-width="2"
+          />
+          <g v-if="!hide_line_points">
+            <circle
+              v-for="point in linePoints"
+              :key="point.key"
+              :cx="point.x"
+              :cy="point.y"
+              r="3"
+              :fill="point.color"
+            />
+          </g>
+        </g>
+
+        <!-- 柱状图 -->
+        <g v-else-if="chart_type === 'bar'">
+          <rect
+            v-for="bar in barSeries"
+            :key="bar.key"
+            :x="bar.x"
+            :y="bar.y"
+            :width="bar.width"
+            :height="bar.height"
+            :fill="bar.color"
+          />
+        </g>
+      </svg>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
+
+import ImageBg from '../ImageBg.vue'
+import type { DataSection } from './Config.vue'
+
+const props = defineProps<{
+  width: number
+  height: number
+  styles: any
+  state?: string
+  part?: string
+  column: number
+  row: number
+  chart_type: 'line' | 'bar' | 'none'
+  hide_line_points: boolean
+  axis_y: {
+    min: number
+    max: number
+  }
+  open_axis_y2: boolean
+  axis_y2?: {
+    min: number
+    max: number
+  }
+  chart_data: DataSection[]
+}>()
+
+const styleMap = useWidgetStyle({
+  widget: 'lv_chart',
+  props
+})
+
+// 主样式配置(获取折线虚线样式)
+const mainStyleConfig = computed(() =>
+  props.styles?.find(
+    (s: any) =>
+      s.part?.name === 'main' && (s.part?.state === props.state || s.part?.state === 'default')
+  )
+)
+
+const gridLineWidth = computed(() => styleMap.value?.mainStyle?.line?.width ?? 1)
+const gridLineColor = computed(() => styleMap.value?.mainStyle?.line?.color ?? '#e0e0e0')
+const gridLineOpacity = computed(() => styleMap.value?.mainStyle?.line?.opacity ?? 1)
+
+const gridDashArray = computed(() => {
+  const dashWidth = mainStyleConfig.value?.line?.dashWidth ?? 0
+  const dashGap = mainStyleConfig.value?.line?.dashGap ?? 0
+  if (dashWidth > 0 || dashGap > 0) {
+    return `${dashWidth} ${dashGap}`
+  }
+  return undefined
+})
+
+// 安全内边距,模拟 LVGL chart 的安全区域,避免图形/网格贴边框
+const padding = 8
+
+const innerLeft = computed(() => padding)
+const innerRight = computed(() => Math.max(padding, props.width - padding))
+const innerTop = computed(() => padding)
+const innerBottom = computed(() => Math.max(padding, props.height - padding))
+
+const innerWidth = computed(() => Math.max(0, innerRight.value - innerLeft.value))
+const innerHeight = computed(() => Math.max(0, innerBottom.value - innerTop.value))
+
+const hasGrid = computed(() => (props.column ?? 0) > 0 || (props.row ?? 0) > 0)
+
+// 网格线
+const verticalGridLines = computed(() => {
+  const count = Math.max(0, props.column ?? 0)
+  const lines: { x1: number; y1: number; x2: number; y2: number }[] = []
+  if (!innerWidth.value || count <= 0) return lines
+
+  // 从安全内边距开始计算:
+  // column = 4 时,i = 0..3,第一条在左侧安全边距,最后一条在右侧安全边距,中间等分
+  for (let i = 0; i < count; i++) {
+    const t = count === 1 ? 0.5 : i / (count - 1)
+    const x = innerLeft.value + t * innerWidth.value
+    lines.push({
+      x1: x,
+      y1: 0,
+      x2: x,
+      y2: props.height
+    })
+  }
+  return lines.length >= 2 ? lines : []
+})
+
+const horizontalGridLines = computed(() => {
+  const count = Math.max(0, props.row ?? 0)
+  const lines: { x1: number; y1: number; x2: number; y2: number }[] = []
+  if (!innerHeight.value || count <= 0) return lines
+
+  // 从安全内边距开始计算:
+  // row = 4 时,i = 0..3,第一条在上侧安全边距,最后一条在下侧安全边距,中间等分
+  for (let i = 0; i < count; i++) {
+    const t = count === 1 ? 0.5 : i / (count - 1)
+    const y = innerTop.value + t * innerHeight.value
+    lines.push({
+      x1: 0,
+      y1: y,
+      x2: props.width,
+      y2: y
+    })
+  }
+  return lines.length >= 2 ? lines : []
+})
+
+type AxisKey = 'y' | 'y2'
+
+const getAxisRange = (axis: AxisKey) => {
+  const cfg = axis === 'y2' && props.open_axis_y2 ? props.axis_y2 || props.axis_y : props.axis_y
+  let min = Number(cfg?.min ?? 0)
+  let max = Number(cfg?.max ?? 100)
+
+  if (min === max) {
+    const delta = Math.abs(min || 1)
+    min -= delta
+    max += delta
+  }
+
+  if (min > max) {
+    const temp = min
+    min = max
+    max = temp
+  }
+
+  return { min, max }
+}
+
+const mapValueToY = (value: number, axis: AxisKey) => {
+  const { min, max } = getAxisRange(axis)
+  const span = max - min
+  if (!innerHeight.value || span === 0) {
+    return (innerTop.value + innerBottom.value) / 2
+  }
+  const t = (value - min) / span
+  const clamped = Math.max(0, Math.min(1, t))
+  return innerBottom.value - clamped * innerHeight.value
+}
+
+const mapIndexToX = (index: number, total: number) => {
+  if (!innerWidth.value) {
+    return (innerLeft.value + innerRight.value) / 2
+  }
+  if (total <= 1) {
+    return innerLeft.value + innerWidth.value / 2
+  }
+  const t = index / (total - 1)
+  return innerLeft.value + t * innerWidth.value
+}
+
+const seriesList = computed(() => (props.chart_data || []) as DataSection[])
+
+// 折线图数据
+const lineSeries = computed(() => {
+  return seriesList.value.map((serie, index) => {
+    const total = Array.isArray(serie.points) ? serie.points.length : 0
+    const axis: AxisKey = serie.chart_axis === 'y2' ? 'y2' : 'y'
+
+    if (!total || !innerWidth.value || !innerHeight.value) {
+      return {
+        key: serie.name || index,
+        color: serie.color || '#2092f5ff',
+        path: '',
+        points: [] as { x: number; y: number; color: string; key: string }[]
+      }
+    }
+
+    const points = serie.points.map((v, idx) => {
+      const x = mapIndexToX(idx, total)
+      const y = mapValueToY(v, axis)
+      return {
+        x,
+        y,
+        color: serie.color || '#2092f5ff',
+        key: `${serie.name || index}-${idx}`
+      }
+    })
+
+    const path = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')
+
+    return {
+      key: serie.name || index,
+      color: serie.color || '#2092f5ff',
+      path,
+      points
+    }
+  })
+})
+
+const linePoints = computed(() => lineSeries.value.flatMap((serie) => serie.points))
+
+// 柱状图数据
+const barSeries = computed(() => {
+  const series = seriesList.value.filter((s) => Array.isArray(s.points) && s.points.length > 0)
+  const seriesCount = series.length
+  if (!seriesCount || !innerWidth.value || !innerHeight.value) return []
+
+  const maxPointCount = series.reduce((max, s) => Math.max(max, s.points.length), 0)
+  if (!maxPointCount) return []
+
+  const groupSpace = innerWidth.value / maxPointCount
+  const groupWidth = groupSpace * 0.7
+  const barWidth = groupWidth / seriesCount
+  const offsetInGroup = (groupSpace - groupWidth) / 2
+
+  const bars: {
+    key: string
+    x: number
+    y: number
+    width: number
+    height: number
+    color: string
+  }[] = []
+
+  for (let i = 0; i < maxPointCount; i++) {
+    const groupStart = innerLeft.value + i * groupSpace + offsetInGroup
+
+    series.forEach((serie, sIndex) => {
+      const v = serie.points[i]
+      if (typeof v !== 'number') return
+      const axis: AxisKey = serie.chart_axis === 'y2' ? 'y2' : 'y'
+
+      const yValue = mapValueToY(v, axis)
+      const baseY = mapValueToY(getAxisRange(axis).min, axis)
+      const top = Math.min(yValue, baseY)
+      const bottom = Math.max(yValue, baseY)
+
+      bars.push({
+        key: `${serie.name || sIndex}-${i}`,
+        x: groupStart + sIndex * barWidth,
+        y: top,
+        width: barWidth,
+        height: Math.max(1, bottom - top),
+        color: serie.color || '#2092f5ff'
+      })
+    })
+  }
+
+  return bars
+})
+</script>

+ 172 - 0
src/renderer/src/lvgl-widgets/chart/Config.vue

@@ -0,0 +1,172 @@
+<template>
+  <div>
+    <el-card
+      :body-class="!dataList.length ? 'hidden' : 'pr-0!'"
+      :header-class="!dataList.length ? 'border-b-none!' : ''"
+      class="mb-12px"
+    >
+      <template #header>
+        <div class="flex items-center justify-between">
+          <span>数据</span>
+          <span class="flex gap-4px">
+            <LuPlus class="cursor-pointer" @click="handleAdd" size="14px" />
+          </span>
+        </div>
+      </template>
+
+      <el-scrollbar height="120px" v-if="dataList.length > 0">
+        <div
+          v-for="(item, index) in dataList"
+          :key="v4()"
+          class="flex items-center pr-12px"
+          @click="handleEdit(item, index)"
+        >
+          <span class="flex-1 truncate text-#00ff00 cursor-pointer" :style="{ color: item.color }">
+            {{ item.name || `data_${index}` }}
+          </span>
+          <LuTrash2 class="cursor-pointer shrink-0" @click.stop="handleDelete(index)" size="14px" />
+        </div>
+      </el-scrollbar>
+    </el-card>
+
+    <el-dialog v-model="dialogVisible" title="编辑数据" width="440px">
+      <el-form :model="formData" label-position="left" label-width="80px">
+        <el-form-item :label="'名称'">
+          <el-input v-model="formData.name" spellcheck="false" disabled />
+        </el-form-item>
+
+        <el-form-item :label="'Y轴'">
+          <el-select v-model="formData.chart_axis" :options="[]" />
+        </el-form-item>
+
+        <el-form-item :label="'颜色'">
+          <ColorPicker
+            v-model:pureColor="formData.color"
+            format="hex8"
+            picker-type="chrome"
+            use-type="pure"
+          />
+          <span class="text-text-active">{{ formData.color }}</span>
+        </el-form-item>
+
+        <el-form-item :label="'数据'">
+          <el-space wrap>
+            <div v-for="(_p, index) in formData.points" :key="index" class="relative">
+              <input-number v-model="formData.points[index]" class="flex-1" />
+
+              <div
+                class="absolute top--10px left--4px cursor-pointer z-10"
+                @click="handleDeletePoint(index)"
+              >
+                <LuX size="14px" />
+              </div>
+            </div>
+          </el-space>
+
+          <div class="w-full mt-12px gap-4px">
+            <el-button type="primary" class="w-full" @click="handleAddPoint">
+              <LuPlus size="14px" />
+              新增数据
+            </el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <el-button type="primary" @click="submit">
+          {{ '确定' }}
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, type Ref, ref } from 'vue'
+import { LuPlus, LuTrash2, LuX } from 'vue-icons-plus/lu'
+import { v4 } from 'uuid'
+import { getNextIndex } from '@/utils'
+
+import { ColorPicker } from '@/components'
+import { klona } from 'klona'
+
+export type DataSection = {
+  name: string
+  chart_axis: 'y' | 'y2'
+  color: string
+  points: number[]
+}
+
+const props = defineProps<{
+  values: Ref<DataSection[]>
+}>()
+
+const dialogVisible = ref(false)
+const formData = ref<DataSection>({
+  name: '',
+  chart_axis: 'y',
+  color: '#000000ff',
+  points: [10, 20, 30, 40, 50]
+})
+
+const dataList = computed({
+  get() {
+    return (props.values?.value || []) as DataSection[]
+  },
+  set(list: DataSection[]) {
+    props.values.value = list
+  }
+})
+
+/**
+ * 添加
+ */
+const handleAdd = () => {
+  const newIndex = getNextIndex(dataList.value, 'name')
+  dataList.value.push({
+    name: `data_${newIndex}`,
+    chart_axis: 'y',
+    color: '#000000ff',
+    points: [10, 20, 30, 40, 50]
+  })
+}
+
+/**
+ * 删除区域
+ */
+const handleDelete = (index: number) => {
+  dataList.value.splice(index, 1)
+}
+
+let tempIndex
+/**
+ * 编辑(直接引用修改)
+ */
+const handleEdit = (record: DataSection, index: number) => {
+  formData.value = klona(record)
+  dialogVisible.value = true
+  tempIndex = index
+}
+
+/**
+ * 删除数据
+ */
+const handleDeletePoint = (index: number) => {
+  formData.value.points.splice(index, 1)
+}
+
+/**
+ * 新增数据
+ */
+const handleAddPoint = () => {
+  formData.value.points.push(0)
+}
+
+/**
+ * 提交
+ */
+const submit = () => {
+  dataList.value[tempIndex] = klona(formData.value)
+  dialogVisible.value = false
+}
+</script>

+ 388 - 0
src/renderer/src/lvgl-widgets/chart/index.tsx

@@ -0,0 +1,388 @@
+import Chart from './Chart.vue'
+import icon from '../assets/icon/icon_24echart.svg'
+import type { IComponentModelConfig } from '../type'
+import i18n from '@/locales'
+import { flagOptions, stateOptions, stateList } from '@/constants'
+import defaultStyle from './style.json'
+import Config from './Config.vue'
+
+export default {
+  label: i18n.global.t('chart'),
+  icon,
+  component: Chart,
+  key: 'lv_chart',
+  group: i18n.global.t('display'),
+  sort: 1,
+  defaultStyle,
+  parts: [
+    {
+      name: 'main',
+      stateList
+    }
+  ],
+  defaultSchema: {
+    name: 'chart',
+    props: {
+      x: 0,
+      y: 0,
+      width: 205,
+      height: 155,
+      flags: [],
+      states: [],
+      scrollbar: 'off',
+      column: 4,
+      row: 4,
+      chart_type: 'line',
+      hide_line_points: false,
+      axis_y: {
+        min: 0,
+        max: 100
+      },
+      // 副Y轴
+      open_axis_y2: false,
+      axis_y2: {
+        min: 0,
+        max: 100
+      },
+      chart_data: [
+        {
+          name: 'data_0',
+          chart_axis: 'y',
+          color: '#000000ff',
+          points: [10, 20, 30, 40, 50]
+        }
+      ]
+    },
+    styles: [
+      {
+        part: {
+          name: 'main',
+          state: 'default'
+        },
+        background: {
+          color: '#ffffffff',
+          image: {
+            imgId: '',
+            recolor: '#ffffff00',
+            alpha: 255
+          }
+        },
+        border: {
+          color: '#e0e0e0ff',
+          width: 0,
+          radius: 6,
+          side: ['all']
+        },
+        line: {
+          color: '#e0e0e0ff',
+          width: 2,
+          // 虚线宽度
+          dashWidth: 0,
+          // 虚线间隔
+          dashGap: 0
+        },
+        shadow: {
+          color: '#000000ff',
+          offsetX: 0,
+          offsetY: 0,
+          spread: 0,
+          width: 0
+        },
+        transform: {
+          width: 0,
+          height: 0,
+          translateX: 0,
+          translateY: 0,
+          originX: 0,
+          originY: 0,
+          rotate: 0,
+          scale: 256
+        }
+      }
+    ]
+  },
+  config: {
+    // 组件属性
+    props: [
+      {
+        label: '名称',
+        field: 'name',
+        valueType: 'text',
+        componentProps: {
+          placeholder: '请输入名称',
+          type: 'text'
+        }
+      },
+      {
+        label: '位置/大小',
+        valueType: 'group',
+        children: [
+          {
+            label: '',
+            field: 'props.x',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -10000,
+              max: 10000
+            },
+            slots: { prefix: 'X' }
+          },
+          {
+            label: '',
+            field: 'props.y',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -10000,
+              max: 10000
+            },
+            slots: { prefix: 'Y' }
+          },
+          {
+            label: '',
+            field: 'props.width',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 1,
+              max: 10000
+            },
+            slots: { prefix: 'W' }
+          },
+          {
+            label: '',
+            field: 'props.height',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 1,
+              max: 10000
+            },
+            slots: { prefix: 'H' }
+          }
+        ]
+      },
+      {
+        label: '滚动条',
+        field: 'props.scrollbar',
+        valueType: 'select',
+        componentProps: {
+          options: [
+            { label: 'On', value: 'on' },
+            { label: 'Off', value: 'off' },
+            { label: 'Auto', value: 'auto' },
+            { label: 'Active', value: 'active' }
+          ]
+        }
+      },
+      {
+        label: '标识',
+        field: 'props.flags',
+        valueType: 'checkbox',
+        componentProps: {
+          options: flagOptions,
+          defaultCollapsed: true
+        }
+      },
+      {
+        label: '状态',
+        field: 'props.states',
+        valueType: 'checkbox',
+        componentProps: {
+          options: stateOptions,
+          defaultCollapsed: true
+        }
+      }
+    ],
+    coreProps: [
+      {
+        label: '直线',
+        valueType: 'group',
+        children: [
+          {
+            field: 'props.column',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 0,
+              max: 200
+            },
+            slots: {
+              prefix: 'C'
+            }
+          },
+          {
+            field: 'props.row',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 0,
+              max: 200
+            },
+            slots: {
+              prefix: 'R'
+            }
+          }
+        ]
+      },
+      {
+        label: '类型',
+        field: 'props.chart_type',
+        labelWidth: '80px',
+        valueType: 'select',
+        componentProps: {
+          options: [
+            { label: 'Line', value: 'line' },
+            { label: 'Bar', value: 'bar' },
+            { label: 'None', value: 'none' }
+          ]
+        }
+      },
+      {
+        valueType: 'dependency',
+        name: ['props.chart_type'],
+        dependency: (dependency) => {
+          return dependency?.['props.chart_type'] === 'line'
+            ? [
+                {
+                  label: '隐藏线条点',
+                  field: 'props.hide_line_points',
+                  labelWidth: '80px',
+                  valueType: 'switch'
+                }
+              ]
+            : []
+        }
+      },
+      {
+        label: '主Y轴',
+        valueType: 'group',
+        children: [
+          {
+            field: 'props.axis_y.min',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -10000,
+              max: 10000
+            },
+            slots: {
+              prefix: 'Min'
+            }
+          },
+          {
+            field: 'props.axis_y.max',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -10000,
+              max: 10000
+            },
+            slots: {
+              prefix: 'Max'
+            }
+          }
+        ]
+      },
+      {
+        label: '副Y轴',
+        field: 'props.open_axis_y2',
+        valueType: 'switch'
+      },
+      {
+        valueType: 'dependency',
+        names: ['props.open_axis_y2'],
+        dependency: (dependency) => {
+          return dependency['props.open_axis_y2']
+            ? [
+                {
+                  label: '主Y轴',
+                  valueType: 'group',
+                  children: [
+                    {
+                      field: 'props.axis_y2.min',
+                      valueType: 'number',
+                      componentProps: {
+                        span: 12,
+                        min: -10000,
+                        max: 10000
+                      },
+                      slots: {
+                        prefix: 'Min'
+                      }
+                    },
+                    {
+                      field: 'props.axis_y2.max',
+                      valueType: 'number',
+                      componentProps: {
+                        span: 12,
+                        min: -10000,
+                        max: 10000
+                      },
+                      slots: {
+                        prefix: 'Max'
+                      }
+                    }
+                  ]
+                }
+              ]
+            : []
+        }
+      },
+      {
+        label: '属性',
+        field: 'props.chart_data',
+        valueType: '',
+        render: (val) => {
+          return <Config values={val} />
+        }
+      }
+    ],
+    // 组件样式
+    styles: [
+      {
+        label: '模块状态',
+        field: 'part',
+        valueType: 'part'
+      },
+      {
+        valueType: 'dependency',
+        name: ['part'],
+        dependency: () => {
+          return [
+            {
+              label: '背景',
+              field: 'background',
+              valueType: 'background'
+            },
+            {
+              label: '边框',
+              field: 'border',
+              valueType: 'border'
+            },
+            {
+              label: '直线',
+              field: 'line',
+              valueType: 'line',
+              componentProps: {
+                hasDash: true,
+                hideRadius: true
+              }
+            },
+            {
+              label: '阴影',
+              field: 'shadow',
+              valueType: 'shadow'
+            },
+            {
+              label: '变换',
+              field: 'transform',
+              valueType: 'transform'
+            }
+          ]
+        }
+      }
+    ]
+  }
+} as IComponentModelConfig

+ 49 - 0
src/renderer/src/lvgl-widgets/chart/style.json

@@ -0,0 +1,49 @@
+{
+  "widget": "lv_chart",
+  "styleName": "defualt",
+  "part": [
+    {
+      "partName": "main",
+      "defaultStyle": {
+        "background": {
+          "color": "#ffffffff",
+          "image": {
+            "imgId": "",
+            "recolor": "#ffffff00",
+            "alpha": 255
+          }
+        },
+        "border": {
+          "color": "#e0e0e0ff",
+          "width": 0,
+          "radius": 6,
+          "side": ["all"]
+        },
+        "line": {
+          "color": "#e0e0e0ff",
+          "width": 2,
+          "dashWidth": 0,
+          "dashGap": 0
+        },
+        "shadow": {
+          "color": "#000000ff",
+          "offsetX": 0,
+          "offsetY": 0,
+          "spread": 0,
+          "width": 0
+        },
+        "transform": {
+          "width": 0,
+          "height": 0,
+          "translateX": 0,
+          "translateY": 0,
+          "originX": 0,
+          "originY": 0,
+          "rotate": 0,
+          "scale": 256
+        }
+      },
+      "state": []
+    }
+  ]
+}

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

@@ -24,6 +24,7 @@ import Line from './line/index'
 import Arc from './arc'
 import Scale from './scale/index'
 import Led from './led/index'
+import Chart from './chart/index'
 
 import Page from './page'
 import { IComponentModelConfig } from './type'
@@ -56,7 +57,8 @@ export const ComponentArray = [
   Line,
   Arc,
   Scale,
-  Led
+  Led,
+  Chart
 ]
 
 const componentMap: { [key: string]: IComponentModelConfig } = ComponentArray.reduce((acc, cur) => {

+ 5 - 1
src/renderer/src/lvgl-widgets/type.d.ts

@@ -311,11 +311,15 @@ export interface IStyleConfig {
     // 宽度
     width: number
     // 圆角开关
-    radius: boolean
+    radius?: boolean
     // 透明度
     alpha?: number
     // 图像
     image?: string
+    // 虚线宽度
+    dashWidth?: number
+    // 虚线间隔
+    dashGap?: number
   }
   // 图像样式
   imageStyle?: {

+ 57 - 3
src/renderer/src/views/designer/config/property/components/StyleLine.vue

@@ -5,9 +5,14 @@
       <span class="text-text-active">{{ modelValue?.color }}</span>
     </el-form-item>
     <el-form-item label="宽度" label-position="left" label-width="60px">
-      <input-number placeholder="请输入" v-model="width" controls-position="right" style="width: 100%" />
+      <input-number
+        placeholder="请输入"
+        v-model="width"
+        controls-position="right"
+        style="width: 100%"
+      />
     </el-form-item>
-    <el-form-item label="圆角" label-position="left" label-width="60px">
+    <el-form-item v-if="!hideRadius" label="圆角" label-position="left" label-width="60px">
       <el-switch v-model="radius" />
     </el-form-item>
 
@@ -22,6 +27,27 @@
         </span>
       </div>
     </el-form-item>
+
+    <el-form-item v-if="hasDash" label="虚线宽度" label-position="left" label-width="60px">
+      <input-number
+        placeholder="请输入"
+        v-model="dashWidth"
+        controls-position="right"
+        style="width: 100%"
+        :min="0"
+        :max="100"
+      />
+    </el-form-item>
+    <el-form-item v-if="hasDash" label="虚线间隔" label-position="left" label-width="60px">
+      <input-number
+        placeholder="请输入"
+        v-model="dashGap"
+        controls-position="right"
+        style="width: 100%"
+        :min="0"
+        :max="100"
+      />
+    </el-form-item>
   </div>
 </template>
 
@@ -32,14 +58,18 @@ import ImageSelect from './ImageSelect.vue'
 
 defineProps<{
   hasImage?: boolean
+  hasDash?: boolean
+  hideRadius?: boolean
 }>()
 
 const modelValue = defineModel<{
   color: string
   width: number
-  radius: boolean
+  radius?: boolean
   image?: string
   alpha?: number
+  dashWidth?: number
+  dashGap?: number
 }>('modelValue')
 
 // color
@@ -94,6 +124,30 @@ const image = computed({
     }
   }
 })
+
+// 虚线宽度
+const dashWidth = computed({
+  get() {
+    return modelValue.value?.dashWidth
+  },
+  set(val: number) {
+    if (modelValue.value) {
+      modelValue.value.dashWidth = val
+    }
+  }
+})
+
+// 虚线间隔
+const dashGap = computed({
+  get() {
+    return modelValue.value?.dashGap
+  },
+  set(val: number) {
+    if (modelValue.value) {
+      modelValue.value.dashGap = val
+    }
+  }
+})
 </script>
 
 <style scoped></style>