Explorar o código

feat: 新增视频控件

jiaxing.liao hai 1 mes
pai
achega
761087185c

+ 2 - 2
project.json5

@@ -96,7 +96,7 @@
       {
         "id": "img_1",
         "fileName": "image1.png",
-        "fielType": "png",
+        "fileType": "png",
         "path": "./src/assets/images/image1.png",
         "compressFormat": "rle", // 无压缩、RLE压缩、QOI压缩、JPEG压缩、PNG压缩
         "alpha": 255, // 透明度0-255
@@ -110,7 +110,7 @@
       {
         "id": "font_1",
         "fileName": "font1.ttf",
-        "fielType": "ttf",
+        "fileType": "ttf",
         "path": "./src/assets/fonts/font1.ttf",
         "range": ["page", "custom", "range"], // 'all' | 'page' | 'custom' | 'range' 0: 全部 1:界面文本 2:自定义文本 3: 编码范围
         // 编码范围

+ 3 - 3
src/renderer/src/locales/en_US.json

@@ -142,7 +142,7 @@
   "time": "Time",
   "date": "Date",
   "dagitalClock": "DagitalClock",
-  "analogClock": "AnalogClock"
+  "analogClock": "AnalogClock",
+  "video": "Video",
+  "media": "Media"
 }
-
-

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

@@ -142,5 +142,7 @@
   "time": "时间",
   "date": "日期",
   "dagitalClock": "数字时钟",
-  "analogClock": "模拟时钟"
+  "analogClock": "模拟时钟",
+  "video": "视频",
+  "media": "多媒体"
 }

+ 0 - 9
src/renderer/src/lvgl-widgets/animimg/Config.vue

@@ -45,9 +45,7 @@
 <script setup lang="ts">
 import { computed, type Ref } from 'vue'
 import { LuArrowDown, LuArrowUp, LuPlus, LuTrash2 } from 'vue-icons-plus/lu'
-import { useProjectStore } from '@/store/modules/project'
 import { moveToPosition } from '@/utils'
-import { LocalImage } from '@/components'
 
 import ImageSelect from '@/views/designer/config/property/components/ImageSelect.vue'
 
@@ -59,8 +57,6 @@ const props = defineProps<{
   }>
 }>()
 
-const projectStore = useProjectStore()
-
 const images = computed({
   get: () => props.values.value.props.images || [],
   set: (value: string[]) => {
@@ -68,11 +64,6 @@ const images = computed({
   }
 })
 
-const getImagePath = (id: string) => {
-  const path = projectStore.project?.resources.images.find((item) => item.id === id)?.path || ''
-  return projectStore.projectPath + path
-}
-
 const handleAdd = () => {
   images.value.push('')
 }

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

@@ -4,7 +4,6 @@ import type { IComponentModelConfig } from '../type'
 import i18n from '@/locales'
 import { flagOptions, stateOptions, stateList } from '@/constants'
 import defaultStyle from './style.json'
-import { dayjs } from 'element-plus'
 
 export default {
   label: i18n.global.t('dagitalClock'),

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

@@ -37,6 +37,8 @@ import DateText from './datetext/index'
 import DagitalClock from './dagital-clock/index'
 import AnalogClock from './analog-clock/index'
 
+import Video from './video/index'
+
 import Page from './page'
 import { IComponentModelConfig } from './type'
 
@@ -81,7 +83,9 @@ export const ComponentArray = [
   Calendar,
   DateText,
   DagitalClock,
-  AnalogClock
+  AnalogClock,
+
+  Video
 ] as IComponentModelConfig[]
 
 const componentMap: { [key: string]: IComponentModelConfig } = ComponentArray.reduce((acc, cur) => {

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

@@ -15,7 +15,6 @@
       />
       <span class="whitespace-pre! z-2">{{ title }}</span>
       <span
-        v-if="closeBtn"
         :style="styleMap?.titleButtonStyle"
         class="bg-#2195f6 z-2 shadow-[0_4px_0_#cccccc] text-white w-40px h-30px rounded-10px relative grid place-items-center"
       >
@@ -82,7 +81,7 @@ const props = defineProps<{
   part: string
   title: string
   content: string
-  closeBtn: boolean
+  // closeBtn: boolean
   btnWidth: number
   btnHeight: number
   btns: { text: string }[]

+ 7 - 7
src/renderer/src/lvgl-widgets/message/index.tsx

@@ -62,7 +62,7 @@ export default {
       states: [],
       title: 'Title',
       content: 'Content',
-      closeBtn: true,
+      // closeBtn: true,
       btnWidth: 60,
       btnHeight: 30,
       btns: [{ text: 'Apply' }, { text: 'Close' }]
@@ -192,12 +192,12 @@ export default {
         valueType: 'text',
         labelWidth: '80px'
       },
-      {
-        label: '关闭按钮',
-        field: 'props.closeBtn',
-        valueType: 'switch',
-        labelWidth: '80px'
-      },
+      // {
+      //   label: '关闭按钮',
+      //   field: 'props.closeBtn',
+      //   valueType: 'switch',
+      //   labelWidth: '80px'
+      // },
       {
         label: '文本',
         field: 'props.content',

+ 32 - 0
src/renderer/src/lvgl-widgets/video/Video.vue

@@ -0,0 +1,32 @@
+<template>
+  <div
+    :style="styleMap?.mainStyle"
+    class="w-full h-full relative overflow-hidden box-border flex items-start bg-#000"
+  >
+    <ImageBg :src="styleMap?.mainStyle?.imageSrc" :imageStyle="styleMap?.mainStyle?.imageStyle" />
+    <LuPlay class="m-auto" size="45px" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
+import ImageBg from '../ImageBg.vue'
+import { LuPlay } from 'vue-icons-plus/lu'
+
+const props = defineProps<{
+  width: number
+  height: number
+  text?: string
+  styles: any
+  state?: string
+  part?: string
+  longMode?: 'circular' | 'clip' | 'dot' | 'scroll' | 'wrap'
+  lightStart?: number
+  lightEnd?: number
+}>()
+
+const styleMap = useWidgetStyle({
+  widget: 'video',
+  props
+})
+</script>

+ 207 - 0
src/renderer/src/lvgl-widgets/video/index.tsx

@@ -0,0 +1,207 @@
+import Video from './Video.vue'
+import icon from '../assets/icon/icon_37video.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('video'),
+  icon,
+  component: Video,
+  key: 'video',
+  group: i18n.global.t('media'),
+  sort: 1,
+  defaultStyle,
+  parts: [
+    {
+      name: 'main',
+      stateList
+    }
+  ],
+  defaultSchema: {
+    name: 'label',
+    props: {
+      x: 0,
+      y: 0,
+      width: 300,
+      height: 200,
+      flags: [
+        'LV_OBJ_FLAG_CLICK_FOCUSABLE',
+        'LV_OBJ_FLAG_SCROLLABLE',
+        'LV_OBJ_FLAG_SCROLL_ELASTIC',
+        'LV_OBJ_FLAG_SCROLL_MOMENTUM',
+        'LV_OBJ_FLAG_SCROLL_CHAIN_HOR',
+        'LV_OBJ_FLAG_SCROLL_CHAIN_VER',
+        'LV_OBJ_FLAG_SCROLL_CHAIN',
+        'LV_OBJ_FLAG_SCROLL_WITH_ARROW',
+        'LV_OBJ_FLAG_SNAPPABLE',
+        'LV_OBJ_FLAG_PRESS_LOCK',
+        'LV_OBJ_FLAG_GESTURE_BUBBLE'
+      ],
+      states: [],
+      source: {
+        type: 'file', // file文件 signal信号
+        file: {
+          id: '',
+          sd_path: '',
+          autoplay: true,
+          loop: false
+        },
+        signal: '通道1'
+      }
+    },
+    styles: []
+  },
+  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.source.type',
+        labelWidth: '80px',
+        valueType: 'select',
+        componentProps: {
+          options: [
+            { label: '视频文件', value: 'file' },
+            { label: '视频信号', value: 'signal' }
+          ]
+        }
+      },
+      {
+        valueType: 'dependency',
+        name: ['props.source.type'],
+        dependency: (form) => {
+          return form?.['props.source.type'] === 'file'
+            ? [
+                {
+                  label: '视频文件',
+                  field: 'props.source.file.id',
+                  labelWidth: '80px',
+                  valueType: 'video'
+                },
+                {
+                  label: 'SD路径',
+                  field: 'props.source.file.sd_path',
+                  labelWidth: '80px',
+                  valueType: 'text'
+                },
+                {
+                  label: '自动播放',
+                  field: 'props.source.file.autoplay',
+                  labelWidth: '80px',
+                  valueType: 'switch'
+                },
+                {
+                  label: '循环播放',
+                  field: 'props.source.file.loop',
+                  labelWidth: '80px',
+                  valueType: 'switch'
+                }
+              ]
+            : [
+                {
+                  label: '视频信号',
+                  field: 'props.source.signal',
+                  labelWidth: '80px',
+                  valueType: 'select',
+                  componentProps: {
+                    options: [
+                      { label: '通道1', value: '通道1' },
+                      { label: '通道2', value: '通道2' },
+                      { label: '通道3', value: '通道3' },
+                      { label: '通道4', value: '通道4' },
+                      { label: '通道5', value: '通道5' },
+                      { label: '通道6', value: '通道6' },
+                      { label: '通道7', value: '通道7' },
+                      { label: '通道8', value: '通道8' }
+                    ]
+                  }
+                }
+              ]
+        }
+      }
+    ],
+    // 组件样式
+    styles: []
+  }
+} as IComponentModelConfig

+ 5 - 0
src/renderer/src/lvgl-widgets/video/style.json

@@ -0,0 +1,5 @@
+{
+  "widget": "video",
+  "styleName": "defualt",
+  "part": []
+}

+ 3 - 3
src/renderer/src/model/index.ts

@@ -99,7 +99,7 @@ export const createFileResource = (path: string, type: 'image' | 'font' | 'other
       return {
         id: v4(),
         fileName,
-        fielType: fileName.split('.').pop() || '',
+        fileType: fileName.split('.').pop() || '',
         path,
         compressFormat: 'none',
         colorFormat: 'RGB565A8',
@@ -115,7 +115,7 @@ export const createFileResource = (path: string, type: 'image' | 'font' | 'other
       return {
         id: v4(),
         fileName,
-        fielType: fileName.split('.').pop() || '',
+        fileType: fileName.split('.').pop() || '',
         path,
         range: ['all'],
         codeRange: [],
@@ -128,7 +128,7 @@ export const createFileResource = (path: string, type: 'image' | 'font' | 'other
       return {
         id: v4(),
         fileName,
-        fielType: fileName.split('.').pop() || '',
+        fileType: fileName.split('.').pop() || '',
         path,
         bin: ''
       }

+ 1 - 9
src/renderer/src/store/modules/action.ts

@@ -34,15 +34,7 @@ export const useActionStore = defineStore('action', () => {
     parent: Page | BaseWidget,
     widgetId: string,
     ancestors: Array<Page | BaseWidget> = [parent]
-  ):
-    | {
-        widget: BaseWidget
-        parent: Page | BaseWidget
-        list: BaseWidget[]
-        index: number
-        ancestors: Array<Page | BaseWidget>
-      }
-    | undefined => {
+  ) => {
     const children = parent.children || []
     for (let index = 0; index < children.length; index++) {
       const widget = children[index]

+ 3 - 3
src/renderer/src/types/resource.d.ts

@@ -5,7 +5,7 @@ export type ImageResource = {
   // 文件名
   fileName: string
   // 文件类型
-  fielType: string
+  fileType: string
   // 文件路径
   path: string
   // 压缩格式
@@ -31,7 +31,7 @@ export type FontResource = {
   // 文件名
   fileName: string
   // 文件类型
-  fielType: string
+  fileType: string
   // 文件路径
   path: string
   /**
@@ -57,7 +57,7 @@ export type OtherResource = {
   // 文件名
   fileName: string
   // 文件类型
-  fielType: string
+  fileType: string
   // 文件路径
   path: string
   // 绑定BinId

+ 11 - 5
src/renderer/src/views/designer/config/property/CusFormItem.vue

@@ -57,11 +57,10 @@
         </template>
       </el-select-v2>
       <!-- 开关 -->
-      <el-switch
-        v-if="schema.valueType === 'switch'"
-        v-model="value"
-        v-bind="schema?.componentProps"
-      />
+      <div v-if="schema.valueType === 'switch'" class="w-full flex justify-end">
+        <el-switch v-model="value" v-bind="schema?.componentProps" />
+      </div>
+
       <!-- 滑动条 -->
       <div v-if="schema.valueType === 'slider'" class="w-full flex gap-20px items-center">
         <el-slider v-model="value" v-bind="schema?.componentProps" style="flex: 1"></el-slider>
@@ -81,6 +80,12 @@
         v-model="value"
         v-bind="schema?.componentProps"
       />
+      <!-- 视频选择 -->
+      <VideoSelect
+        v-if="schema.valueType === 'video'"
+        v-model="value"
+        v-bind="schema?.componentProps"
+      />
       <!-- 图标选择 -->
       <SymbolSelect
         v-if="schema.valueType === 'symbol'"
@@ -277,6 +282,7 @@ import CusTimePicker from './components/CusTimePicker.vue'
 import ImageSelect from './components/ImageSelect.vue'
 import SymbolSelect from './components/SymbolSelect.vue'
 import { ColorPicker, MonacoEditor } from '@/components'
+import VideoSelect from './components/VideoSelect.vue'
 
 import StyleBackground from './components/StyleBackground.vue'
 import StyleBorder from './components/StyleBorder.vue'

+ 83 - 0
src/renderer/src/views/designer/config/property/components/VideoSelect.vue

@@ -0,0 +1,83 @@
+<template>
+  <el-dialog title="文件选择" v-model="visible" width="800px">
+    <div class="flex h-320px overflow-y-auto gap-8px flex-wrap">
+      <div
+        class="w-100px h-100px border-solid border-border cursor-pointer flex flex-col"
+        v-for="item in videoList"
+        :key="item.id"
+        :class="item.id === selected ? 'border-accent-blue!' : ''"
+        @click="handleClick(item.id)"
+        :title="item.fileName"
+      >
+        <div class="w-100px h-70px flex items-center justify-center">
+          <GrDocumentVideo size="45px" />
+        </div>
+
+        <div class="h-20px leading-30px text-12px text-text-secondary px-2px truncate">
+          {{ item.fileName }}
+        </div>
+      </div>
+      <el-empty class="mx-auto" v-if="!videoList.length" description="暂无资源" />
+    </div>
+    <template #footer>
+      <el-button @click="handleCancel">取消</el-button>
+      <el-button type="primary" :disabled="!selected" @click="handleConfirm">确定</el-button>
+    </template>
+  </el-dialog>
+
+  <el-input spellcheck="false" :model-value="selectedFile?.fileName" readonly>
+    <template #suffix>
+      <LuX v-if="selectedFile" class="cursor-pointer" size="16px" @click="handleClear" />
+    </template>
+    <template #append>
+      <GrDocumentVideo size="20px" class="cursor-pointer" @click="visible = true" />
+    </template>
+  </el-input>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import { useProjectStore } from '@/store/modules/project'
+import { LuX } from 'vue-icons-plus/lu'
+import { GrDocumentVideo } from 'vue-icons-plus/gr'
+
+const modelValue = defineModel('modelValue')
+const projectStore = useProjectStore()
+const visible = ref(false)
+const selected = ref(modelValue.value)
+// 资源列表
+const videoList = computed(() => {
+  return (projectStore.project?.resources.others || []).filter((item) => item.fileType === 'mp4')
+})
+// 选中资源
+const selectedFile = computed(() => {
+  return videoList.value.find((item) => item.id === modelValue.value)
+})
+
+const handleClick = (val: string) => {
+  if (selected.value === val) {
+    selected.value = ''
+  } else {
+    selected.value = val
+  }
+}
+
+// 取消
+const handleCancel = () => {
+  visible.value = false
+}
+
+// 确定
+const handleConfirm = () => {
+  visible.value = false
+  modelValue.value = selected.value
+}
+
+// 清除
+const handleClear = () => {
+  selected.value = ''
+  modelValue.value = ''
+}
+</script>
+
+<style scoped></style>

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

@@ -43,7 +43,7 @@
         </el-form>
       </el-collapse-item>
       <!-- 样式设置 -->
-      <el-collapse-item name="style">
+      <el-collapse-item v-if="formConfig.styles?.length" name="style">
         <template #title>
           <div class="flex items-center">
             <IoColorPaletteOutline size="12px" />

+ 1 - 1
src/renderer/src/views/designer/sidebar/Hierarchy.vue

@@ -218,7 +218,7 @@ const findPageById = (pageId?: string) => {
     .find((page) => page.id === pageId)
 }
 
-const findWidgetListById = (widgets: BaseWidget[], widgetId: string): BaseWidget[] | undefined => {
+const findWidgetListById = (widgets: BaseWidget[], widgetId: string) => {
   for (const widget of widgets) {
     if (widget.id === widgetId) {
       return widgets

+ 2 - 2
src/renderer/src/views/designer/sidebar/components/EditFontModal.vue

@@ -9,7 +9,7 @@
     <!-- <div class="w-full text-10px text-text-secondary flex gap-20px my-12px">
       <div class="felx-1">
         <span>文件类型:</span>
-        <span>{{ formData.fielType }}</span>
+        <span>{{ formData.fileType }}</span>
       </div>
     </div> -->
     <el-form
@@ -106,7 +106,7 @@ const editBinModalRef = ref<InstanceType<typeof EditBinModal>>()
 const formData = ref<FontResource>({
   id: '',
   fileName: '',
-  fielType: '',
+  fileType: '',
   path: '',
   bin: '',
   extText: '',

+ 1 - 1
src/renderer/src/views/designer/sidebar/components/EditImageModal.vue

@@ -176,7 +176,7 @@ const ditheringAlgorithmOptions = [
 const formData = ref<ImageResource>({
   id: '',
   fileName: '',
-  fielType: '',
+  fileType: '',
   path: '',
   compressFormat: 'none',
   colorFormat: '',

+ 32 - 11
src/renderer/src/views/designer/sidebar/components/ResourceItem.vue

@@ -1,14 +1,23 @@
 <template>
-  <p ref="listBoxRef"
+  <p
+    ref="listBoxRef"
     class="h-32px flex items-center justify-between gap-4px px-4 py-2 m-0 overflow-hidden group/item hover:bg-bg-tertiary"
-    @click="handleClick" @contextmenu="handleContextmenu">
+    @click="handleClick"
+    @contextmenu="handleContextmenu"
+  >
     <span v-if="type === 'font'" class="w-32px h-32px grid place-items-center">
-      <BsFiletypeTtf v-if="data.fielType === 'ttf'" size="16px" />
-      <BsFiletypeWoff v-else-if="data.fielType === 'woff'" size="16px" />
+      <BsFiletypeTtf v-if="data.fileType === 'ttf'" size="16px" />
+      <BsFiletypeWoff v-else-if="data.fileType === 'woff'" size="16px" />
       <img v-else :src="fontImg" class="w-16px h-16px" />
     </span>
-    <span v-if="type === 'image'" class="shrink-0 w-32px h-32px rounded-4px bg-bg-tertiary grid place-items-center">
-      <LocalImage :src="projectStore.projectPath + data.path" class="h-full w-full object-contain" />
+    <span
+      v-if="type === 'image'"
+      class="shrink-0 w-32px h-32px rounded-4px bg-bg-tertiary grid place-items-center"
+    >
+      <LocalImage
+        :src="projectStore.projectPath + data.path"
+        class="h-full w-full object-contain"
+      />
     </span>
     <span class="flex-1 flex items-center gap-4px">
       <span class="truncate max-w-120px" :title="data.fileName">{{ data.fileName }}</span>
@@ -18,7 +27,6 @@
       </span>
     </span>
 
-
     <span class="items-center gap-8px shrink-0 hidden group-hover/item:flex">
       <el-tooltip content="编辑" :append-to="listBoxRef">
         <span v-if="type !== 'other'" class="cursor-pointer" @click="handleEdit">
@@ -27,7 +35,12 @@
       </el-tooltip>
       <el-tooltip content="删除" :append-to="listBoxRef">
         <span>
-          <el-popconfirm :append-to="listBoxRef" class="box-item" title="确认删除?" @confirm="$emit('delete')">
+          <el-popconfirm
+            :append-to="listBoxRef"
+            class="box-item"
+            title="确认删除?"
+            @confirm="$emit('delete')"
+          >
             <template #reference>
               <span class="cursor-pointer">
                 <LuTrash2 size="14px" />
@@ -37,9 +50,17 @@
         </span>
       </el-tooltip>
     </span>
-    <el-dropdown ref="dropdownRef" :virtual-ref="triggerRef" :show-arrow="false" :popper-options="{
-      modifiers: [{ name: 'offset', options: { offset: [0, 0] } }]
-    }" virtual-triggering trigger="contextmenu" placement="bottom-start">
+    <el-dropdown
+      ref="dropdownRef"
+      :virtual-ref="triggerRef"
+      :show-arrow="false"
+      :popper-options="{
+        modifiers: [{ name: 'offset', options: { offset: [0, 0] } }]
+      }"
+      virtual-triggering
+      trigger="contextmenu"
+      placement="bottom-start"
+    >
       <template #dropdown>
         <el-dropdown-menu>
           <el-dropdown-item @click="openInExplorer">在资源管理器中打开</el-dropdown-item>