Просмотр исходного кода

fix: 修改多语言应用到控件

jiaxing.liao 1 месяц назад
Родитель
Сommit
3472375ac3
27 измененных файлов с 278 добавлено и 81 удалено
  1. 0 1
      src/renderer/src/lvgl-widgets/barcode/index.ts
  2. 40 11
      src/renderer/src/lvgl-widgets/button-matrix/ButtonMatrix.vue
  3. 3 2
      src/renderer/src/lvgl-widgets/button-matrix/Config.vue
  4. 6 3
      src/renderer/src/lvgl-widgets/button/Button.vue
  5. 25 2
      src/renderer/src/lvgl-widgets/canvas/Canvas.vue
  6. 2 6
      src/renderer/src/lvgl-widgets/canvas/Config.vue
  7. 1 1
      src/renderer/src/lvgl-widgets/checkbox/Checkbox.vue
  8. 104 0
      src/renderer/src/lvgl-widgets/components/LanguageInput.vue
  9. 2 1
      src/renderer/src/lvgl-widgets/dropdown/Config.vue
  10. 11 8
      src/renderer/src/lvgl-widgets/hooks/useLanguage.ts
  11. 1 1
      src/renderer/src/lvgl-widgets/image-button/ImageButton.vue
  12. 5 2
      src/renderer/src/lvgl-widgets/label/Label.vue
  13. 2 1
      src/renderer/src/lvgl-widgets/list/Config.vue
  14. 3 3
      src/renderer/src/lvgl-widgets/menu/Config.vue
  15. 2 1
      src/renderer/src/lvgl-widgets/message/Config.vue
  16. 1 4
      src/renderer/src/lvgl-widgets/qrcode/index.ts
  17. 2 2
      src/renderer/src/lvgl-widgets/roller/Config.vue
  18. 2 1
      src/renderer/src/lvgl-widgets/scale/Config.vue
  19. 2 6
      src/renderer/src/lvgl-widgets/span-group/Config.vue
  20. 14 4
      src/renderer/src/lvgl-widgets/span-group/SpanGroup.vue
  21. 2 2
      src/renderer/src/lvgl-widgets/table/Config.vue
  22. 17 7
      src/renderer/src/lvgl-widgets/table/Table.vue
  23. 3 2
      src/renderer/src/lvgl-widgets/tabview/Config.vue
  24. 1 1
      src/renderer/src/lvgl-widgets/textarea/Textarea.vue
  25. 2 2
      src/renderer/src/lvgl-widgets/window/Window.vue
  26. 16 2
      src/renderer/src/store/modules/project.ts
  27. 9 5
      src/renderer/src/views/designer/sidebar/components/LanguageModal.vue

+ 0 - 1
src/renderer/src/lvgl-widgets/barcode/index.ts

@@ -200,7 +200,6 @@ export default {
         field: 'props.text',
         valueType: 'textarea',
         componentProps: {
-          supportLangues: true,
           onValueChange: (_value: string, formData: any) => {
             syncBarcodeWidth(formData)
           }

+ 40 - 11
src/renderer/src/lvgl-widgets/button-matrix/ButtonMatrix.vue

@@ -1,30 +1,49 @@
 <template>
-  <div :style="styleMap?.mainStyle" class="w-full h-full box-border flex flex-col overflow-hidden relative">
-    <ImageBg v-if="styleMap?.mainStyle?.imageSrc" :src="styleMap?.mainStyle?.imageSrc"
-      :image-color-style="styleMap?.mainStyle?.imageStyle" />
-    <div v-for="(row, index) in group || []" :key="index" class="flex-1 w-full flex items-center"
-      :style="{ columnGap: styleMap?.mainStyle?.columnGap }">
-      <div class="h-full flex items-center justify-center overflow-hidden whitespace-pre! relative"
-        v-for="(item, index) in row || []" :key="`${index}-${index}`" :style="{
+  <div
+    :style="styleMap?.mainStyle"
+    class="w-full h-full box-border flex flex-col overflow-hidden relative"
+  >
+    <ImageBg
+      v-if="styleMap?.mainStyle?.imageSrc"
+      :src="styleMap?.mainStyle?.imageSrc"
+      :image-color-style="styleMap?.mainStyle?.imageStyle"
+    />
+    <div
+      v-for="(row, index) in resolvedGroup"
+      :key="index"
+      class="flex-1 w-full flex items-center"
+      :style="{ columnGap: styleMap?.mainStyle?.columnGap }"
+    >
+      <div
+        class="h-full flex items-center justify-center overflow-hidden whitespace-pre! relative"
+        v-for="(item, index) in row || []"
+        :key="`${index}-${index}`"
+        :style="{
           ...(isCustom ? getBtnStyle(item) || styleMap?.itemsStyle : styleMap?.itemsStyle),
           ...getBtnFlex(
             item.width,
             row.map((item) => item.width)
           )
-        }">
-        <ImageBg v-if="getBtnStyle(item)?.imageSrc" :src="getBtnStyle(item)?.imageSrc"
-          :image-color-style="getBtnStyle(item)?.imageStyle" />
-        <span class="z-2">{{ item.text }}</span>
+        }"
+      >
+        <ImageBg
+          v-if="getBtnStyle(item)?.imageSrc"
+          :src="getBtnStyle(item)?.imageSrc"
+          :image-color-style="getBtnStyle(item)?.imageStyle"
+        />
+        <span class="z-2" :style="item.resolvedStyle" v-html="item.resolvedText.text"></span>
       </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
+import { computed } from 'vue'
 import { useWidgetStyle, getStyle } from '../hooks/useWidgetStyle'
 import { ButtonItem } from './data'
 import { isEmpty, assign } from 'lodash-es'
 import { useProjectStore } from '@/store/modules/project'
+import { useLanguage } from '../hooks/useLanguage'
 
 import ImageBg from '../ImageBg.vue'
 
@@ -41,6 +60,16 @@ const styleMap = useWidgetStyle({
   widget: 'lv_buttonmatrix',
   props
 })
+const { resolveText, getResolvedFontStyle } = useLanguage()
+const resolvedGroup = computed(() =>
+  (props.group || []).map((row) =>
+    (row || []).map((item) => ({
+      ...item,
+      resolvedText: resolveText(item?.text, true),
+      resolvedStyle: getResolvedFontStyle(item?.text)
+    }))
+  )
+)
 
 const projectStore = useProjectStore()
 

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

@@ -50,10 +50,10 @@
   >
     <el-form ref="form" :model="formData" :rules="rules" hide-required-asterisk>
       <el-form-item label="名称" prop="name">
-        <el-input v-model="formData.name"></el-input>
+        <el-input v-model="formData.name" spellcheck="false" />
       </el-form-item>
       <el-form-item label="文本" prop="text">
-        <el-input type="textarea" :rows="1" v-model="formData.text"></el-input>
+        <LanguageInput type="textarea" :rows="1" v-model="formData.text" />
       </el-form-item>
       <!-- <el-form-item label="宽度">
         <input-number
@@ -122,6 +122,7 @@ import { get, assign } from 'lodash-es'
 import StylePart from '@/views/designer/config/property/components/StylePart.vue'
 import CusFormItem from '@/views/designer/config/property/CusFormItem.vue'
 import { IoColorPaletteOutline } from 'vue-icons-plus/io'
+import LanguageInput from '../components/LanguageInput.vue'
 
 const props = defineProps<{
   values: Ref<{

+ 6 - 3
src/renderer/src/lvgl-widgets/button/Button.vue

@@ -15,7 +15,7 @@
     <span
       ref="txtRef"
       class="z-2 lv_button_txt whitespace-pre!"
-      :style="textStyle"
+      :style="{ fontFamily: textStyle.fontFamily, fontSize: textStyle.fontSize }"
       v-html="innerText"
     ></span>
   </div>
@@ -45,7 +45,7 @@ const { resolveText, getTextStyle } = useLanguage()
 
 // 处理symbol图标
 const innerText = computed(() => {
-  const text = resolveText(props.text).text
+  const text = resolveText(props.text, true).text
   const pattern = /\{LV[^}]*\}/g
   const matches = text.match(pattern)
   const map: Record<string, string> = {}
@@ -57,7 +57,10 @@ const innerText = computed(() => {
   return text.replace(pattern, (match) => `<i class="lvgl-icon not-italic" >${map[match]}</i>`)
 })
 
-const textStyle = getTextStyle(() => undefined, props.text)
+const textStyle = getTextStyle(
+  () => undefined,
+  () => props.text
+)
 
 const styleMap = useWidgetStyle({
   widget: 'lv_button',

+ 25 - 2
src/renderer/src/lvgl-widgets/canvas/Canvas.vue

@@ -29,9 +29,10 @@
           :fill="el.props.font_color"
           :font-size="el.props.font_size"
           :text-decoration="textDecorMap[el.props.text_decor] || 'none'"
+          :style="getCanvasTextStyle(el.props)"
           dominant-baseline="hanging"
         >
-          {{ el.props.text }}
+          {{ getCanvasText(el.props.text) }}
         </text>
 
         <!-- 圆弧 -->
@@ -86,6 +87,7 @@
 <script setup lang="ts">
 import { computed } from 'vue'
 import { useProjectStore } from '@/store/modules/project'
+import { useLanguage } from '../hooks/useLanguage'
 
 type Point = { x: number; y: number }
 
@@ -164,6 +166,7 @@ const props = defineProps<{
 }>()
 
 const projectStore = useProjectStore()
+const { resolveText, getResolvedFontStyle } = useLanguage()
 
 const boxStyle = computed(() => {
   return {
@@ -199,6 +202,26 @@ const imageElements = computed(() => {
     })
 })
 
+const getCanvasText = (text: string) => resolveText(text).text
+
+const getCanvasTextStyle = (textProps: TextProps) => {
+  const resolvedStyle = getResolvedFontStyle(textProps.text)
+  if (!resolvedStyle.fontSize && textProps.font_size) {
+    resolvedStyle.fontSize = `${textProps.font_size}px`
+  }
+
+  if (!resolvedStyle.fontFamily && textProps.font_family && textProps.font_family !== 'xx') {
+    const font = projectStore.project?.resources.fonts.find(
+      (item) => item.id === textProps.font_family
+    )
+    if (font?.fileName) {
+      resolvedStyle.fontFamily = `'${font.fileName}'`
+    }
+  }
+
+  return resolvedStyle
+}
+
 // 直线/多边形 points 字符串
 const getLinePoints = (points: Point[] = []) => {
   return points.map((p) => `${p.x},${p.y}`).join(' ')
@@ -268,4 +291,4 @@ const getArcPath = (p: ArcProps) => {
 }
 </script>
 
-<style scoped></style>
+<style scoped></style>

+ 2 - 6
src/renderer/src/lvgl-widgets/canvas/Config.vue

@@ -199,12 +199,7 @@
             </el-radio-group>
           </el-form-item>
           <el-form-item label="文本">
-            <el-input
-              spellcheck="false"
-              type="textarea"
-              v-model="formData.props.text"
-              placeholder="请输入文本"
-            />
+            <LanguageInput type="textarea" v-model="formData.props.text" placeholder="请输入文本" />
           </el-form-item>
         </template>
 
@@ -476,6 +471,7 @@ import { RxUnderline, RxStrikethrough } from 'vue-icons-plus/rx'
 import { GoStrikethrough } from 'vue-icons-plus/go'
 import ImageSelect from '@/views/designer/config/property/components/ImageSelect.vue'
 import { useProjectStore } from '@/store/modules/project'
+import LanguageInput from '../components/LanguageInput.vue'
 
 type CanvasElementType = 'rect' | 'text' | 'image' | 'arc' | 'line' | 'triangle'
 

+ 1 - 1
src/renderer/src/lvgl-widgets/checkbox/Checkbox.vue

@@ -37,7 +37,7 @@ const props = defineProps<{
 }>()
 const { resolveText, getTextStyle } = useLanguage()
 const resolvedText = computed(() => resolveText(props.text))
-const textStyle = getTextStyle(() => undefined, props.text)
+const textStyle = getTextStyle(() => undefined, () => props.text)
 
 const styleMap = useWidgetStyle({
   widget: 'lv_checkbox',

+ 104 - 0
src/renderer/src/lvgl-widgets/components/LanguageInput.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="language-input relative w-full">
+    <el-input
+      ref="inputRef"
+      v-model="modelValue"
+      spellcheck="false"
+      :type="type"
+      :rows="rows"
+      v-bind="$attrs"
+      @focus="syncSelection"
+      @click="syncSelection"
+      @keyup="syncSelection"
+      @mouseup="syncSelection"
+    />
+    <LuLanguages
+      size="12px"
+      class="absolute cursor-pointer text-#909399 hover:text-#409eff"
+      :class="type === 'textarea' ? 'right-8px top-8px' : 'right-8px top-1/2 -translate-y-1/2'"
+      @click.stop="handleOpenLanguageModal"
+    />
+  </div>
+  <LanguageSelectModal ref="languageModalRef" @select="handleSelectLanguage" />
+</template>
+
+<script setup lang="ts">
+import { nextTick, ref } from 'vue'
+import { LuLanguages } from 'vue-icons-plus/lu'
+import LanguageSelectModal from '@/views/designer/config/property/components/LanguageSelectModal.vue'
+
+withDefaults(
+  defineProps<{
+    type?: 'text' | 'textarea'
+    rows?: number
+  }>(),
+  {
+    type: 'text',
+    rows: 2
+  }
+)
+
+const modelValue = defineModel<string>('modelValue')
+const inputRef = ref()
+const languageModalRef = ref<InstanceType<typeof LanguageSelectModal>>()
+const selection = ref({
+  start: 0,
+  end: 0
+})
+
+const getInputElement = () => {
+  const inputInstance = inputRef.value as any
+  return inputInstance?.input || inputInstance?.textarea || null
+}
+
+const syncSelection = () => {
+  const inputEl = getInputElement()
+  if (!inputEl) return
+
+  selection.value = {
+    start: inputEl.selectionStart ?? 0,
+    end: inputEl.selectionEnd ?? inputEl.selectionStart ?? 0
+  }
+}
+
+const handleOpenLanguageModal = () => {
+  syncSelection()
+  languageModalRef.value?.open()
+}
+
+const handleSelectLanguage = (languageKey: string) => {
+  if (!languageKey) return
+  insertContent(`#{${languageKey}}`)
+}
+
+const insertContent = async (content: string) => {
+  const currentValue = String(modelValue.value ?? '')
+  const start = selection.value.start ?? currentValue.length
+  const end = selection.value.end ?? start
+
+  modelValue.value = `${currentValue.slice(0, start)}${content}${currentValue.slice(end)}`
+
+  await nextTick()
+
+  const nextPosition = start + content.length
+  const inputEl = getInputElement()
+  inputEl?.focus?.()
+  inputEl?.setSelectionRange?.(nextPosition, nextPosition)
+  selection.value = {
+    start: nextPosition,
+    end: nextPosition
+  }
+}
+</script>
+
+<style scoped lang="less">
+.language-input {
+  ::v-deep(.el-input__wrapper) {
+    padding-right: 24px;
+  }
+
+  ::v-deep(.el-textarea__inner) {
+    padding-right: 24px;
+  }
+}
+</style>

+ 2 - 1
src/renderer/src/lvgl-widgets/dropdown/Config.vue

@@ -13,7 +13,7 @@
         :key="index"
       >
         <div class="w-full relative group/item">
-          <el-input spellcheck="false" v-model="modelValue[index]" />
+          <LanguageInput v-model="modelValue[index]" />
           <div class="absolute top--7px right--4px cursor-pointer" @click="handleDeleteItem(index)">
             <LuX size="14px" />
           </div>
@@ -26,6 +26,7 @@
 <script setup lang="ts">
 import { computed, type Ref } from 'vue'
 import { LuPlus, LuX } from 'vue-icons-plus/lu'
+import LanguageInput from '../components/LanguageInput.vue'
 
 const props = defineProps<{
   values: Ref<string[]>

+ 11 - 8
src/renderer/src/lvgl-widgets/hooks/useLanguage.ts

@@ -1,4 +1,4 @@
-import { computed, unref, type CSSProperties, type MaybeRefOrGetter } from 'vue'
+import { computed, toValue, type CSSProperties, type MaybeRefOrGetter } from 'vue'
 import { useProjectStore } from '@/store/modules/project'
 import type { Language } from '@/types/language'
 
@@ -10,8 +10,8 @@ type ResolvedLanguageText = {
 
 const TOKEN_REGEXP = /#\{([A-Za-z_][A-Za-z0-9_]*)\}/g
 
-const normalizeFontSize = (fontSize?: string) => {
-  const value = String(fontSize || '').trim()
+const normalizeFontSize = (fontSize?: string | number | null) => {
+  const value = String(fontSize ?? '').trim()
   if (!value) return undefined
   return /^\d+(\.\d+)?$/.test(value) ? `${value}px` : value
 }
@@ -36,7 +36,7 @@ export const useLanguage = () => {
     )
   }
 
-  const resolveText = (source?: string | null): ResolvedLanguageText => {
+  const resolveText = (source?: string | null, useLabel = false): ResolvedLanguageText => {
     const raw = String(source ?? '')
     let overrideFontFamily: string | undefined
     let overrideFontSize: string | undefined
@@ -59,7 +59,9 @@ export const useLanguage = () => {
         overrideFontSize = normalizeFontSize(value.fontSize)
       }
 
-      return value.value || ''
+      return useLabel
+        ? `<span style="font-family: ${overrideFontFamily}; font-size: ${overrideFontSize}">${value.value || ''}</span>`
+        : value.value || ''
     })
 
     return {
@@ -71,6 +73,7 @@ export const useLanguage = () => {
 
   const getResolvedFontStyle = (source?: string | null): CSSProperties => {
     const resolved = resolveText(source)
+    console.log('Resolved language text:', resolved)
     const style: CSSProperties = {}
 
     if (resolved.fontFamily) {
@@ -85,11 +88,11 @@ export const useLanguage = () => {
 
   const getTextStyle = (
     baseStyle: MaybeRefOrGetter<CSSProperties | undefined>,
-    source: string | null | undefined
+    source: MaybeRefOrGetter<string | null | undefined>
   ) => {
     return computed<CSSProperties>(() => {
-      const style = { ...(unref(baseStyle) || {}) }
-      Object.assign(style, getResolvedFontStyle(source))
+      const style = { ...(toValue(baseStyle) || {}) }
+      Object.assign(style, getResolvedFontStyle(toValue(source)))
 
       return style
     })

+ 1 - 1
src/renderer/src/lvgl-widgets/image-button/ImageButton.vue

@@ -45,7 +45,7 @@ const src = ref('')
 const projectStore = useProjectStore()
 const { resolveText, getTextStyle } = useLanguage()
 const resolvedText = computed(() => resolveText(props.text))
-const textStyle = getTextStyle(() => undefined, props.text)
+const textStyle = getTextStyle(() => undefined, () => props.text)
 
 const styleMap = useWidgetStyle({
   widget: 'lv_imagebutton',

+ 5 - 2
src/renderer/src/lvgl-widgets/label/Label.vue

@@ -109,7 +109,7 @@ const highlight = (html: string, start: number, end: number): string => {
 
 // 处理symbol图标
 const innerText = computed(() => {
-  const text = resolveText(props.text).text
+  const text = resolveText(props.text, true).text
   const pattern = /\{LV[^}]*\}/g
   const matches = text.match(pattern)
   const map: Record<string, string> = {}
@@ -133,7 +133,10 @@ const innerText = computed(() => {
   return html
 })
 
-const textStyle = getTextStyle(() => undefined, props.text)
+const textStyle = getTextStyle(
+  () => undefined,
+  () => props.text
+)
 
 const styleMap = useWidgetStyle({
   widget: 'lv_label',

+ 2 - 1
src/renderer/src/lvgl-widgets/list/Config.vue

@@ -32,7 +32,7 @@
           </el-select>
         </el-form-item>
         <el-form-item label="文本">
-          <el-input spellcheck="false" v-model="formData.text"></el-input>
+          <LanguageInput v-model="formData.text" />
         </el-form-item>
         <el-form-item v-if="formData.type === 'symbol'" label="图标">
           <el-input spellcheck="false" readonly @click="handleShowSymbolModal">
@@ -109,6 +109,7 @@ import { symbols } from '@/constants'
 
 import ImageSelect from '@/views/designer/config/property/components/ImageSelect.vue'
 import SymbolSelectModal from '@/views/designer/config/property/components/SymbolSelectModal.vue'
+import LanguageInput from '../components/LanguageInput.vue'
 
 const props = defineProps<{
   values: Ref<ListItem[]>

+ 3 - 3
src/renderer/src/lvgl-widgets/menu/Config.vue

@@ -30,10 +30,9 @@
           class="border-border border-solid border-1px p-4px bg-gray-800 mb-12px mr-12px rounded-4px"
         >
           <div class="flex items-center gap-8px">
-            <el-input
+            <LanguageInput
               v-model="section.name"
               type="textarea"
-              spellcheck="false"
               :rows="1"
               placeholder="子标题"
             />
@@ -83,7 +82,7 @@
           </el-select>
         </el-form-item>
         <el-form-item label="文本">
-          <el-input spellcheck="false" v-model="formData.name"></el-input>
+          <LanguageInput v-model="formData.name" />
         </el-form-item>
         <el-form-item v-if="formData.type === 'symbol'" label="图标">
           <el-input spellcheck="false" readonly @click="handleShowSymbolModal">
@@ -155,6 +154,7 @@ import { getNextIndex } from '@/utils'
 
 import ImageSelect from '@/views/designer/config/property/components/ImageSelect.vue'
 import SymbolSelectModal from '@/views/designer/config/property/components/SymbolSelectModal.vue'
+import LanguageInput from '../components/LanguageInput.vue'
 
 const props = defineProps<{
   values: Ref<any>

+ 2 - 1
src/renderer/src/lvgl-widgets/message/Config.vue

@@ -13,7 +13,7 @@
         :key="index"
       >
         <div class="w-full flex items-center gap-4px relative group/item">
-          <el-input spellcheck="false" v-model="children[index].text" />
+          <LanguageInput v-model="children[index].text" />
           <div class="cursor-pointer" @click="handleDeleteItem(index)">
             <LuTrash2 size="14px" />
           </div>
@@ -26,6 +26,7 @@
 <script setup lang="ts">
 import { computed, type Ref } from 'vue'
 import { LuPlus, LuTrash2 } from 'vue-icons-plus/lu'
+import LanguageInput from '../components/LanguageInput.vue'
 
 const props = defineProps<{
   values: Ref<{ text: string }[]>

+ 1 - 4
src/renderer/src/lvgl-widgets/qrcode/index.ts

@@ -124,10 +124,7 @@ export default {
       {
         label: '文本',
         field: 'props.text',
-        valueType: 'textarea',
-        componentProps: {
-          supportLangues: true
-        }
+        valueType: 'textarea'
       }
     ],
     styles: []

+ 2 - 2
src/renderer/src/lvgl-widgets/roller/Config.vue

@@ -21,10 +21,9 @@
           class="w-full mb-6px flex items-center gap-4px pr-12px"
         >
           <el-radio :value="index" class="mr-0! shrink-0" />
-          <el-input
+          <LanguageInput
             v-model="options[index]"
             class="flex-1"
-            spellcheck="false"
             placeholder="输入选项"
           />
           <LuTrash2 class="cursor-pointer shrink-0" size="14px" @click.stop="handleDelete(index)" />
@@ -37,6 +36,7 @@
 <script setup lang="ts">
 import { computed, watch, type Ref } from 'vue'
 import { LuPlus, LuTrash2 } from 'vue-icons-plus/lu'
+import LanguageInput from '../components/LanguageInput.vue'
 
 type RollerFormData = {
   props?: {

+ 2 - 1
src/renderer/src/lvgl-widgets/scale/Config.vue

@@ -33,7 +33,7 @@
     <el-dialog draggable append-to-body 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" placeholder="area_x" />
+          <LanguageInput v-model="formData.name" placeholder="area_x" />
         </el-form-item>
 
         <el-form-item :label="'起始值'">
@@ -96,6 +96,7 @@ import { getNextIndex } from '@/utils'
 
 import { ColorPicker } from '@/components'
 import { klona } from 'klona'
+import LanguageInput from '../components/LanguageInput.vue'
 
 export type AreaSection = {
   name: string

+ 2 - 6
src/renderer/src/lvgl-widgets/span-group/Config.vue

@@ -79,12 +79,7 @@
           </el-radio-group>
         </el-form-item>
         <el-form-item label="文本">
-          <el-input
-            spellcheck="false"
-            type="textarea"
-            v-model="formData.text"
-            placeholder="请输入文本"
-          />
+          <LanguageInput type="textarea" v-model="formData.text" placeholder="请输入文本" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -106,6 +101,7 @@ import { MdNotInterested } from 'vue-icons-plus/md'
 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'
 
 const props = defineProps<{
   values: Ref<SpanItem[]>

+ 14 - 4
src/renderer/src/lvgl-widgets/span-group/SpanGroup.vue

@@ -2,10 +2,10 @@
   <div :style="{ ...(styleMap?.mainStyle || {}), ...otherStyles }" class="w-full h-full">
     <span
       class="shrink-0 whitespace-pre!"
-      v-for="(item, index) in items"
+      v-for="(item, index) in resolvedItems"
       :key="index"
       :style="getSpanStyle(item)"
-      >{{ item.text }}</span
+      >{{ item.resolvedText.text }}</span
     >
   </div>
 </template>
@@ -14,6 +14,7 @@
 import { computed, CSSProperties } from 'vue'
 import { LineEnum } from './data'
 import { useWidgetStyle } from '../hooks/useWidgetStyle'
+import { useLanguage } from '../hooks/useLanguage'
 
 import type { SpanItem } from './data'
 
@@ -30,6 +31,14 @@ const styleMap = useWidgetStyle({
   widget: 'lv_span',
   props
 })
+const { resolveText, getResolvedFontStyle } = useLanguage()
+const resolvedItems = computed(() =>
+  (props.items || []).map((item) => ({
+    ...item,
+    resolvedText: resolveText(item.text),
+    resolvedStyle: getResolvedFontStyle(item.text)
+  }))
+)
 
 // 额外样式
 const otherStyles = computed(() => {
@@ -48,7 +57,7 @@ const otherStyles = computed(() => {
 })
 
 // 获取子项样式
-const getSpanStyle = (item: SpanItem) => {
+const getSpanStyle = (item: SpanItem & { resolvedStyle: CSSProperties }) => {
   const lineStyle =
     item.text_decor === LineEnum['LV_TEXT_DECOR_UNDERLINE | LV_TEXT_DECOR_STRIKETHROUGH']
       ? 'underline line-through'
@@ -61,7 +70,8 @@ const getSpanStyle = (item: SpanItem) => {
   return {
     color: item.text_color,
     fontSize: `${item.font_size}px`,
-    textDecoration: lineStyle
+    textDecoration: lineStyle,
+    ...item.resolvedStyle
   }
 }
 </script>

+ 2 - 2
src/renderer/src/lvgl-widgets/table/Config.vue

@@ -38,12 +38,11 @@
             :key="`${rowIndex}_${columnIndex}`"
             class="relative flex-1"
           >
-            <el-input
+            <LanguageInput
               type="textarea"
               :rows="1"
               :autosize="false"
               resize="none"
-              spellcheck="false"
               class="flex-1"
               v-model="row[columnIndex]"
             />
@@ -70,6 +69,7 @@
 import { computed, type Ref } from 'vue'
 import { LuX } from 'vue-icons-plus/lu'
 import { PiColumnsPlusRightLight, PiRowsPlusBottomLight } from 'vue-icons-plus/pi'
+import LanguageInput from '../components/LanguageInput.vue'
 
 const props = defineProps<{
   values: Ref<any>

+ 17 - 7
src/renderer/src/lvgl-widgets/table/Table.vue

@@ -6,7 +6,7 @@
         <thead>
           <tr>
             <th
-              v-for="(header, index) in headers"
+              v-for="(header, index) in resolvedHeaders"
               :key="index"
               :style="{
                 ...styleMap?.itemsStyle,
@@ -18,7 +18,7 @@
                 :src="styleMap?.itemsStyle?.imageSrc"
                 :imageStyle="styleMap?.itemsStyle?.imageStyle"
               />
-              <span class="relative z-99"> {{ header }}</span>
+              <span class="relative z-99" :style="header.style"> {{ header.text }}</span>
             </th>
           </tr>
         </thead>
@@ -34,7 +34,7 @@
                 :src="styleMap?.itemsStyle?.imageSrc"
                 :imageStyle="styleMap?.itemsStyle?.imageStyle"
               />
-              <span class="relative z-99"> {{ cell }}</span>
+              <span class="relative z-99" :style="cell.style"> {{ cell.text }}</span>
             </td>
           </tr>
         </tbody>
@@ -46,6 +46,7 @@
 <script setup lang="ts">
 import { computed } from 'vue'
 import { useWidgetStyle } from '../hooks/useWidgetStyle'
+import { useLanguage } from '../hooks/useLanguage'
 import ImageBg from '../ImageBg.vue'
 
 const props = defineProps<{
@@ -60,15 +61,24 @@ const styleMap = useWidgetStyle({
   widget: 'lv_table',
   props
 })
+const { resolveText, getResolvedFontStyle } = useLanguage()
 
 // 表头
-const headers = computed(() => {
-  return props.items?.[0] || []
-})
+const resolvedHeaders = computed(() =>
+  (props.items?.[0] || []).map((header) => ({
+    text: resolveText(header).text,
+    style: getResolvedFontStyle(header)
+  }))
+)
 
 // 内容
 const tableData = computed(() => {
-  return props.items?.slice(1) || []
+  return (props.items?.slice(1) || []).map((row) =>
+    (row || []).map((cell) => ({
+      text: resolveText(cell).text,
+      style: getResolvedFontStyle(cell)
+    }))
+  )
 })
 </script>
 

+ 3 - 2
src/renderer/src/lvgl-widgets/tabview/Config.vue

@@ -33,10 +33,10 @@
   >
     <el-form ref="form" :model="formData" :rules="rules" hide-required-asterisk>
       <el-form-item label="名称" prop="name">
-        <el-input v-model="formData.name" spellcheck="false"></el-input>
+        <LanguageInput v-model="formData.name" />
       </el-form-item>
       <el-form-item label="文本" prop="text">
-        <el-input type="textarea" v-model="formData.text" spellcheck="false"></el-input>
+        <LanguageInput type="textarea" :rows="2" v-model="formData.text" />
       </el-form-item>
     </el-form>
     <template #footer>
@@ -50,6 +50,7 @@ import { ref, computed, type Ref } from 'vue'
 import { LuPlus, LuTrash2 } from 'vue-icons-plus/lu'
 import { FormInstance } from 'element-plus'
 import { klona } from 'klona'
+import LanguageInput from '../components/LanguageInput.vue'
 
 const props = defineProps<{
   values: Ref<any>

+ 1 - 1
src/renderer/src/lvgl-widgets/textarea/Textarea.vue

@@ -81,7 +81,7 @@ watch(() => [props.width, props.height, getContent.value, props.nowrap], handleR
 })
 
 const contentSource = computed(() => (resolvedText.value.text ? props.text : props.placeholder))
-const textStyle = getTextStyle(() => undefined, contentSource.value)
+const textStyle = getTextStyle(() => undefined, contentSource)
 const contentStyle = computed(() => ({
   ...textStyle.value,
   color: !resolvedText.value.text ? '#ccc' : '',

+ 2 - 2
src/renderer/src/lvgl-widgets/window/Window.vue

@@ -78,8 +78,8 @@ const props = defineProps<{
 const { resolveText, getTextStyle } = useLanguage()
 const resolvedTitle = computed(() => resolveText(props.title))
 const resolvedContent = computed(() => resolveText(props.text))
-const titleTextStyle = getTextStyle(() => undefined, props.title)
-const contentTextStyle = getTextStyle(() => undefined, props.text)
+const titleTextStyle = getTextStyle(() => undefined, () => props.title)
+const contentTextStyle = getTextStyle(() => undefined, () => props.text)
 
 const styleMap = useWidgetStyle({
   widget: 'lv_win',

+ 16 - 2
src/renderer/src/store/modules/project.ts

@@ -116,13 +116,27 @@ export const useProjectStore = defineStore('project', () => {
     }
   )
 
+  const checkNotLoadFont = (fontName: string) => {
+    return new Promise((resolve, reject) => {
+      document.fonts.ready.then(function () {
+        document.fonts.forEach(function (fontFace) {
+          if (fontFace.family === fontName) {
+            reject()
+          }
+        })
+
+        resolve(true)
+      })
+    })
+  }
+
   watch(
     () => project.value?.resources.fonts,
-    (fonts?: FontResource[]) => {
+    async (fonts?: FontResource[]) => {
       // 动态加载全部字体
       // 判断字体是否已经加载
       const fontPromises = fonts
-        ?.filter((font) => !document.fonts.check(font.fileName))
+        ?.filter(async (font) => await checkNotLoadFont(font.fileName))
         .map((font) => {
           const fontFace = new FontFace(
             font.fileName,

+ 9 - 5
src/renderer/src/views/designer/sidebar/components/LanguageModal.vue

@@ -70,11 +70,15 @@
                     />
                     <el-select v-model="languageItem.font" style="flex: 1" :placeholder="t('font')">
                       <el-option
-                        v-for="font in fonts"
-                        :key="font.id"
-                        :value="font.id"
-                        :label="font.fileName"
-                      />
+                        v-for="item in fonts"
+                        :key="item.id"
+                        :value="item.id"
+                        :label="item.fileName"
+                      >
+                        <div :style="{ fontFamily: `'${item.fileName}'` }">
+                          {{ item.fileName }}
+                        </div>
+                      </el-option>
                     </el-select>
                     <input-number
                       v-model="languageItem.fontSize"