Browse Source

feat: 添加资源管理模块

jiaxing.liao 2 weeks ago
parent
commit
0da681c325

+ 2 - 0
package.json

@@ -23,9 +23,11 @@
   "dependencies": {
     "@electron-toolkit/preload": "^3.0.2",
     "@electron-toolkit/utils": "^4.0.0",
+    "@types/fs-extra": "^11.0.4",
     "@vueuse/components": "^14.0.0",
     "@vueuse/core": "^14.0.0",
     "element-plus": "^2.11.4",
+    "fs-extra": "^11.3.2",
     "klona": "^2.0.6",
     "monaco-editor": "^0.54.0",
     "normalize.css": "^8.0.1",

+ 21 - 0
pnpm-lock.yaml

@@ -14,6 +14,9 @@ importers:
       '@electron-toolkit/utils':
         specifier: ^4.0.0
         version: 4.0.0(electron@38.2.2)
+      '@types/fs-extra':
+        specifier: ^11.0.4
+        version: 11.0.4
       '@vueuse/components':
         specifier: ^14.0.0
         version: 14.0.0(vue@3.5.22(typescript@5.9.3))
@@ -23,6 +26,9 @@ importers:
       element-plus:
         specifier: ^2.11.4
         version: 2.11.4(vue@3.5.22(typescript@5.9.3))
+      fs-extra:
+        specifier: ^11.3.2
+        version: 11.3.2
       klona:
         specifier: ^2.0.6
         version: 2.0.6
@@ -767,6 +773,9 @@ packages:
   '@types/estree@1.0.8':
     resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
 
+  '@types/fs-extra@11.0.4':
+    resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
+
   '@types/fs-extra@9.0.13':
     resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
 
@@ -779,6 +788,9 @@ packages:
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
+  '@types/jsonfile@6.1.4':
+    resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
+
   '@types/keyv@3.1.4':
     resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
 
@@ -3788,6 +3800,11 @@ snapshots:
 
   '@types/estree@1.0.8': {}
 
+  '@types/fs-extra@11.0.4':
+    dependencies:
+      '@types/jsonfile': 6.1.4
+      '@types/node': 22.18.9
+
   '@types/fs-extra@9.0.13':
     dependencies:
       '@types/node': 22.18.9
@@ -3798,6 +3815,10 @@ snapshots:
 
   '@types/json-schema@7.0.15': {}
 
+  '@types/jsonfile@6.1.4':
+    dependencies:
+      '@types/node': 22.18.9
+
   '@types/keyv@3.1.4':
     dependencies:
       '@types/node': 22.18.9

+ 96 - 23
src/main/files.ts

@@ -1,11 +1,11 @@
-import { dialog } from 'electron'
-
-const fs = require('fs')
+import type { OpenDialogOptions, Event } from 'electron'
+import { dialog, ipcMain } from 'electron'
+import * as fs from 'fs-extra'
 
 /**
- * 打开文件夹
+ * 选择文件夹
  */
-export const openDirectory = async () => {
+export const choeseDirectory = async () => {
   const { canceled, filePaths } = await dialog.showOpenDialog({
     properties: ['openDirectory']
   })
@@ -17,13 +17,17 @@ export const openDirectory = async () => {
 }
 
 /**
- * 打开文件
+ * 选择文件
  */
-export const openFile = async () => {
-  const { canceled, filePaths } = await dialog.showOpenDialog({})
+export const choeseFile = async (_e: Event, options: OpenDialogOptions = {}) => {
+  const { canceled, filePaths } = await dialog.showOpenDialog({
+    title: '选择文件',
+    buttonLabel: '选择',
+    ...options
+  })
 
   if (!canceled) {
-    return filePaths[0]
+    return filePaths
   }
   return
 }
@@ -31,36 +35,105 @@ export const openFile = async () => {
 /**
  * 保存文件
  */
-export const writeFile = async (content: string, filePath: string) => {
-  fs.writeFileSync(filePath, content)
+export const writeFile = async (_e: Event, filePath: string, content: string) => {
+  return fs.writeFileSync(filePath, content)
 }
 
 /**
  * 读取文件
  */
-export const readFile = async (filePath: string) => {
-  return fs.readFileSync(filePath, 'utf-8')
+export const readFile = async (_e: Event, filePath: string, encoding: BufferEncoding) => {
+  return fs.readFileSync(filePath, { encoding })
 }
 
 /**
  * 检查文件是否存在
  */
-export const checkFileExists = async (filePath: string) => {
+export const checkFileExists = async (_e: Event, filePath: string) => {
   return fs.existsSync(filePath)
 }
 
 /**
  * 创建目录
  */
-export const createDirectory = async (directoryPath: string) => {
-  fs.mkdirSync(directoryPath, { recursive: true })
+export const createDirectory = async (_e: Event, directoryPath: string) => {
+  return fs.mkdirSync(directoryPath, { recursive: true })
+}
+
+/**
+ * 复制文件
+ */
+export const copyFile = async (_e: Event, sourcePath: string, destinationPath: string) => {
+  return fs.copyFileSync(sourcePath, destinationPath)
+}
+
+/**
+ * 打开文件夹
+ */
+export const openDir = async (
+  _e: Event,
+  directoryPath: string,
+  cb: (err: NodeJS.ErrnoException | null, dir: fs.Dir) => void
+) => {
+  return fs.opendir(directoryPath, cb)
+}
+
+/**
+ * 复制文件夹
+ */
+export const cp = (_e: Event, sourcePath: string, destinationPath: string) => {
+  return fs.cpSync(sourcePath, destinationPath)
+}
+
+/**
+ * 删除文件
+ */
+export const removeFile = (_e: Event, sourcePath: string) => {
+  return fs.rmSync(sourcePath)
+}
+
+/**
+ * 打开并锁定文件
+ */
+export const openFile = (_e: Event, sourcePath: string, flag: string) => {
+  return fs.openSync(sourcePath, flag || 'w')
+}
+
+/**
+ * 关闭并解锁文件
+ */
+export const closeFile = (_e: Event, fd: number) => {
+  return fs.closeSync(fd)
+}
+
+/**
+ * 修改文件名
+ */
+export const renameFile = (_e: Event, sourcePath: string, targetPath: string) => {
+  return fs.renameSync(sourcePath, targetPath)
 }
 
-export default {
-  openDirectory,
-  openFile,
-  readFile,
-  writeFile,
-  checkFileExists,
-  createDirectory
+export function handleFile() {
+  // 获取文件夹
+  ipcMain.handle('get-directory', choeseDirectory)
+  // 获取文件
+  ipcMain.handle('get-file', choeseFile)
+  // 创建文件夹
+  ipcMain.handle('create-directory', createDirectory)
+  // 写入文件
+  ipcMain.handle('write-file', writeFile)
+  // 读取文件
+  ipcMain.handle('read-file', readFile)
+  // 检查路径是否存在
+  ipcMain.handle('check-file-path', checkFileExists)
+  // 复制文件
+  ipcMain.handle('copy-file', copyFile)
+  // 删除文件
+  ipcMain.handle('delete-file', removeFile)
+  // 独占打开
+  ipcMain.handle('exclusive-open', openFile)
+  // 独占关闭
+  ipcMain.handle('exclusive-close', closeFile)
+  // 修改文件名
+  ipcMain.handle('modify-file-name', renameFile)
 }

+ 3 - 5
src/main/index.ts

@@ -2,7 +2,7 @@ import { app, shell, BrowserWindow, ipcMain } from 'electron'
 import { join } from 'path'
 import { electronApp, optimizer, is } from '@electron-toolkit/utils'
 import icon from '../../resources/icon.png?asset'
-import { openDirectory, openFile } from './files'
+import { handleFile } from './files'
 
 const net = require('net')
 
@@ -87,10 +87,8 @@ app.whenReady().then(() => {
 
   // IPC test
   ipcMain.on('ping', () => console.log('pong'))
-  // Get Directory
-  ipcMain.handle('get-directory', openDirectory)
-  // Get File
-  ipcMain.handle('get-file', openFile)
+  // 文件处理
+  handleFile()
 
   createWindow()
 

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

@@ -45,6 +45,7 @@ declare module 'vue' {
     ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTree: typeof import('element-plus/es')['ElTree']
+    LocalImage: typeof import('./src/components/LocalImage/index.vue')['default']
     MonacoEditor: typeof import('./src/components/MonacoEditor/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

File diff suppressed because it is too large
+ 1 - 0
src/renderer/src/assets/font.svg


+ 30 - 0
src/renderer/src/components/LocalImage/index.vue

@@ -0,0 +1,30 @@
+<template>
+  <img :src="imageSrc" v-bind="$attrs"></img>
+</template>
+
+<script setup lang="ts">
+  import { defineProps, ref, watch } from 'vue';
+
+  const props = defineProps<{
+    src: string;
+  }>()
+
+  const imageSrc = ref('')
+  
+  // 读取文件
+  const readFile = async () => {
+    if(!props.src) return
+    const res = await window.electron.ipcRenderer.invoke('read-file', props.src, 'base64')
+    imageSrc.value = `data:image/${props.src.split('.')[1]};base64,` + res
+  }
+
+  watch(
+    () => props.src, 
+    () => {
+      readFile()
+    },
+    { immediate: true }
+  )
+</script>
+
+<style scoped></style>

+ 53 - 5
src/renderer/src/model/index.ts

@@ -2,6 +2,7 @@ import type { Page } from '@/types/page'
 import type { Screen } from '@/types/screen'
 import type { ScreenConfig } from '@/types/appMeta'
 import type { Bin } from '@/types/bins'
+import type { Resource, ImageResource, FontResource, OtherResource } from '@/types/resource'
 
 import { v4 } from 'uuid'
 
@@ -17,15 +18,15 @@ export const createScreen = (config: ScreenConfig): Screen => {
     // 名称
     name: `screen_${config.type}`,
     // 类型
-    type: config.type,
+    type: 'screen',
     // 屏幕宽
     width: config.width,
     // 屏幕高
     height: config.height,
-    // 隐藏
-    hidden: false,
-    // 锁定
-    locked: false,
+    // 颜色格式
+    colorFormat: config.colorFormat,
+    // 颜色深度
+    colorDepth: config.colorDepth,
     // 页面
     pages: [createPage()],
     // 元信息
@@ -77,3 +78,50 @@ export const createBin = (index: number): Bin => {
     path: ''
   }
 }
+
+/**
+ * 创建文件资源
+ * @param path 文件路径
+ * @param type 类型
+ */
+export const createFileResource = (path: string, type: 'image' | 'font' | 'other'): Resource => {
+  const fileName = path.split('\\').pop() || ''
+
+  switch (type) {
+    // 图片资源
+    case 'image': {
+      return {
+        id: v4(),
+        fileName,
+        fielType: fileName.split('.').pop() || '',
+        path,
+        compressFormat: 'none',
+        alpha: 255,
+        bin: ''
+      }
+    }
+    // 字体资源
+    case 'font': {
+      return {
+        id: v4(),
+        fileName,
+        fielType: fileName.split('.').pop() || '',
+        path,
+        range: ['all'],
+        codeRange: [],
+        extText: '',
+        bin: ''
+      }
+    }
+    default: {
+      // 其他资源
+      return {
+        id: v4(),
+        fileName,
+        fielType: fileName.split('.').pop() || '',
+        path,
+        bin: ''
+      }
+    }
+  }
+}

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

@@ -34,6 +34,8 @@ export const useProjectStore = defineStore('project', () => {
     screens: Screen[]
   }>()
 
+  // 项目路径
+  const projectPath = ref<string>()
   // 活动页面key
   const activePageId = ref<string>()
 
@@ -47,7 +49,7 @@ export const useProjectStore = defineStore('project', () => {
    * 创建应用
    * @param meta 应用元信息
    */
-  const createApp = (meta: AppMeta) => {
+  const createApp = async (meta: AppMeta & { path: string }) => {
     // 1、应用元信息
     project.value = {
       version: '1.0.0',
@@ -80,6 +82,37 @@ export const useProjectStore = defineStore('project', () => {
         project.value.bins.push(createBin(i))
       }
     }
+
+    projectPath.value = `${meta.path}/${meta.name}`
+    // 4、创建项目文件夹
+    await window.electron.ipcRenderer.invoke('create-directory', `${meta.path}\\${meta.name}`)
+    // // assets
+    // await window.electron.ipcRenderer.invoke(
+    //   'create-directory',
+    //   `${meta.path}\\${meta.name}\\src\\assets`
+    // )
+    // images
+    await window.electron.ipcRenderer.invoke(
+      'create-directory',
+      `${meta.path}\\${meta.name}\\src\\assets\\images`
+    )
+    // fonts
+    await window.electron.ipcRenderer.invoke(
+      'create-directory',
+      `${meta.path}\\${meta.name}\\src\\assets\\fonts`
+    )
+    // others
+    await window.electron.ipcRenderer.invoke(
+      'create-directory',
+      `${meta.path}\\${meta.name}\\src\\assets\\others`
+    )
+    // TODO: 复制模板工程结构
+    // 工程json
+    await window.electron.ipcRenderer.invoke(
+      'write-file',
+      `${meta.path}\\${meta.name}\\project.json`,
+      JSON.stringify(project.value)
+    )
   }
 
   // 删除页面
@@ -93,6 +126,7 @@ export const useProjectStore = defineStore('project', () => {
   const deleteWidget = (widgetId: string) => {
     project.value?.screens.forEach((screen) => {
       screen.pages.forEach((page) => {
+        // TODO: 遍历删除
         page.children = page.children.filter((widget) => widget.id !== widgetId)
       })
     })
@@ -104,6 +138,7 @@ export const useProjectStore = defineStore('project', () => {
     activePageId,
     activePage,
     deletePage,
-    deleteWidget
+    deleteWidget,
+    projectPath
   }
 })

+ 18 - 11
src/renderer/src/types/resource.d.ts

@@ -10,10 +10,10 @@ export type ImageResource = {
   path: string
   // 压缩格式
   compressFormat: CompressFormat
-  // 透明度 0-1
+  // 透明度 0-255
   alpha: number
-  // 绑定bin
-  bin: number
+  // 绑定BinId
+  bin: string
 }
 
 export type TextRange = 'all' | 'page' | 'custom' | 'range'
@@ -26,14 +26,21 @@ export type FontResource = {
   fielType: string
   // 文件路径
   path: string
-  // 字体范围类型
-  range: TextRange[] // 0: 全部 1:界面文本 2:自定义文本 3: 编码范围
-  // 编码范围
+  /**
+   * 字体范围类型
+   * all: 全部 page:界面文本 custom:自定义文本 range: 编码范围
+   */
+  range: TextRange[]
+  /**
+   * 字体范围编码
+   */
   codeRange: string[]
-  // 额外的文本内容
+  /**
+   * 字体范围文本
+   */
   extText: string
-  // 绑定bin
-  bin: number
+  // 绑定BinId
+  bin: string
 }
 
 export type OtherResource = {
@@ -45,8 +52,8 @@ export type OtherResource = {
   fielType: string
   // 文件路径
   path: string
-  // 绑定bin
-  bin: number
+  // 绑定BinId
+  bin: string
 }
 
 export type Resource = ImageResource | FontResource | OtherResource

+ 5 - 5
src/renderer/src/types/screen.d.ts

@@ -7,15 +7,15 @@ export type Screen = {
   // 名称
   name: string
   // 类型
-  type: number
+  type: 'screen'
   // 屏幕宽
   width: number
   // 屏幕高
   height: number
-  // 隐藏
-  hidden: boolean
-  // 锁定
-  locked: boolean
+  // 颜色格式
+  colorFormat: string
+  // 颜色深度
+  colorDepth: string
   // 页面
   pages: Page[]
   // 元信息

+ 23 - 0
src/renderer/src/utils/index.ts

@@ -0,0 +1,23 @@
+/**
+ * 根据路径获取文件
+ * @param path 路径
+ * @param encoding 编码
+ * @returns content BufferEncoding
+ */
+export const getFileByPath = async (path: string, encoding: BufferEncoding = 'base64') => {
+  if (!path) return ''
+  const res = await window.electron.ipcRenderer.invoke('read-file', path, encoding)
+  return res
+}
+
+/**
+ * 根据路径获取图片
+ * @param path 路径
+ * @param encoding 编码
+ * @returns base64
+ */
+export const getImageByPath = async (path: string) => {
+  if (!path) return ''
+  const res = await getFileByPath(path)
+  return `data:image/${path.split('.').pop()};base64,` + res
+}

+ 1 - 1
src/renderer/src/views/designer/modals/projectModal/index.vue

@@ -380,7 +380,7 @@ const formData = reactive<
   version: '1.0.0',
   description: '',
   name: 'ProjectName',
-  path: 'D:\\',
+  path: 'D:\\sunmicroDesignerProjects',
   type: 'chip',
   chip: {
     model: '',

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

@@ -24,12 +24,12 @@
       <el-tree
         ref="treeRef"
         style="max-width: 600px"
-        :data="projectStore.activePage?.children"
         default-expand-all
         node-key="id"
         highlight-current
         check-on-click-node
-        :props="{ label: 'name' }"
+        :data="projectStore.activePage ? [projectStore.activePage] : []"
+        :props="{ label: 'name', children: 'children' }"
       >
         <template #default="{ node, data }">
           <PageTreeItem :node="node" :data="data" />

+ 197 - 0
src/renderer/src/views/designer/sidebar/Resource.vue

@@ -0,0 +1,197 @@
+<template>
+  <SplitterCollapse>
+    <SplitterCollapseItem title="图片">
+      <template #header-right>
+        <el-tooltip content="添加">
+          <span class="mr-12px" @click.capture.stop="handleAddImage"><LuPlus size="14px" /></span>
+        </el-tooltip>
+      </template>
+      <div class="w-full h-full flex flex-col">
+        <div class="p-12px flex gap-8px shrink-0">
+          <el-input v-model="imageSearch" size="small" placeholder="输入搜索..." />
+        </div>
+        <el-scrollbar class="flex-1">
+          <ResourceItem
+            v-for="item in getImages || []"
+            :key="item.id"
+            :data="item"
+            type="image"
+            @delete="deleteResource(item, 'images')"
+          />
+          <div v-if="!getImages?.length" class="text-center">暂无图片~</div>
+        </el-scrollbar>
+      </div>
+    </SplitterCollapseItem>
+    <SplitterCollapseItem title="字体">
+      <template #header-right>
+        <el-tooltip content="添加">
+          <span class="mr-12px" @click.capture.stop="handleAddFont"><LuPlus size="14px" /></span>
+        </el-tooltip>
+      </template>
+      <div class="w-full h-full flex flex-col">
+        <div class="p-12px flex gap-8px shrink-0">
+          <el-input v-model="fontSearch" size="small" placeholder="输入搜索..." />
+        </div>
+        <el-scrollbar class="flex-1">
+          <ResourceItem
+            v-for="item in getFonts || []"
+            :key="item.id"
+            :data="item"
+            type="font"
+            @delete="deleteResource(item, 'fonts')"
+          />
+          <div v-if="!getFonts?.length" class="text-center">暂无字体~</div>
+        </el-scrollbar>
+      </div>
+    </SplitterCollapseItem>
+    <SplitterCollapseItem title="其他数据">
+      <template #header-right>
+        <el-tooltip content="添加">
+          <span class="mr-12px" @click.capture.stop="handleAddOther"><LuPlus size="14px" /></span>
+        </el-tooltip>
+      </template>
+      <div class="w-full h-full flex flex-col">
+        <div class="p-12px flex gap-8px shrink-0">
+          <el-input v-model="otherSearch" size="small" placeholder="输入搜索..." />
+        </div>
+        <el-scrollbar class="flex-1">
+          <ResourceItem
+            v-for="item in getOthers || []"
+            :key="item.id"
+            :data="item"
+            type="other"
+            @delete="deleteResource(item, 'others')"
+          />
+          <div v-if="!getOthers?.length" class="text-center">暂无资源~</div>
+        </el-scrollbar>
+      </div>
+    </SplitterCollapseItem>
+  </SplitterCollapse>
+</template>
+
+<script setup lang="ts">
+import type { FontResource, ImageResource, OtherResource, Resource } from '@/types/resource'
+
+import { ref, computed } from 'vue'
+import { SplitterCollapse, SplitterCollapseItem } from '@/components/SplitterCollapse'
+import { LuPlus } from 'vue-icons-plus/lu'
+import { useProjectStore } from '@/store/modules/project'
+import { createFileResource } from '@/model'
+import ResourceItem from './components/ResourceItem.vue'
+
+const projectStore = useProjectStore()
+const imageSearch = ref('')
+const fontSearch = ref('')
+const otherSearch = ref('')
+
+const getImages = computed(() => {
+  return projectStore.project?.resources.images.filter((item) =>
+    item.fileName.includes(imageSearch.value)
+  )
+})
+
+const getFonts = computed(() => {
+  return projectStore.project?.resources.fonts.filter((item) =>
+    item.fileName.includes(fontSearch.value)
+  )
+})
+
+const getOthers = computed(() => {
+  return projectStore.project?.resources.others.filter((item) =>
+    item.fileName.includes(otherSearch.value)
+  )
+})
+
+// 添加图片
+const handleAddImage = async () => {
+  const paths = await window.electron.ipcRenderer.invoke('get-file', {
+    title: '选择文件',
+    buttonLabel: '添加',
+    properties: ['openFile', 'multiSelections'],
+    filters: [
+      {
+        name: 'Images',
+        extensions: ['png', 'jpg', 'jpeg', 'gif']
+      }
+    ]
+  })
+
+  paths.forEach(async (path) => {
+    const fileName = path.split('\\').pop()
+    // 复制文件
+    await window.electron.ipcRenderer.invoke(
+      'copy-file',
+      path,
+      `${projectStore.projectPath}\\src\\assets\\images\\${fileName}`
+    )
+    // 记录文件
+    projectStore.project?.resources.images.push(
+      createFileResource(`\\src\\assets\\images\\${fileName}`, 'image') as ImageResource
+    )
+  })
+}
+
+// 添加字体
+const handleAddFont = async () => {
+  const paths = await window.electron.ipcRenderer.invoke('get-file', {
+    title: '选择文件',
+    buttonLabel: '添加',
+    properties: ['openFile', 'multiSelections'],
+    filters: [
+      {
+        name: 'Fonts',
+        extensions: ['ttf', 'woff', 'otf']
+      }
+    ]
+  })
+
+  paths.forEach(async (path) => {
+    const fileName = path.split('\\').pop()
+    // 复制文件
+    await window.electron.ipcRenderer.invoke(
+      'copy-file',
+      path,
+      `${projectStore.projectPath}\\src\\assets\\fonts\\${fileName}`
+    )
+    // 记录文件
+    projectStore.project?.resources.fonts.push(
+      createFileResource(`\\src\\assets\\fonts\\${fileName}`, 'font') as FontResource
+    )
+  })
+}
+
+// 添加其他资源
+const handleAddOther = async () => {
+  const paths = await window.electron.ipcRenderer.invoke('get-file', {
+    title: '选择文件',
+    buttonLabel: '添加',
+    properties: ['openFile', 'multiSelections']
+  })
+
+  paths.forEach(async (path) => {
+    const fileName = path.split('\\').pop()
+    // 复制文件
+    await window.electron.ipcRenderer.invoke(
+      'copy-file',
+      path,
+      `${projectStore.projectPath}\\src\\assets\\others\\${fileName}`
+    )
+    // 记录文件
+    projectStore.project?.resources.others.push(
+      createFileResource(`\\src\\assets\\images\\${fileName}`, 'other') as OtherResource
+    )
+  })
+}
+
+// 删除资源
+const deleteResource = async (resource: Resource, type: 'images' | 'fonts' | 'others') => {
+  await window.electron.ipcRenderer.invoke('delete-file', projectStore.projectPath + resource.path)
+  const index =
+    projectStore.project?.resources[type].findIndex((item) => item.id === resource.id) || -1
+  if (index > -1) {
+    projectStore.project?.resources[type].splice(index, 1)
+  }
+}
+</script>
+
+<style scoped></style>

+ 7 - 10
src/renderer/src/views/designer/sidebar/components/PageTreeItem.vue

@@ -7,16 +7,13 @@
     </div>
     <div class="flex items-center gap-4px pr-12px invisible group-hover/item:visible">
       <el-tooltip v-if="data.type === 'page'" content="删除">
-        <el-popconfirm
-          class="box-item"
-          title="确认删除?"
-          placement="top-start"
-          @confirm="deleteWidget(data)"
-        >
-          <template #reference>
-            <span><LuTrash2 size="14px" /></span>
-          </template>
-        </el-popconfirm>
+        <span>
+          <el-popconfirm class="box-item" title="确认删除?" @confirm="deleteWidget(data)">
+            <template #reference>
+              <span><LuTrash2 size="14px" /></span>
+            </template>
+          </el-popconfirm>
+        </span>
       </el-tooltip>
       <el-tooltip content="隐藏/显示">
         <span @click.capture.stop="data.hidden = !data.hidden">

+ 55 - 0
src/renderer/src/views/designer/sidebar/components/ResourceItem.vue

@@ -0,0 +1,55 @@
+<template>
+  <p class="flex items-center justify-between gap-4px px-4 py-2 m-0 overflow-hidden group/item">
+    <span v-if="type === 'font'" class="w-32px h-32px grid place-items-center">
+      <BsFiletypeTtf size="16px" />
+    </span>
+    <span
+      v-if="type === 'image'"
+      class="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 truncate" :title="data.fileName">{{ data.fileName }}</span>
+
+    <span class="items-center gap-8px shrink-0 hidden group-hover/item:flex">
+      <el-tooltip content="编辑">
+        <span class="cursor-pointer" @click="$emit('edit')"><LuPencilLine size="14px" /></span>
+      </el-tooltip>
+      <el-tooltip content="删除">
+        <span>
+          <el-popconfirm class="box-item" title="确认删除?" @confirm="$emit('delete')">
+            <template #reference>
+              <span class="cursor-pointer"><LuTrash2 size="14px" /></span>
+            </template>
+          </el-popconfirm>
+        </span>
+      </el-tooltip>
+    </span>
+  </p>
+</template>
+
+<script setup lang="ts">
+import type { Resource } from '@/types/resource'
+
+import { defineProps, defineEmits } from 'vue'
+import LocalImage from '@/components/LocalImage/index.vue'
+import { useProjectStore } from '@/store/modules/project'
+import { LuTrash2, LuPencilLine } from 'vue-icons-plus/lu'
+import { BsFiletypeTtf, BsFiletypeWoff } from 'vue-icons-plus/bs'
+
+defineEmits<{
+  delete: []
+  edit: []
+}>()
+
+defineProps<{
+  data: Resource
+  type: 'image' | 'font' | 'other'
+}>()
+const projectStore = useProjectStore()
+</script>
+
+<style scoped></style>

+ 7 - 10
src/renderer/src/views/designer/sidebar/components/ScreenTreeItem.vue

@@ -14,16 +14,13 @@
           <span><LuPencilLine size="14px" @click.capture.stop="edit = true" /></span>
         </el-tooltip>
         <el-tooltip content="删除" v-if="data.type === 'page'">
-          <el-popconfirm
-            class="box-item"
-            title="确认删除?"
-            placement="top-start"
-            @confirm="deletePage(data)"
-          >
-            <template #reference>
-              <span><LuTrash2 size="14px" /></span>
-            </template>
-          </el-popconfirm>
+          <span>
+            <el-popconfirm class="box-item" title="确认删除?" @confirm="deletePage(data)">
+              <template #reference>
+                <span><LuTrash2 size="14px" /></span>
+              </template>
+            </el-popconfirm>
+          </span>
         </el-tooltip>
         <el-tooltip content="隐藏/显示" v-if="data.type === 'page'">
           <span @click.capture.stop="data.hidden = !data.hidden">

+ 12 - 11
src/renderer/src/views/designer/sidebar/index.vue

@@ -4,10 +4,14 @@
       <div class="w-full flex-1">
         <ul class="list-none p-0 m-0 text-12px text-text-secondary sidebar-menu">
           <li
-            class="sidebar-menu-item"
+            class="sidebar-menu-item border-l-1 border-l-solid border-l-transparent"
             v-for="item in sidebarMenu"
             :key="item.key"
-            :class="activeMenu === item.key ? 'text-text-active bg-bg-primary' : ''"
+            :class="
+              activeMenu === item.key
+                ? 'text-text-active border-l-1 border-l-solid border-l-text-active!'
+                : ''
+            "
             @click="activeMenu = item.key"
           >
             <el-tooltip
@@ -29,23 +33,19 @@
       </div>
     </div>
     <!-- 目录大纲 -->
-    <div class="flex-1" v-show="activeMenu === 'file'">
+    <div class="flex-1 overflow-hidden" v-show="activeMenu === 'file'">
       <Hierarchy />
     </div>
     <!-- 控件库 -->
-    <div class="flex-1" v-show="activeMenu === 'widget'">
+    <div class="flex-1 overflow-hidden" v-show="activeMenu === 'widget'">
       <Libary />
     </div>
     <!-- 资源管理 -->
-    <div class="flex-1" v-show="activeMenu === 'resource'">
-      <SplitterCollapse>
-        <SplitterCollapseItem title="图片"> </SplitterCollapseItem>
-        <SplitterCollapseItem title="字体"> </SplitterCollapseItem>
-        <SplitterCollapseItem title="其他数据"> </SplitterCollapseItem>
-      </SplitterCollapse>
+    <div class="flex-1 overflow-hidden" v-show="activeMenu === 'resource'">
+      <Resource />
     </div>
     <!-- 代码查看 -->
-    <div class="flex-1" v-show="activeMenu === 'code'">
+    <div class="flex-1 overflow-hidden" v-show="activeMenu === 'code'">
       <SplitterCollapse>
         <SplitterCollapseItem title="屏幕页面"> </SplitterCollapseItem>
         <SplitterCollapseItem title="页面大纲"> </SplitterCollapseItem>
@@ -67,6 +67,7 @@ import { LuFiles, LuBoxes, LuSquareCode, LuSettings2, LuInbox } from 'vue-icons-
 import Hierarchy from './Hierarchy.vue'
 import Libary from './Libary.vue'
 import Schema from './Schema.vue'
+import Resource from './Resource.vue'
 
 const { t } = useI18n()
 

+ 2 - 0
src/renderer/src/views/designer/workspace/stage/index.vue

@@ -127,6 +127,8 @@ watch(
     if (val) {
       state.width = val.width
       state.height = val.height
+      await nextTick()
+      handleCenter()
     }
   }
 )