Kaynağa Gözat

feat: 新增背景渐变配置弹窗

jiaxing.liao 2 hafta önce
ebeveyn
işleme
9caebe1d7c

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

@@ -12,6 +12,7 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    ColorModal: typeof import('./src/components/ColorModal/index.vue')['default']
     ColorPicker: typeof import('./src/components/ColorPicker/index.vue')['default']
     EditorModal: typeof import('./src/components/EditorModal/index.vue')['default']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
@@ -44,6 +45,7 @@ declare module 'vue' {
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+    ElSegmented: typeof import('element-plus/es')['ElSegmented']
     ElSelect: typeof import('element-plus/es')['ElSelect']
     ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
     ElSlider: typeof import('element-plus/es')['ElSlider']
@@ -71,6 +73,7 @@ declare module 'vue' {
 
 // For TSX support
 declare global {
+  const ColorModal: typeof import('./src/components/ColorModal/index.vue')['default']
   const ColorPicker: typeof import('./src/components/ColorPicker/index.vue')['default']
   const EditorModal: typeof import('./src/components/EditorModal/index.vue')['default']
   const ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
@@ -103,6 +106,7 @@ declare global {
   const ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
   const ElRow: typeof import('element-plus/es')['ElRow']
   const ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+  const ElSegmented: typeof import('element-plus/es')['ElSegmented']
   const ElSelect: typeof import('element-plus/es')['ElSelect']
   const ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
   const ElSlider: typeof import('element-plus/es')['ElSlider']

+ 603 - 0
src/renderer/src/components/ColorModal/index.vue

@@ -0,0 +1,603 @@
+<template>
+  <el-dialog v-model="show" title="背景颜色" width="440px" draggable>
+    <el-scrollbar height="400px">
+      <div class="w-full flex flex-col items-center">
+        <el-segmented v-if="useType === 'both'" v-model="type" :options="typeOptions" />
+
+        <div v-if="type !== 'pure'" class="w-full flex flex-col items-center">
+          <h4 class="m-0">预览</h4>
+          <div class="w-80% h-60px rounded-4px mb-10px" :style="previewGradient"></div>
+        </div>
+
+        <!-- 纯色 -->
+        <div v-if="type === 'pure'" class="mt-20px">
+          <ColorPicker v-model:pureColor="pureColor" format="hex8" picker-type="chrome" use-type="pure" isWidget />
+        </div>
+
+        <!-- 基础渐变 -->
+        <div v-else-if="type === 'gradient'" class="w-full">
+          <el-form label-width="80px" label-position="left">
+            <el-form-item label="渐变方向">
+              <el-radio-group v-model="basicDirection">
+                <el-radio value="horizontal">水平渐变</el-radio>
+                <el-radio value="vertical">垂直渐变</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="开始颜色">
+              <div class="flex items-center gap-12px w-full">
+                <ColorPicker v-model:pureColor="basicStartColor" format="hex8" picker-type="chrome" use-type="pure" />
+                <div>
+                  {{ basicStartColor }}
+                </div>
+              </div>
+            </el-form-item>
+
+            <el-form-item label="结束颜色">
+              <div class="flex items-center gap-12px w-full">
+                <ColorPicker v-model:pureColor="basicEndColor" format="hex8" picker-type="chrome" use-type="pure" />
+                <div>
+                  {{ basicEndColor }}
+                </div>
+              </div>
+            </el-form-item>
+
+            <el-form-item label="开始点">
+              <div class="flex items-center gap-8px w-full">
+                <el-slider v-model="basicStartPos" :min="0" :max="255" style="flex: 1" />
+                <span class="w-40px text-right text-text-active">
+                  {{ basicStartPos }}
+                </span>
+              </div>
+            </el-form-item>
+
+            <el-form-item label="结束点">
+              <div class="flex items-center gap-8px w-full">
+                <el-slider v-model="basicEndPos" :min="0" :max="255" style="flex: 1" />
+                <span class="w-40px text-right text-text-active">
+                  {{ basicEndPos }}
+                </span>
+              </div>
+            </el-form-item>
+          </el-form>
+        </div>
+
+        <!-- 高级渐变 -->
+        <div v-else class="w-full">
+          <el-form label-position="top" class="advanced-form">
+            <el-form-item label="渐变类型">
+              <el-radio-group v-model="advancedType" @change="onChangeAdvancedType">
+                <el-radio label="linear">线性渐变</el-radio>
+                <el-radio label="radial">径向渐变</el-radio>
+                <el-radio label="conical">锥形渐变</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <!-- 线性渐变:起始/结束坐标 -->
+            <template v-if="advancedType === 'linear'">
+              <el-form-item label="开始坐标">
+                <div class="w-full flex items-center gap-8px">
+                  <input-number v-model="linearStartX" :min="0" :max="width" size="small" style="flex: 1;">
+                    <template #prefix>X</template>
+                  </input-number>
+                  <input-number v-model="linearStartY" :min="0" :max="height" size="small" style="flex: 1;">
+                    <template #prefix>Y</template>
+                  </input-number>
+                </div>
+              </el-form-item>
+              <el-form-item label="结束坐标">
+                <div class="w-full flex items-center gap-8px">
+                  <input-number v-model="linearEndX" :min="0" :max="width" size="small" style="flex: 1;">
+                    <template #prefix>X</template>
+                  </input-number>
+                  <input-number v-model="linearEndY" :min="0" :max="height" size="small" style="flex: 1;">
+                    <template #prefix>Y</template>
+                  </input-number>
+                </div>
+              </el-form-item>
+            </template>
+
+            <!-- 径向渐变:中心点 -->
+            <template v-else-if="advancedType === 'radial'">
+              <el-form-item label="中心点坐标">
+                <div class="w-full flex items-center gap-8px">
+                  <input-number v-model="centerX" :min="0" :max="width" size="small" style="flex: 1;">
+                    <template #prefix>X</template>
+                  </input-number>
+                  <input-number v-model="centerY" :min="0" :max="height" size="small" style="flex: 1;">
+                    <template #prefix>Y</template>
+                  </input-number>
+                </div>
+              </el-form-item>
+            </template>
+
+            <!-- 锥形渐变:中心点 + 角度 -->
+            <template v-else>
+              <el-form-item label="中心点坐标">
+                <div class="w-full flex items-center gap-8px">
+                  <input-number v-model="centerX" :min="0" :max="width" size="small" style="flex: 1;" />
+                  <input-number v-model="centerY" :min="0" :max="height" size="small" style="flex: 1;" />
+                </div>
+              </el-form-item>
+              <el-form-item label="角度">
+                <div class="w-full flex items-center gap-8px">
+                  <input-number v-model="coneStartAngle" :min="0" :max="3600" size="small" style="width: 100%;">
+                    <template #prefix>S</template>
+                  </input-number>
+                  <input-number v-model="coneEndAngle" :min="0" :max="3600" size="small" style="width: 100%;">
+                    <template #prefix>E</template>
+                  </input-number>
+                </div>
+              </el-form-item>
+            </template>
+
+            <!-- 渐变点列表 -->
+            <el-form-item label="渐变点">
+              <div class="w-full flex flex-col gap-12px">
+                <el-button type="primary" :disabled="pointsList.length >= 16" @click="addPoint">
+                  新增渐变点
+                </el-button>
+                <div v-for="(pt, index) in pointsList" :key="index" class="gradient-point-item">
+                  <el-card shadow="hover" bodyClass="p-10px! relative">
+                    <div class="flex gap-12px items-center pr-12px jusitfy-between">
+                      <ColorPicker class="flex-1" v-model:pureColor="pt.color" format="hex8" picker-type="chrome"
+                        use-type="pure" />
+                      <div class="flex-1/3">
+                        <div>颜色值</div>
+                        <div>{{ pt.color }}</div>
+                      </div>
+                      <div class="flex-1/3">
+                        <div>位置:{{ pt.point }}</div>
+                        <div><el-slider v-model="pt.point" :min="0" :max="255" /></div>
+                      </div>
+                    </div>
+                    <div class="absolute right-4px top-0">
+                      <el-button v-if="pointsList.length > 2" link @click="removePoint(index)">
+                        <LuTrash2 size="14px" />
+                      </el-button>
+                    </div>
+                  </el-card>
+                </div>
+              </div>
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+    </el-scrollbar>
+
+
+    <template #footer>
+      <el-button type="primary" @click="submit">确定</el-button>
+    </template>
+  </el-dialog>
+  <div v-if="useType != 'pure'" class="trigger" @click="open" :style="triggerStyle">
+    <slot name="trigger"></slot>
+  </div>
+  <ColorPicker v-else v-model:pureColor="pureColor" format="hex8" picker-type="chrome" use-type="pure" />
+
+</template>
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+import { ColorPicker } from '..'
+import { parseCssGradient, generateCssGradient } from '@/utils'
+import { klona } from 'klona'
+import { LuTrash2 } from 'vue-icons-plus/lu'
+
+import type { GradientColor } from '@/lvgl-widgets/type'
+
+const pureColor = defineModel<string>('pureColor')
+const gradientColor = defineModel<GradientColor | string>('gradientColor')
+
+const props = withDefaults(defineProps<{
+  useType?: 'pure' | 'gradient' | 'both';
+  onlyColor?: boolean;
+  width?: number
+  height?: number
+}>(), {
+  useType: 'pure'
+})
+const show = ref(false)
+
+const type = ref<'pure' | 'gradient' | 'advanced'>('pure')
+const typeOptions = computed(() => {
+  const options = [
+    {
+      label: '纯色',
+      value: 'pure'
+    },
+    {
+      label: '基础渐变',
+      value: 'gradient'
+    },
+  ]
+  // 如果宽高存在,则高级渐变可选
+  if (props.width && props.height) {
+    options.push({
+      label: '高级渐变',
+      value: 'advanced'
+    })
+  }
+
+  return options
+})
+
+// 内部渐变配置对象
+const internalGradient = ref<GradientColor | null>(null)
+
+const ensureGradient = () => {
+  if (!internalGradient.value) {
+    internalGradient.value = {
+      type: 'linear',
+      gradientType: 'gradient',
+      direction: 'horizontal',
+      points: [
+        {
+          point: 0,
+          color: pureColor.value || '#000000FF'
+        },
+        {
+          point: 255,
+          color: '#FFFFFFFF'
+        }
+      ],
+      start: {
+        x: 0,
+        y: 0
+      },
+      end: {
+        x: props.width || 100,
+        y: props.height || 100
+      }
+    }
+  }
+}
+
+const emitGradientChange = () => {
+  if (!internalGradient.value) return
+  gradientColor.value = klona(internalGradient.value)
+}
+
+// 外部 v-model 同步到内部对象
+watch(
+  () => gradientColor.value,
+  (val) => {
+    if (!val) {
+      internalGradient.value = null
+      return
+    }
+    if (typeof val === 'string') {
+      internalGradient.value = parseCssGradient(val) || null
+    } else {
+      internalGradient.value = klona(val)
+      type.value = val.gradientType === 'gradient' ? 'gradient' : 'advanced'
+    }
+  },
+  { immediate: true }
+)
+
+watch(type, (val) => {
+  if (!internalGradient.value) return
+  internalGradient.value.gradientType = val === 'gradient' ? 'gradient' : 'advanced'
+  // 保留前2个渐变点
+  if (val === 'gradient') {
+    internalGradient.value.type = 'linear'
+    const points = internalGradient.value.points
+    if (points && points.length > 2) {
+      internalGradient.value.points = points.slice(0, 2)
+    }
+  }
+})
+
+// 打开弹窗时,根据当前是否有渐变自动切换 tab
+const open = () => {
+  if (gradientColor.value) {
+    ensureGradient()
+  }
+  show.value = true
+}
+
+defineExpose({
+  open
+})
+
+// 触发器样式:优先使用渐变,否则使用纯色
+const triggerStyle = computed(() => {
+  if (gradientColor.value) {
+    if (typeof gradientColor.value === 'string') {
+      return {
+        backgroundImage: gradientColor.value as string
+      }
+    }
+    return {
+      backgroundImage: generateCssGradient(gradientColor.value as GradientColor, props.width, props.height)
+    }
+  }
+
+  return {
+    backgroundColor: pureColor.value || '#ffffff00'
+  }
+})
+
+// 预览渐变色
+const previewGradient = computed(() => {
+  return { background: internalGradient.value ? generateCssGradient(internalGradient.value, props.width, props.height) : '#ffffff00' }
+})
+
+// 基础渐变:方向
+const basicDirection = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.direction || 'horizontal'
+  },
+  set(val: 'horizontal' | 'vertical') {
+    ensureGradient()
+    if (!internalGradient.value) return
+    internalGradient.value.direction = val
+    internalGradient.value.type = 'linear'
+  }
+})
+
+// 基础渐变:起始/结束颜色
+const basicStartColor = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.points?.[0]?.color || '#000000FF'
+  },
+  set(val: string) {
+    ensureGradient()
+    if (!internalGradient.value?.points) return
+    internalGradient.value.points[0].color = val
+  }
+})
+
+const basicEndColor = computed({
+  get() {
+    ensureGradient()
+    const pts = internalGradient.value?.points || []
+    return pts[pts.length - 1]?.color || '#FFFFFFFF'
+  },
+  set(val: string) {
+    ensureGradient()
+    if (!internalGradient.value?.points) return
+    const pts = internalGradient.value.points
+    pts[pts.length - 1].color = val
+  }
+})
+
+// 基础渐变:起始/结束点(0~255),若起始 > 结束,则交换
+const basicStartPos = computed({
+  get() {
+    ensureGradient()
+    const pos = internalGradient.value?.points?.[0]?.point
+    return pos ?? 0
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value?.points) return
+    const pts = internalGradient.value.points
+    let v = Math.min(255, Math.max(0, val))
+
+    pts[0].point = v
+  }
+})
+
+const basicEndPos = computed({
+  get() {
+    ensureGradient()
+    const pts = internalGradient.value?.points
+    const pos = pts?.[pts.length - 1]?.point
+    return pos ?? 255
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value?.points) return
+    const pts = internalGradient.value.points
+    let v = Math.min(255, Math.max(0, val))
+
+    pts[pts.length - 1].point = v
+  }
+})
+
+// 高级渐变:类型
+const advancedType = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.type || 'linear'
+  },
+  set(val: GradientColor['type']) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    internalGradient.value.type = val
+  }
+})
+
+// 线性渐变:起始/结束坐标(0~100,对应控件宽高百分比)
+const linearStartX = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.start?.x ?? 0
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.start) internalGradient.value.start = { x: 0, y: 0 }
+    internalGradient.value.start.x = Math.max(0, val)
+  }
+})
+
+const linearStartY = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.start?.y ?? 0
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.start) internalGradient.value.start = { x: 0, y: 0 }
+    internalGradient.value.start.y = Math.max(0, val)
+  }
+})
+
+const linearEndX = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.end?.x ?? 100
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.end) internalGradient.value.end = { x: 100, y: 100 }
+    internalGradient.value.end.x = Math.max(0, val)
+  }
+})
+
+const linearEndY = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.end?.y ?? 100
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.end) internalGradient.value.end = { x: 100, y: 100 }
+    internalGradient.value.end.y = Math.max(0, val)
+  }
+})
+
+// 径向/锥形渐变:中心点(0~100,默认 50)
+const centerX = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.center?.x ?? 50
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.center) internalGradient.value.center = { x: 50, y: 50 }
+    internalGradient.value.center.x = Math.max(0, val)
+  }
+})
+
+const centerY = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.center?.y ?? 50
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.center) internalGradient.value.center = { x: 50, y: 50 }
+    internalGradient.value.center.y = Math.max(0, val)
+  }
+})
+
+// 锥形渐变:角度 0~3600
+const coneStartAngle = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.angle?.start ?? 0
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.angle) internalGradient.value.angle = { start: 0, end: 360 }
+    internalGradient.value.angle.start = Math.min(3600, Math.max(0, val))
+  }
+})
+
+const coneEndAngle = computed({
+  get() {
+    ensureGradient()
+    return internalGradient.value?.angle?.end ?? 360
+  },
+  set(val: number) {
+    ensureGradient()
+    if (!internalGradient.value) return
+    if (!internalGradient.value.angle) internalGradient.value.angle = { start: 0, end: 360 }
+    internalGradient.value.angle.end = Math.min(3600, Math.max(0, val))
+  }
+})
+
+// 高级渐变:渐变点列表(最少 2,最多 16)
+const pointsList = computed({
+  get() {
+    ensureGradient()
+    if (!internalGradient.value!.points || internalGradient.value!.points.length < 2) {
+      internalGradient.value!.points = [
+        { color: '#000000FF', point: 0 },
+        { color: '#FFFFFFFF', point: 255 }
+      ]
+    }
+    return internalGradient.value!.points!
+  },
+  set(val) {
+    if (!internalGradient.value) return
+    internalGradient.value.points = val
+  }
+})
+
+const sortPointsByPosition = () => {
+  if (!internalGradient.value?.points) return
+  internalGradient.value.points = internalGradient.value.points.sort((a, b) => {
+    const pa = a.point ?? a.point
+    const pb = b.point ?? b.point
+    return pa - pb
+  })
+}
+
+const addPoint = () => {
+  ensureGradient()
+  if (!internalGradient.value?.points) return
+  if (internalGradient.value.points.length >= 16) return
+
+  const last = internalGradient.value.points[internalGradient.value.points.length - 1]
+  const lastPos = last.point ?? 255
+  const newPos = Math.min(255, lastPos + 10)
+
+  internalGradient.value.points.push({
+    color: '#FFFFFFFF',
+    point: newPos
+  })
+  sortPointsByPosition()
+}
+
+const removePoint = (index: number) => {
+  ensureGradient()
+  if (!internalGradient.value?.points) return
+  if (internalGradient.value.points.length <= 2) return
+  internalGradient.value.points.splice(index, 1)
+}
+
+const onChangeAdvancedType = (val) => {
+  ensureGradient()
+  if (!internalGradient.value) return
+  if (val === 'linear') {
+    internalGradient.value.end = {
+      x: props.width || 100,
+      y: props.height || 100
+    }
+  } else {
+    internalGradient.value.center = {
+      x: (props.width || 100) / 2,
+      y: (props.height || 100) / 2
+    }
+  }
+}
+
+const submit = () => {
+  emitGradientChange()
+  show.value = false
+}
+</script>
+
+<style scoped>
+.trigger {
+  width: 24px;
+  height: 24px;
+  border-radius: 4px;
+  cursor: pointer;
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+  background-repeat: repeat;
+  margin-right: 10px;
+}
+</style>

+ 1 - 0
src/renderer/src/components/index.ts

@@ -5,3 +5,4 @@ export * from './SplitterCollapse'
 export { default as ViewTitle } from './ViewTitle/index.vue'
 export { default as IconButton } from './IconButton/index.vue'
 export { default as HightLight } from './HightLight/index.vue'
+export { default as ColorModal } from './ColorModal/index.vue'

+ 16 - 4
src/renderer/src/lvgl-widgets/hooks/useWidgetStyle.ts

@@ -2,6 +2,7 @@ import componentMap from '..'
 import { useProjectStore } from '@/store/modules/project'
 import { assign } from 'lodash-es'
 import { ref, watch } from 'vue'
+import { generateCssGradient } from '@/utils'
 
 import type { IStyleConfig, PartItem } from '../type'
 import type { CSSProperties } from 'vue'
@@ -50,12 +51,17 @@ type StyleMap = Record<
  * @param value
  * @returns
  */
-export const getStyle = (key, value) => {
+export const getStyle = (key, value, options?: any) => {
   const style: CSSProperties = {}
   switch (key) {
     // 背景样式
     case 'background': {
-      style.backgroundColor = value?.color
+      if (typeof value?.color === 'object') {
+        // 渐变颜色
+        style.background = generateCssGradient(value?.color, options?.width, options?.height)
+      } else if (typeof value?.color === 'string') {
+        style.backgroundColor = value?.color
+      }
       break
     }
     // 文字样式
@@ -242,7 +248,7 @@ export const useWidgetStyle = (param: StyleParam) => {
   }
 
   const handleStyle = () => {
-    const { part, state, styles = [] } = param.props
+    const { part, state, styles = [], props } = param.props
     parts.forEach((partItem) => {
       styleMap.value[`${partItem.name}Style`] = {}
       // 从控件配置的样式列表查找对应样式
@@ -266,7 +272,13 @@ export const useWidgetStyle = (param: StyleParam) => {
       // 遍历style 获取样式
       Object.keys(style || {}).forEach((key) => {
         // 合并样式
-        assign(styleMap.value[`${partItem.name}Style`], getStyle(key, style?.[key]))
+        assign(
+          styleMap.value[`${partItem.name}Style`],
+          getStyle(key, style?.[key], {
+            width: props?.width,
+            height: props?.height
+          })
+        )
 
         // 获取背景图片src及颜色
         if (key === 'background' && style?.[key]?.image?.imgId) {

+ 259 - 0
src/renderer/src/lvgl-widgets/scale/Scale.vue

@@ -0,0 +1,259 @@
+<template>
+  <div :style="{
+    ...styleMap?.mainStyle
+  }" class="relative w-full h-full box-border overflow-hidden relative">
+
+    <ImageBg v-if="styleMap?.mainStyle?.imageSrc" :src="styleMap?.mainStyle?.imageSrc"
+      :imageStyle="styleMap?.mainStyle?.imageStyle" />
+
+    <div class="absolute inset-0 w-full h-full" :style="{ transform: `rotate(${props.rotate}deg)` }">
+      <svg :viewBox="`0 0 ${width} ${height}`" preserveAspectRatio="xMidYMid meet" class="w-full h-full block"
+        xmlns="http://www.w3.org/2000/svg">
+        <defs>
+          <!-- 背景条图片 pattern:保持图像原始尺寸,居中显示 -->
+          <pattern v-if="styleMap?.mainStyle?.curve?.imageSrc" :id="`${arcId}-bg-pattern`" patternUnits="userSpaceOnUse"
+            :width="width" :height="height" x="0" y="0">
+            <image :href="styleMap.mainStyle.curve.imageSrc" transform="translate(-50%, -50%)" />
+          </pattern>
+          <!-- 进度值条图片 pattern:保持图像原始尺寸,居中显示 -->
+          <pattern v-if="styleMap?.indicatorStyle?.curve?.imageSrc" :id="`${arcId}-indicator-pattern`"
+            patternUnits="userSpaceOnUse" :width="width" :height="height" x="0" y="0">
+            <image :href="styleMap.indicatorStyle.curve.imageSrc" transform="translate(-50%, -50%)" />
+          </pattern>
+        </defs>
+        <!-- 背景条:绘制完整的起始到结束角度 -->
+        <path :d="bgPath" fill="none"
+          :stroke="styleMap?.mainStyle?.curve?.imageSrc ? `url(#${arcId}-bg-pattern)` : (styleMap?.mainStyle?.curve?.color || '#eeeeee')"
+          :stroke-width="styleMap?.mainStyle?.curve?.width ?? 1"
+          :stroke-linecap="styleMap?.mainStyle?.curve?.radius ? 'round' : 'butt'"
+          :opacity="styleMap?.mainStyle?.curve?.opacity" />
+
+        <!-- 进度值条 -->
+        <path :d="valuePath" fill="none"
+          :stroke="styleMap?.indicatorStyle?.curve?.imageSrc ? `url(#${arcId}-indicator-pattern)` : (styleMap?.indicatorStyle?.curve?.color || '#2092f5')"
+          :stroke-width="styleMap?.indicatorStyle?.curve?.width ?? 1"
+          :stroke-linecap="styleMap?.indicatorStyle?.curve?.radius ? 'round' : 'butt'"
+          :opacity="styleMap?.indicatorStyle?.curve?.opacity" />
+
+        <!-- 进度圆点:无背景图时用实心圆 -->
+        <circle v-if="dotPos && !styleMap?.knobStyle?.imageSrc" :cx="dotPos.x" :cy="dotPos.y" :r="knobRadius.radius"
+          :fill="styleMap?.knobStyle?.backgroundColor || '#2092f5'" />
+      </svg>
+
+      <!-- 进度圆点背景图:与 SVG 同坐标系,用 ImageBg 统一实现 -->
+      <div v-if="dotPos && styleMap?.knobStyle?.imageSrc" class="absolute overflow-hidden pointer-events-none"
+        :style="knobOverlayStyle">
+        <ImageBg :src="styleMap.knobStyle.imageSrc" :imageStyle="styleMap.knobStyle.imageStyle" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
+import { useProjectStore } from '@/store/modules/project';
+
+import ImageBg from '../ImageBg.vue';
+
+const arcId = ref('arc-' + Math.random().toString(36).slice(2))
+
+const props = defineProps<{
+  width: number
+  height: number
+  styles: any
+  state?: string
+  part?: string
+  mode: 'normal' | 'symmetrical' | 'reverse'
+  rangeStart: number
+  rangeEnd: number
+  angleStart: number
+  angleEnd: number
+  value: number
+  rotate: number
+  // 偏移开关
+  rotateOffset?: boolean
+  // 偏移量(度数)
+  rotateOffsetValue?: number
+}>()
+
+const projectStore = useProjectStore()
+
+const styleMap = useWidgetStyle({
+  widget: 'lv_arc',
+  props
+})
+
+const cx = computed(() => {
+  const { width, height } = props
+  const min = Math.min(width, height)
+  return min / 2
+})
+
+/** 弧线半径:与背景/进度条一致,留出边距避免贴边 */
+const trackRadius = computed(() => {
+  const { width, height } = props
+  const min = Math.min(width, height)
+  const curveWidth = styleMap.value?.mainStyle?.curve?.width ?? 12
+  return Math.max(4, min / 2 - curveWidth / 2 - knobRadius.value.padding)
+})
+
+/** 从 schema 的 styles 中解析 knob 的 padding.left 作为圆点半径 */
+const knobRadius = computed(() => {
+  const defaultIndicatorStyle = projectStore.globalStyle
+    ?.find((item) => item.widget === 'lv_arc')
+    ?.part?.find((item) => item.partName === 'indicator')?.defaultStyle
+
+  const defaultKnobStyle = projectStore.globalStyle
+    ?.find((item) => item.widget === 'lv_arc')
+    ?.part?.find((item) => item.partName === 'knob')?.defaultStyle
+
+  const indicatorStyle = props.styles?.find((s: any) => s.part?.name === 'indicator' && s.part.state === props.state)
+  const knobStyle = props.styles?.find((s: any) => s.part?.name === 'knob' && s.part.state === props.state)
+  const padding = knobStyle?.padding?.left ?? defaultKnobStyle?.padding?.left
+  const r = indicatorStyle?.curve?.width ?? defaultIndicatorStyle?.curve?.width ?? 12
+
+  return {
+    radius: r / 2 + padding,
+    padding: padding,
+    width: r
+  }
+})
+
+/**
+ * 极坐标转直角坐标
+ * SVG 角度习惯:0度在右侧(3点钟),需调整使0度在上方(12点钟)
+ */
+function polarToCartesian(
+  centerX: number,
+  centerY: number,
+  radius: number,
+  angleInDegrees: number
+) {
+  // LVGL 习惯通常 0 度在右侧,如果需要 0 度在上方,这里减去 90
+  // 这里我们遵循标准:angleStart 为输入值
+  const radians = ((angleInDegrees - 0) * Math.PI) / 180.0
+  return {
+    x: centerX + radius * Math.cos(radians),
+    y: centerY + radius * Math.sin(radians)
+  }
+}
+
+/**
+ * 生成 SVG 弧线路径指令
+ * 始终从 startAngle 顺时针画到 endAngle,得到“近整圆、缺口在底部”的环(图2效果)
+ */
+function describeArc(x: number, y: number, radius: number, startAngle: number, endAngle: number) {
+  const start = polarToCartesian(x, y, radius, startAngle)
+  const end = polarToCartesian(x, y, radius, endAngle)
+  // 顺时针扫过的角度(0~360)
+  let clockwiseSpan = (endAngle - startAngle) % 360
+  if (clockwiseSpan <= 0) clockwiseSpan += 360
+  if (clockwiseSpan === 0) clockwiseSpan = 360
+  const largeArcFlag = clockwiseSpan > 180 ? '1' : '0'
+  const sweepFlag = '1' // 顺时针
+
+  return [
+    'M',
+    start.x,
+    start.y,
+    'A',
+    radius,
+    radius,
+    0,
+    largeArcFlag,
+    sweepFlag,
+    end.x,
+    end.y
+  ].join(' ')
+}
+
+// 1. 背景路径
+const bgPath = computed(() => {
+  const r = trackRadius.value
+  return describeArc(cx.value, cx.value, r, props.angleStart, props.angleEnd)
+})
+
+// 顺时针从 angleStart 到 angleEnd 的弧度跨度(0~360)
+const clockwiseSpan = computed(() => {
+  const { angleStart, angleEnd } = props
+  let s = (angleEnd - angleStart) % 360
+  if (s <= 0) s += 360
+  return s === 0 ? 360 : s
+})
+
+// 2. 进度计算逻辑(与 describeArc 的顺时针方向一致)
+const progressData = computed(() => {
+  const { value, rangeStart, rangeEnd, angleStart, angleEnd, mode } = props
+
+  const rangeDiff = rangeEnd - rangeStart
+  const ratio =
+    Math.abs(rangeDiff) < 1e-9
+      ? 0
+      : Math.max(0, Math.min(1, (value - rangeStart) / rangeDiff))
+
+  const span = clockwiseSpan.value
+  let startA = angleStart
+  let endA = angleEnd
+
+  if (mode === 'normal') {
+    // 从 angleStart 顺时针填充 span * ratio
+    endA = angleStart + span * ratio
+  } else if (mode === 'reverse') {
+    startA = angleEnd - span * ratio
+    endA = angleEnd
+  } else if (mode === 'symmetrical') {
+    const midAngle = angleStart + span / 2
+    const midValue = (rangeStart + rangeEnd) / 2
+    const rangeToEnd = rangeEnd - midValue
+    const rangeToStart = midValue - rangeStart
+
+    if (value >= midValue && Math.abs(rangeToEnd) >= 1e-9) {
+      startA = midAngle
+      const halfRatio = Math.max(0, Math.min(1, (value - midValue) / rangeToEnd))
+      endA = midAngle + (span / 2) * halfRatio
+    } else if (value < midValue && Math.abs(rangeToStart) >= 1e-9) {
+      endA = midAngle
+      const halfRatio = Math.max(0, Math.min(1, (midValue - value) / rangeToStart))
+      startA = midAngle - (span / 2) * halfRatio
+    } else {
+      startA = midAngle
+      endA = midAngle
+    }
+  }
+
+  return { startA, endA }
+})
+
+// 3. 值路径(与背景弧同半径)
+const valuePath = computed(() => {
+  const { startA, endA } = progressData.value
+  if (startA === endA) return ''
+  return describeArc(cx.value, cx.value, trackRadius.value, startA, endA)
+})
+
+// 4. 圆点位置(与弧同半径)
+const dotPos = computed(() => {
+  // 原始结束角:normal / symmetrical 用 endA,reverse 用 startA
+  const baseAngle =
+    props.mode === 'reverse' ? progressData.value.startA : progressData.value.endA
+  // 仅对“结束值原点”做偏移,不整体偏移整条进度弧
+  const offset = props.rotateOffset ? props.rotateOffsetValue ?? 0 : 0
+  const angle = baseAngle + offset
+  return polarToCartesian(cx.value, cx.value, trackRadius.value, angle)
+})
+
+/** 进度圆点 overlay 的定位样式(与 viewBox 比例一致,用 ImageBg 时使用) */
+const knobOverlayStyle = computed(() => {
+  if (!dotPos.value || !props.width || !props.height) return {}
+  const { x, y } = dotPos.value
+  const r = knobRadius.value.radius
+  return {
+    left: `${((x - r) / props.width) * 100}%`,
+    top: `${((y - r) / props.height) * 100}%`,
+    width: `${(r * 2 / props.width) * 100}%`,
+    height: `${(r * 2 / props.height) * 100}%`,
+    borderRadius: '50%'
+  }
+})
+</script>

+ 386 - 0
src/renderer/src/lvgl-widgets/scale/index.ts

@@ -0,0 +1,386 @@
+import Scale from './Scale.vue'
+import icon from '../assets/icon/icon_22scale.svg'
+import type { IComponentModelConfig } from '../type'
+import i18n from '@/locales'
+import { flagOptions, stateOptions, stateList } from '@/constants'
+import defaultStyle from './style.json'
+
+export default {
+  label: i18n.global.t('scale'),
+  icon,
+  component: Scale,
+  key: 'lv_scale',
+  group: i18n.global.t('display'),
+  sort: 1,
+  defaultStyle,
+  parts: [
+    {
+      name: 'main',
+      stateList
+    },
+    {
+      name: 'items',
+      stateList
+    },
+    {
+      name: 'indicator',
+      stateList
+    },
+  ],
+  defaultSchema: {
+    name: 'arc',
+    props: {
+      x: 0,
+      y: 0,
+      width: 120,
+      height: 120,
+      flags: [],
+      states: [],
+      mode: 'normal',
+      rangeStart: 0,
+      rangeEnd: 100,
+      angleStart: 135,
+      angleEnd: 45,
+      value: 70,
+      rotate: 0,
+      // 旋转偏移开关
+      rotateOffset: false,
+      rotateOffsetValue: 60,
+      // 变化率
+      changeRate: 720
+    },
+    styles: [
+      {
+        part: {
+          name: 'main',
+          state: 'default'
+        },
+        background: {
+          color: '#ffffff00',
+          image: {
+            imgId: '',
+            color: '#ffffff00',
+            alpha: 255
+          }
+        },
+        border: {
+          color: '#2092f5ff',
+          width: 0,
+          radius: 6,
+          side: ['all']
+        },
+        padding: {
+          top: 20,
+          right: 20,
+          bottom: 20,
+          left: 20
+        },
+        curve: {
+          color: '#e0e0e0ff',
+          width: 12,
+          radius: true,
+          alpha: 255,
+          image: ''
+        },
+        shadow: {
+          color: '#2092f5ff',
+          x: 0,
+          y: 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.flags',
+        valueType: 'checkbox',
+        componentProps: {
+          options: flagOptions,
+          defaultCollapsed: true
+        }
+      },
+      {
+        label: '状态',
+        field: 'props.states',
+        valueType: 'checkbox',
+        componentProps: {
+          options: stateOptions,
+          defaultCollapsed: true
+        }
+      }
+    ],
+    coreProps: [
+      {
+        label: '模式',
+        field: 'props.mode',
+        valueType: 'select',
+        labelWidth: '80px',
+        componentProps: {
+          options: [
+            { label: 'normal', value: 'normal' },
+            { label: 'symmetrical', value: 'symmetrical' },
+            { label: 'reverse', value: 'reverse' }
+          ]
+        }
+      },
+      {
+        label: '范围',
+        valueType: 'group',
+        children: [
+          {
+            field: 'props.rangeStart',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -100000,
+              max: 100000
+            },
+            slots: {
+              prefix: 'S'
+            }
+          },
+          {
+            field: 'props.rangeEnd',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: -100000,
+              max: 100000
+            },
+            slots: {
+              prefix: 'E'
+            }
+          }
+        ]
+      },
+      {
+        label: '值',
+        field: 'props.value',
+        labelWidth: '80px',
+        valueType: 'number'
+      },
+      {
+        label: '角度',
+        valueType: 'group',
+        children: [
+          {
+            field: 'props.angleStart',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 0,
+              max: 360
+            },
+            slots: {
+              prefix: 'S'
+            }
+          },
+          {
+            field: 'props.angleEnd',
+            valueType: 'number',
+            componentProps: {
+              span: 12,
+              min: 0,
+              max: 360
+            },
+            slots: {
+              prefix: 'E'
+            }
+          }
+        ]
+      },
+      {
+        label: '旋转',
+        field: 'props.rotate',
+        labelWidth: '80px',
+        valueType: 'number'
+      },
+      {
+        label: '旋转偏移',
+        field: 'props.rotateOffset',
+        labelWidth: '80px',
+        valueType: 'switch'
+      },
+      {
+        valueType: 'dependency',
+        name: ['props.rotateOffset'],
+        dependency: (dependency) => {
+          return dependency?.['props.rotateOffset']
+            ? [
+                {
+                  label: '旋转偏移值',
+                  field: 'props.rotateOffsetValue',
+                  labelWidth: '80px',
+                  valueType: 'number',
+                  componentProps: {
+                    min: -360,
+                    max: 360
+                  }
+                }
+              ]
+            : []
+        }
+      },
+      {
+        label: '变化率',
+        field: 'props.changeRate',
+        valueType: 'number',
+        labelWidth: '80px',
+        componentProps: {
+          min: 1,
+          max: 10000
+        }
+      }
+    ],
+    // 组件样式
+    styles: [
+      {
+        label: '模块状态',
+        field: 'part',
+        valueType: 'part'
+      },
+      {
+        valueType: 'dependency',
+        name: ['part'],
+        dependency: ({ part }) => {
+          return part?.name === 'main'
+            ? [
+                {
+                  label: '背景',
+                  field: 'background',
+                  valueType: 'background'
+                },
+                {
+                  label: '边框',
+                  field: 'border',
+                  valueType: 'border'
+                },
+                {
+                  label: '内边距',
+                  field: 'padding',
+                  valueType: 'padding'
+                },
+                {
+                  label: '曲线',
+                  field: 'curve',
+                  valueType: 'line',
+                  componentProps: {
+                    hasImage: true
+                  }
+                },
+                {
+                  label: '阴影',
+                  field: 'shadow',
+                  valueType: 'shadow'
+                },
+                {
+                  label: '变换',
+                  field: 'transform',
+                  valueType: 'transform'
+                }
+              ]
+            : part?.name === 'indicator'
+              ? [
+                  {
+                    label: '曲线',
+                    field: 'curve',
+                    valueType: 'line',
+                    componentProps: {
+                      hasImage: true
+                    }
+                  }
+                ]
+              : [
+                  {
+                    label: '背景',
+                    field: 'background',
+                    valueType: 'background'
+                  },
+                  {
+                    label: '内边距',
+                    field: 'padding',
+                    valueType: 'padding',
+                    componentProps: {
+                      allInOne: true
+                    }
+                  }
+                ]
+        }
+      }
+    ]
+  }
+} as IComponentModelConfig

+ 89 - 0
src/renderer/src/lvgl-widgets/scale/style.json

@@ -0,0 +1,89 @@
+{
+  "widget": "lv_arc",
+  "styleName": "default",
+  "part": [
+    {
+      "partName": "main",
+      "defaultStyle": {
+        "background": {
+          "color": "#ffffff00",
+          "image": {
+            "imgId": "",
+            "color": "#ffffff00",
+            "alpha": 255
+          }
+        },
+        "border": {
+          "color": "#2092f5ff",
+          "width": 0,
+          "radius": 6,
+          "side": ["all"]
+        },
+        "padding": {
+          "top": 20,
+          "right": 20,
+          "bottom": 20,
+          "left": 20
+        },
+        "curve": {
+          "color": "#e0e0e0ff",
+          "width": 12,
+          "radius": true,
+          "alpha": 255,
+          "image": ""
+        },
+        "shadow": {
+          "color": "#2092f5ff",
+          "x": 0,
+          "y": 0,
+          "spread": 0,
+          "width": 0
+        },
+        "transform": {
+          "width": 0,
+          "height": 0,
+          "translateX": 0,
+          "translateY": 0,
+          "originX": 0,
+          "originY": 0,
+          "rotate": 0,
+          "scale": 256
+        }
+      },
+      "state": []
+    },
+    {
+      "partName": "indicator",
+      "defaultStyle": {
+        "curve": {
+          "color": "#2196f3ff",
+          "width": 12,
+          "radius": true,
+          "alpha": 255,
+          "image": ""
+        }
+      },
+      "state": []
+    },
+    {
+      "partName": "knob",
+      "defaultStyle": {
+        "background": {
+          "color": "#2196f3ff",
+          "image": {
+            "imgId": "",
+            "color": "#ffffff00",
+            "alpha": 255
+          }
+        },
+        "padding": {
+          "left": 5,
+          "right": 5,
+          "top": 5,
+          "bottom": 5
+        }
+      },
+      "state": []
+    }
+  ]
+}

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

@@ -146,6 +146,8 @@ export type GradientColor = {
   type: 'linear' | 'radial' | 'conical'
   // 渐变方向
   direction?: 'horizontal' | 'vertical'
+  // 渐变类型
+  gradientType: 'gradient' | 'advanced'
   // 中心坐标
   center?: {
     x: number
@@ -156,9 +158,19 @@ export type GradientColor = {
     start: number
     end: number
   }
+  // 起始坐标
+  start?: {
+    x: number
+    y: number
+  }
+  // 结束坐标
+  end?: {
+    x: number
+    y: number
+  }
   // 渐变点
   points?: {
-    // 渐变点坐标
+    // 渐变点坐标 0~255
     point: number
     // 渐变点颜色 带透明度
     color: string

+ 25 - 9
src/renderer/src/utils/color.ts

@@ -60,15 +60,28 @@ function hex8ToRgba(hex: string): string {
 }
 
 /**
- * 核心方法:将对象转换回 CSS 字符串
+ * 核心方法:将渐变对象转换回 CSS 字符串
  */
-export function generateCssGradient(config: GradientColor): string {
+export function generateCssGradient(
+  config: GradientColor,
+  width?: number,
+  height?: number
+): string {
   const { type, points, direction, angle, center } = config
 
   // 1. 处理颜色点部分
   const stops = (points || [])
-    .sort((a, b) => a.point - b.point) // 确保顺序正确
-    .map((p) => `${hex8ToRgba(p.color)} ${p.point}%`)
+    // 优先按照 LVGL 渐变位置(0~255)排序
+    .sort((a, b) => {
+      const pa = a.point
+      const pb = b.point
+      return pa - pb
+    })
+    .map((p) => {
+      // LVGL 渐变位置 0~255 映射为 CSS 0~100%
+      const percent = p.point !== undefined ? Math.round((p.point / 255) * 100) : p.point
+      return `${hex8ToRgba(p.color)} ${percent}%`
+    })
     .join(', ')
 
   // 2. 根据类型组装前缀和参数
@@ -85,15 +98,15 @@ export function generateCssGradient(config: GradientColor): string {
   }
 
   if (type === 'radial') {
-    const cx = center?.x ?? 50
-    const cy = center?.y ?? 50
+    const cx = center?.x && width ? (center.x / width) * 100 : 50
+    const cy = center?.y && height ? (center.y / height) * 100 : 50
     return `radial-gradient(circle at ${cx}% ${cy}%, ${stops})`
   }
 
   if (type === 'conical') {
     const startAngle = angle?.start ?? 0
-    const cx = center?.x ?? 50
-    const cy = center?.y ?? 50
+    const cx = center?.x && width ? (center.x / width) * 100 : 50
+    const cy = center?.y && height ? (center.y / height) * 100 : 50
     return `conic-gradient(from ${startAngle}deg at ${cx}% ${cy}%, ${stops})`
   }
 
@@ -116,6 +129,7 @@ export function parseCssGradient(css: string): GradientColor | null {
 
   const result: GradientColor = {
     type: rawType === 'conic' ? 'conical' : (rawType as any),
+    gradientType: rawType === 'conic' ? 'advanced' : 'gradient',
     points: []
   }
 
@@ -125,9 +139,11 @@ export function parseCssGradient(css: string): GradientColor | null {
     const stopMatch = arg.match(/(rgba?\(.*?\)|#[a-fA-F0-9]+)\s+(\d+)%/)
 
     if (stopMatch) {
+      const pointPercent = parseFloat(stopMatch[2])
       result?.points?.push({
         color: rgbaToHex8(stopMatch[1]),
-        point: parseFloat(stopMatch[2])
+        // 反向计算 LVGL 渐变位置 0~255
+        point: Math.round((pointPercent / 100) * 255)
       })
     } else {
       // 处理配置项 (第一个参数可能是角度、位置或形状)

+ 30 - 34
src/renderer/src/views/designer/config/property/components/StyleBackground.vue

@@ -1,37 +1,21 @@
 <template>
   <div>
     <el-form-item label="背景颜色" label-position="left" label-width="60px">
-      <ColorPicker
-        v-model:pureColor="pureColor"
-        v-model:gradientColor="gradientColor"
-        format="hex8"
-        picker-type="chrome"
-        :use-type="useType"
-      />
-      <span class="text-text-active">{{ modelValue?.color }}</span>
+      <ColorModal v-model:pureColor="pureColor as string" v-model:gradientColor="gradientColor" :useType="useType"
+        :width="width" :height="height" />
+      <span class="text-text-active">{{ typeof modelValue?.color === 'object' ? '渐变颜色' : modelValue?.color }}</span>
     </el-form-item>
     <el-form-item v-if="!onlyColor" label="背景图片" label-position="left" label-width="60px">
       <ImageSelect v-model="image" />
     </el-form-item>
     <el-form-item v-if="!onlyColor" label="图片遮罩" label-position="left" label-width="60px">
-      <ColorPicker
-        use-type="pure"
-        picker-type="chrome"
-        format="hex8"
-        v-model:pureColor="imageColor"
-        :disabled="!modelValue?.image"
-      />
+      <ColorPicker use-type="pure" picker-type="chrome" format="hex8" v-model:pureColor="imageColor"
+        :disabled="!modelValue?.image" />
       <span class="text-text-active">{{ imageColor }}</span>
     </el-form-item>
     <el-form-item v-if="!onlyColor" label="透明度" label-position="left" label-width="60px">
       <div class="w-full flex gap-20px items-center">
-        <el-slider
-          v-model="imageAlpha"
-          :disabled="!modelValue?.image"
-          :max="255"
-          :min="0"
-          style="flex: 1"
-        ></el-slider>
+        <el-slider v-model="imageAlpha" :disabled="!modelValue?.image" :max="255" :min="0" style="flex: 1"></el-slider>
         <span class="text-text-active inline w-30px cursor-pointer">
           {{ imageAlpha }}
         </span>
@@ -42,14 +26,17 @@
 
 <script setup lang="ts">
 import { computed } from 'vue'
-import { ColorPicker } from '@/components'
-import { parseCssGradient, generateCssGradient } from '@/utils'
+import { ColorPicker, ColorModal } from '@/components'
 import ImageSelect from './ImageSelect.vue'
+import { useProjectStore } from '@/store/modules/project'
 
 import type { GradientColor } from '@/lvgl-widgets/type'
 
-withDefaults(defineProps<{ useType?: 'pure' | 'gradient' | 'both'; onlyColor?: boolean }>(), {
-  useType: 'pure'
+withDefaults(defineProps<{
+  useType?: 'pure' | 'gradient' | 'both'
+  onlyColor?: boolean
+}>(), {
+  useType: 'both'
 })
 
 const modelValue = defineModel<{
@@ -60,10 +47,22 @@ const modelValue = defineModel<{
     alpha: number
   }
 }>('modelValue')
+
+const projectStore = useProjectStore()
+
+const width = computed(() => {
+  if (projectStore.activeWidget?.type === 'page') return projectStore.activeScreen?.width
+  return projectStore.activeWidget?.props?.width
+})
+const height = computed(() => {
+  if (projectStore.activeWidget?.type === 'page') return projectStore.activeScreen?.height
+  return projectStore.activeWidget?.props?.height
+})
+
 // 纯色
 const pureColor = computed({
   get() {
-    return modelValue.value?.color
+    return typeof modelValue.value?.color === 'string' ? modelValue.value?.color : undefined
   },
   set(val: string) {
     if (modelValue.value) {
@@ -74,14 +73,11 @@ const pureColor = computed({
 // 渐变
 const gradientColor = computed({
   get() {
-    return typeof modelValue.value?.color === 'object'
-      ? generateCssGradient(modelValue.value.color)
-      : ''
+    return typeof modelValue.value?.color === 'object' ? modelValue.value?.color : undefined
   },
-  set(val) {
-    const color = parseCssGradient(val!)
-    if (color && modelValue.value) {
-      modelValue.value.color = color
+  set(val: GradientColor) {
+    if (modelValue.value?.color) {
+      modelValue.value.color = val
     }
   }
 })