Quellcode durchsuchen

feat: 变量配置样式转换

jiaxing.liao vor 1 Monat
Ursprung
Commit
2e83249f8f

+ 0 - 0
.agent/variable/SKILL.md


+ 39 - 13
src/renderer/src/lvgl-widgets/hooks/useWidgetStyle.ts

@@ -4,6 +4,7 @@ import { assign } from 'lodash-es'
 import { computed, ref, watch } from 'vue'
 import { generateCssGradient } from '@/utils'
 import { DEFAULT_THEME_KEY } from '@/constants'
+import { findVariableById, getAllVariables, isVariableBound } from '@/utils/variableBinding'
 
 import type { IStyleConfig, PartItem } from '../type'
 import type { CSSProperties } from 'vue'
@@ -246,6 +247,27 @@ export const useWidgetStyle = (param: StyleParam) => {
     const { part, state, styles = [], props } = param.props
     const currentTheme = projectStore.project?.currentTheme || DEFAULT_THEME_KEY
     const getStyleTheme = (styleItem: any) => styleItem?.theme || DEFAULT_THEME_KEY
+    const allVariables = getAllVariables(
+      projectStore.project?.variables || [],
+      projectStore.activePage?.variables
+    )
+    const resolveVariableValue = (val: any): any => {
+      if (isVariableBound(val)) {
+        const variable = findVariableById(val.varId, allVariables)
+        return variable ? variable.value : val.originValue
+      }
+      if (Array.isArray(val)) {
+        return val.map((item) => resolveVariableValue(item))
+      }
+      if (val && typeof val === 'object') {
+        const result = {}
+        Object.keys(val).forEach((k) => {
+          result[k] = resolveVariableValue(val[k])
+        })
+        return result
+      }
+      return val
+    }
     parts.forEach((partItem) => {
       styleMap.value[`${partItem.name}Style`] = {}
       // 从控件配置的样式列表查找对应样式
@@ -282,13 +304,15 @@ export const useWidgetStyle = (param: StyleParam) => {
 
       // 合并到预设样式
       style = assign({}, presetStyle || {}, defaultStyle || {}, style || {})
+      const resolvedStyle = resolveVariableValue(style)
 
       // 遍历style 获取样式
-      Object.keys(style || {}).forEach((key) => {
+      Object.keys(resolvedStyle || {}).forEach((key) => {
+        const resolvedValue = resolvedStyle?.[key]
         // 合并样式
         assign(
           styleMap.value[`${partItem.name}Style`],
-          getStyle(key, style?.[key], {
+          getStyle(key, resolvedValue, {
             width: props?.width,
             height: props?.height,
             fontNameMap: projectStore.fontNameMap
@@ -296,23 +320,23 @@ export const useWidgetStyle = (param: StyleParam) => {
         )
 
         // 获取背景图片src及颜色
-        if (key === 'background' && style?.[key]?.image?.imgId) {
-          styleMap.value[`${partItem.name}Style`].imageSrc = getImageSrc(style?.[key]?.image?.imgId)
+        if (key === 'background' && resolvedValue?.image?.imgId) {
+          styleMap.value[`${partItem.name}Style`].imageSrc = getImageSrc(resolvedValue?.image?.imgId)
           styleMap.value[`${partItem.name}Style`].imageStyle = {
-            backgroundColor: style?.[key]?.image?.recolor,
-            opacity: (style?.[key]?.image?.alpha || 255) / 255
+            backgroundColor: resolvedValue?.image?.recolor,
+            opacity: (resolvedValue?.image?.alpha || 255) / 255
           }
         }
         // 图片样式
         if (key === 'imageStyle') {
           styleMap.value[`${partItem.name}Style`].image = {
-            backgroundColor: style?.[key]?.recolor,
-            opacity: (style?.[key]?.alpha || 255) / 255
+            backgroundColor: resolvedValue?.recolor,
+            opacity: (resolvedValue?.alpha || 255) / 255
           }
         }
         // 线段返回原本值,并解析 image 为 imageSrc/imageAlpha
         if (key === 'line') {
-          const lineData = style?.line
+          const lineData = resolvedValue
           styleMap.value[`${partItem.name}Style`].line = {
             color: lineData?.color ?? '',
             width: lineData?.width ?? 0,
@@ -322,7 +346,7 @@ export const useWidgetStyle = (param: StyleParam) => {
           }
         }
         if (key === 'curve') {
-          const curveData = style?.curve
+          const curveData = resolvedValue
           styleMap.value[`${partItem.name}Style`].curve = {
             color: curveData?.color ?? '',
             width: curveData?.width ?? 0,
@@ -333,10 +357,12 @@ export const useWidgetStyle = (param: StyleParam) => {
         }
       })
       // 处理行高 默认行高为1.2倍
-      if (style?.spacer?.lineHeight && style?.text?.size) {
-        const baseHeight = style.text.size * 1.2
+      const spacerValue = resolvedStyle?.spacer
+      const textValue = resolvedStyle?.text
+      if (spacerValue?.lineHeight && textValue?.size) {
+        const baseHeight = textValue.size * 1.2
         styleMap.value[`${partItem.name}Style`].lineHeight =
-          `${baseHeight + style.spacer.lineHeight}px`
+          `${baseHeight + spacerValue.lineHeight}px`
       }
     })
   }

+ 2 - 2
src/renderer/src/lvgl-widgets/label/index.ts

@@ -236,7 +236,7 @@ export default {
             valueType: 'number',
             componentProps: {
               span: 12,
-              min: -1,
+              min: 0,
               max: 10000
             },
             slots: {
@@ -248,7 +248,7 @@ export default {
             valueType: 'number',
             componentProps: {
               span: 12,
-              min: -1,
+              min: 0,
               max: 10000
             },
             slots: {

+ 38 - 24
src/renderer/src/lvgl-widgets/span-group/Config.vue

@@ -63,30 +63,43 @@
           />
         </el-form-item>
         <el-form-item label="">
-          <el-radio-group v-model="formData.text_decor">
-            <el-radio-button :value="LineEnum.LV_TEXT_DECOR_NONE">
-              <el-tooltip content="none">
-                <MdNotInterested size="14px" />
-              </el-tooltip>
-            </el-radio-button>
-            <el-radio-button :value="LineEnum.LV_TEXT_DECOR_UNDERLINE">
-              <el-tooltip content="underline">
-                <RxUnderline size="14px" />
-              </el-tooltip>
-            </el-radio-button>
-            <el-radio-button :value="LineEnum.LV_TEXT_DECOR_STRIKETHROUGH">
-              <el-tooltip content="strikethrough">
-                <RxStrikethrough size="14px" />
-              </el-tooltip>
-            </el-radio-button>
-            <el-radio-button
-              :value="LineEnum['LV_TEXT_DECOR_UNDERLINE | LV_TEXT_DECOR_STRIKETHROUGH']"
-            >
-              <el-tooltip content="strikethrough and underline">
-                <GoStrikethrough size="14px" />
-              </el-tooltip>
-            </el-radio-button>
-          </el-radio-group>
+          <VariableBindWrapper
+            v-model="formData.text"
+            :variable-config="{
+              type: 'enum',
+              enumMap: {
+                0: LineEnum.LV_TEXT_DECOR_NONE,
+                1: LineEnum.LV_TEXT_DECOR_UNDERLINE,
+                2: LineEnum.LV_TEXT_DECOR_STRIKETHROUGH,
+                3: LineEnum['LV_TEXT_DECOR_UNDERLINE | LV_TEXT_DECOR_STRIKETHROUGH']
+              }
+            }"
+          >
+            <el-radio-group v-model="formData.text_decor">
+              <el-radio-button :value="LineEnum.LV_TEXT_DECOR_NONE">
+                <el-tooltip content="none">
+                  <MdNotInterested size="14px" />
+                </el-tooltip>
+              </el-radio-button>
+              <el-radio-button :value="LineEnum.LV_TEXT_DECOR_UNDERLINE">
+                <el-tooltip content="underline">
+                  <RxUnderline size="14px" />
+                </el-tooltip>
+              </el-radio-button>
+              <el-radio-button :value="LineEnum.LV_TEXT_DECOR_STRIKETHROUGH">
+                <el-tooltip content="strikethrough">
+                  <RxStrikethrough size="14px" />
+                </el-tooltip>
+              </el-radio-button>
+              <el-radio-button
+                :value="LineEnum['LV_TEXT_DECOR_UNDERLINE | LV_TEXT_DECOR_STRIKETHROUGH']"
+              >
+                <el-tooltip content="strikethrough and underline">
+                  <GoStrikethrough size="14px" />
+                </el-tooltip>
+              </el-radio-button>
+            </el-radio-group>
+          </VariableBindWrapper>
         </el-form-item>
         <el-form-item label="文本">
           <LanguageInput type="textarea" v-model="formData.text" placeholder="请输入文本" />
@@ -112,6 +125,7 @@ import { RxUnderline, RxStrikethrough } from 'vue-icons-plus/rx'
 import { GoStrikethrough } from 'vue-icons-plus/go'
 import { useProjectStore } from '@/store/modules/project'
 import LanguageInput from '../components/LanguageInput.vue'
+import VariableBindWrapper from '@/views/designer/config/property/components/VariableBindWrapper.vue'
 
 const props = defineProps<{
   values: Ref<SpanItem[]>

+ 2 - 0
src/renderer/src/types/variables.d.ts

@@ -36,4 +36,6 @@ export type VariableBinding = {
   varId: string
   // 原始值
   originValue: any
+  // 来源类型
+  source: 'page' | 'global'
 }

+ 7 - 2
src/renderer/src/utils/variableBinding.ts

@@ -25,10 +25,15 @@ export function getPropertyDisplayValue(propValue: any, allVariables: VariableGr
 /**
  * 绑定变量到属性
  */
-export function bindVariableToProperty(propValue: any, varId: string): VariableBinding {
+export function bindVariableToProperty(
+  propValue: any,
+  varId: string,
+  source: 'page' | 'global'
+): VariableBinding {
   return {
     varId,
-    originValue: propValue
+    originValue: propValue,
+    source
   }
 }
 

+ 35 - 3
src/renderer/src/views/designer/config/VariableConfig.vue

@@ -141,6 +141,8 @@ import { ElMessageBox } from 'element-plus'
 import { variableType } from '@/constants'
 import { v4 } from 'uuid'
 import { useI18n } from 'vue-i18n'
+import { useProjectStore } from '@/store/modules/project'
+import { findVariableUsages } from '@/utils/variableBinding'
 
 interface Emits {
   (e: 'update:variables', val: VariableGroup[]): void
@@ -153,6 +155,7 @@ const props = defineProps<{
 }>()
 
 const { t } = useI18n()
+const projectStore = useProjectStore()
 
 const globalVariables = computed({
   get() {
@@ -179,8 +182,6 @@ const addVariables = (variables) => {
     id: v4(),
     name: `var_${variables.length + 1}`,
     value: '',
-    initialValue: '',
-    source: 'global',
     type: 'char'
   })
 }
@@ -198,6 +199,12 @@ const walkWidgets = (widgets: BaseWidget[] = [], callback: (widget: BaseWidget)
 const addPageVariables = () => {
   // todo: 打开弹窗,列出当前页面全部控件,然后根据控件属性选择变量
   // 选择属性后会自动绑定属性的变量类型,初始值使用属性的当前值
+  props.pageVariables?.push({
+    id: v4(),
+    name: '',
+    value: '',
+    type: 'char'
+  })
 }
 
 /**
@@ -212,7 +219,32 @@ const handleVariablesRemove = (
   varItem: Variable,
   scope: 'global' | 'page'
 ) => {
-  // todo: 判断当前变量是否被使用,若被使用需要提示确认操作后再删除
+  const deleteVariable = () => {
+    const index = variables.findIndex((item) => item.id === varItem.id)
+    if (index !== -1) {
+      variables.splice(index, 1)
+    }
+  }
+
+  const pageChildren = projectStore.activePage?.children || []
+  const usageCount = findVariableUsages(varItem.id, pageChildren).length
+
+  if (usageCount === 0) {
+    deleteVariable()
+    return
+  }
+
+  ElMessageBox.confirm(
+    `当前变量已被 ${usageCount} 处${scope === 'global' ? '控件属性' : '页面控件属性'}使用,删除后相关绑定将失效,是否继续删除?`,
+    '提示',
+    {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    }
+  ).then(() => {
+    deleteVariable()
+  })
 }
 
 /**

+ 13 - 1
src/renderer/src/views/designer/config/property/components/StyleBackground.vue

@@ -2,7 +2,7 @@
   <div>
     <el-form-item label="背景颜色" label-position="left" label-width="70px">
       <!-- 支持变量 -->
-      <VariableBindWrapper v-model="gradientColor" :variable-config="{ type: 'int32_t' }">
+      <VariableBindWrapper v-model="color" :variable-config="{ type: 'int32_t' }">
         <div class="flex items-center">
           <ColorModal
             v-model:pureColor="pureColor"
@@ -94,6 +94,18 @@ const height = computed(() => {
   return projectStore.activeWidget?.props?.height
 })
 
+// 颜色
+const color = computed({
+  get() {
+    return modelValue.value?.color
+  },
+  set(val: string) {
+    if (modelValue.value) {
+      modelValue.value.color = val
+    }
+  }
+})
+
 // 纯色
 const pureColor = computed({
   get() {

+ 1 - 1
src/renderer/src/views/designer/config/property/components/StyleShadow.vue

@@ -10,9 +10,9 @@
             picker-type="chrome"
             use-type="pure"
           />
+          <span class="text-text-active">{{ modelValue?.color }}</span>
         </div>
       </VariableBindWrapper>
-      <span class="text-text-active">{{ modelValue?.color }}</span>
     </el-form-item>
     <el-row :gutter="12">
       <el-col :span="12">

+ 19 - 5
src/renderer/src/views/designer/config/property/components/VariableBindWrapper.vue

@@ -6,7 +6,13 @@
 
     <el-popover v-if="canBindVariable" ref="popoverRef" placement="right" trigger="click">
       <template #reference>
-        <div class="cursor-pointer" :class="isBound ? 'active w-full flex items-center' : ''">
+        <div
+          class="cursor-pointer"
+          :class="[
+            isBound ? 'active w-full flex items-center' : '',
+            isGlobalBound ? 'global-bound' : ''
+          ]"
+        >
           <TbVariable :size="14" class="shrink-0" style="margin-top: 0.2em" />
           <span v-if="isBound" class="ml-4px flex-1 truncate">{{ boundVariableName }}</span>
         </div>
@@ -113,6 +119,10 @@ const boundVariableName = computed(() => {
   return variable?.name || ''
 })
 
+const isGlobalBound = computed(() => {
+  return isBound.value && modelValue.value?.source === 'global'
+})
+
 /**
  * 添加页面变量
  */
@@ -147,9 +157,9 @@ const addPageVariables = () => {
         type: props.variableConfig?.type as VariableType,
         value: val
       }
-
+      console.log(newVar)
       projectStore.activePage?.variables?.push(newVar)
-      modelValue.value = { varId: newVar.id, originValue: val }
+      modelValue.value = { varId: newVar.id, originValue: val, source: 'page' }
     })
     .catch(() => {
       ElMessage({
@@ -170,8 +180,8 @@ const currentBoundVarId = computed(() => {
  * 变量选择确认后的处理
  * @param varId
  */
-const handleVariableBindConfirm = (varId: string) => {
-  const boundValue = bindVariableToProperty(modelValue.value, varId)
+const handleVariableBindConfirm = (varId: string, source: 'page' | 'global') => {
+  const boundValue = bindVariableToProperty(modelValue.value, varId, source)
   modelValue.value = boundValue
   showVariableSelector.value = false
 }
@@ -210,5 +220,9 @@ const handleOpenVariableSelector = () => {
     color: var(--accent-blue);
     font-size: 14px;
   }
+
+  .global-bound {
+    color: var(--el-color-success);
+  }
 }
 </style>

+ 13 - 3
src/renderer/src/views/designer/config/property/components/VariableSelectorDialog.vue

@@ -74,7 +74,7 @@ const props = defineProps<{
 
 const emit = defineEmits<{
   'update:modelValue': [value: boolean]
-  confirm: [varId: string]
+  confirm: [varId: string, source: 'page' | 'global']
   cancel: []
 }>()
 
@@ -126,6 +126,16 @@ const selectedVariable = computed<Variable | undefined>(() => {
   return undefined
 })
 
+const selectedVariableSource = computed<'page' | 'global' | ''>(() => {
+  if (!selectedVarId.value) return ''
+  for (const group of filteredVariableGroups.value) {
+    if (group.variables.some((item) => item.id === selectedVarId.value)) {
+      return group.id === 'page-vars' ? 'page' : 'global'
+    }
+  }
+  return ''
+})
+
 const selectedVariableName = computed(() => selectedVariable.value?.name || '')
 
 const formatValue = (value: any): string => {
@@ -164,8 +174,8 @@ watch(filteredVariableGroups, () => {
 })
 
 const handleConfirm = () => {
-  if (selectedVariable.value) {
-    emit('confirm', selectedVarId.value)
+  if (selectedVariable.value && selectedVariableSource.value) {
+    emit('confirm', selectedVarId.value, selectedVariableSource.value)
     dialogVisible.value = false
   }
 }