Przeglądaj źródła

feat: 资源管理编辑、删除、资源管理器打开等

jiaxing.liao 2 tygodni temu
rodzic
commit
70dce6ee9f

+ 1 - 0
package.json

@@ -28,6 +28,7 @@
     "@vueuse/core": "^14.0.0",
     "element-plus": "^2.11.4",
     "fs-extra": "^11.3.2",
+    "image-size": "^2.0.2",
     "klona": "^2.0.6",
     "monaco-editor": "^0.54.0",
     "normalize.css": "^8.0.1",

+ 10 - 0
pnpm-lock.yaml

@@ -29,6 +29,9 @@ importers:
       fs-extra:
         specifier: ^11.3.2
         version: 11.3.2
+      image-size:
+        specifier: ^2.0.2
+        version: 2.0.2
       klona:
         specifier: ^2.0.6
         version: 2.0.6
@@ -1986,6 +1989,11 @@ packages:
     engines: {node: '>=0.10.0'}
     hasBin: true
 
+  image-size@2.0.2:
+    resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==}
+    engines: {node: '>=16.x'}
+    hasBin: true
+
   import-fresh@3.3.1:
     resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
     engines: {node: '>=6'}
@@ -5375,6 +5383,8 @@ snapshots:
   image-size@0.5.5:
     optional: true
 
+  image-size@2.0.2: {}
+
   import-fresh@3.3.1:
     dependencies:
       parent-module: 1.0.1

+ 11 - 1
src/main/files.ts

@@ -1,5 +1,5 @@
 import type { OpenDialogOptions, Event } from 'electron'
-import { dialog, ipcMain } from 'electron'
+import { dialog, ipcMain, shell } from 'electron'
 import * as fs from 'fs-extra'
 
 /**
@@ -113,6 +113,14 @@ export const renameFile = (_e: Event, sourcePath: string, targetPath: string) =>
   return fs.renameSync(sourcePath, targetPath)
 }
 
+/**
+ * 资源管理器中打开
+ * @param path 文件路径
+ */
+export const openFileInExplorer = (_e: Event, path: string) => {
+  return shell.showItemInFolder(path)
+}
+
 export function handleFile() {
   // 获取文件夹
   ipcMain.handle('get-directory', choeseDirectory)
@@ -136,4 +144,6 @@ export function handleFile() {
   ipcMain.handle('exclusive-close', closeFile)
   // 修改文件名
   ipcMain.handle('modify-file-name', renameFile)
+  // 资源管理器中打开
+  ipcMain.handle('open-file-in-explorer', openFileInExplorer)
 }

+ 1 - 1
src/renderer/auto-imports.d.ts

@@ -6,5 +6,5 @@
 // biome-ignore lint: disable
 export {}
 declare global {
-
+  const ElMessage: typeof import('element-plus/es').ElMessage
 }

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

@@ -15,28 +15,39 @@ declare module 'vue' {
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
     ElAutoComplete: typeof import('element-plus/es')['ElAutoComplete']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElCollapse: typeof import('element-plus/es')['ElCollapse']
     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDivider: typeof import('element-plus/es')['ElDivider']
+    ElDropdown: typeof import('element-plus/es')['ElDropdown']
+    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
     ElFlex: typeof import('element-plus/es')['ElFlex']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElFromItem: typeof import('element-plus/es')['ElFromItem']
     ElHeader: typeof import('element-plus/es')['ElHeader']
+    ElIcon: typeof import('element-plus/es')['ElIcon']
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElOption: typeof import('element-plus/es')['ElOption']
     ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
+    ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']
     ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSelectV2: typeof import('element-plus/es')['ElSelectV2']
     ElSlider: typeof import('element-plus/es')['ElSlider']
     ElSpace: typeof import('element-plus/es')['ElSpace']
     ElSplitter: typeof import('element-plus/es')['ElSplitter']
@@ -47,9 +58,14 @@ declare module 'vue' {
     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']
+    PanelTitle: typeof import('./src/components/PanelTitle/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SplitterCollapse: typeof import('./src/components/SplitterCollapse/index.vue')['default']
     SplitterCollapseItem: typeof import('./src/components/SplitterCollapse/SplitterCollapseItem.vue')['default']
+    ViewTitle: typeof import('./src/components/ViewTitle/index.vue')['default']
+  }
+  export interface GlobalDirectives {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
   }
 }

+ 25 - 17
src/renderer/src/components/LocalImage/index.vue

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

+ 2 - 2
src/renderer/src/components/SplitterCollapse/SplitterCollapseItem.vue

@@ -10,10 +10,10 @@
       <div class="w-full h-full flex flex-col">
         <!-- header -->
         <div
-          class="h-32px bg-bg-tertiary flex items-center justify-between px-1 cursor-pointer border-b-1px border-b-solid border-border"
+          class="h-32px bg-bg-secondary flex items-center justify-between px-1 cursor-pointer border-b-1px border-b-solid border-border"
           @click="isExpanded ? collapse() : expand()"
         >
-          <div class="left">
+          <div class="left flex items-center">
             <span class="mr-8px text-text-primary">
               <ai-outline-down :size="12" v-if="isExpanded" />
               <ai-outline-right :size="12" v-else />

+ 22 - 0
src/renderer/src/components/ViewTitle/index.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="h-32px bg-bg-secondary">
+    <div class="px-12px leading-32px text-text-primary font-bold flex items-center justify-between">
+      <span class="flex-1">
+        <slot>
+          {{ title }}
+        </slot>
+      </span>
+      <span>
+        <slot name="right"></slot>
+      </span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { defineProps } from 'vue'
+
+defineProps({
+  title: String
+})
+</script>

+ 6 - 1
src/renderer/src/store/modules/project.ts

@@ -38,6 +38,8 @@ export const useProjectStore = defineStore('project', () => {
   const projectPath = ref<string>()
   // 活动页面key
   const activePageId = ref<string>()
+  // 图片压缩格式
+  const imageCompressFormat = ref<string[]>([])
 
   // 活动页面
   const activePage = computed(() => {
@@ -113,6 +115,8 @@ export const useProjectStore = defineStore('project', () => {
       `${meta.path}\\${meta.name}\\project.json`,
       JSON.stringify(project.value)
     )
+
+    imageCompressFormat.value = meta.imageCompress
   }
 
   // 删除页面
@@ -139,6 +143,7 @@ export const useProjectStore = defineStore('project', () => {
     activePage,
     deletePage,
     deleteWidget,
-    projectPath
+    projectPath,
+    imageCompressFormat
   }
 })

+ 11 - 6
src/renderer/src/utils/index.ts

@@ -1,11 +1,13 @@
+import { imageSize } from 'image-size'
+
 /**
  * 根据路径获取文件
  * @param path 路径
  * @param encoding 编码
  * @returns content BufferEncoding
  */
-export const getFileByPath = async (path: string, encoding: BufferEncoding = 'base64') => {
-  if (!path) return ''
+export const getFileByPath = async (path: string, encoding?: BufferEncoding) => {
+  if (!path) return
   const res = await window.electron.ipcRenderer.invoke('read-file', path, encoding)
   return res
 }
@@ -14,10 +16,13 @@ export const getFileByPath = async (path: string, encoding: BufferEncoding = 'ba
  * 根据路径获取图片
  * @param path 路径
  * @param encoding 编码
- * @returns base64
+ * @returns {base64, dimensions}
  */
 export const getImageByPath = async (path: string) => {
-  if (!path) return ''
-  const res = await getFileByPath(path)
-  return `data:image/${path.split('.').pop()};base64,` + res
+  if (!path) return
+  const res = await getFileByPath(path, 'base64')
+  const base64 = `data:image/${path.split('.').pop()};base64,` + res
+  const buffer = await getFileByPath(path)
+  const dimensions = imageSize(buffer)
+  return { base64, dimensions }
 }

+ 11 - 0
src/renderer/src/views/designer/sidebar/Method.vue

@@ -0,0 +1,11 @@
+<template>
+  <div>
+    <ViewTitle title="函数方法" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import ViewTitle from '@/components/ViewTitle/index.vue'
+</script>
+
+<style scoped></style>

+ 5 - 5
src/renderer/src/views/designer/sidebar/Resource.vue

@@ -18,7 +18,7 @@
             type="image"
             @delete="deleteResource(item, 'images')"
           />
-          <div v-if="!getImages?.length" class="text-center">暂无图片~</div>
+          <div v-if="!getImages?.length" class="text-center text-text-secondary">暂无图片~</div>
         </el-scrollbar>
       </div>
     </SplitterCollapseItem>
@@ -40,7 +40,7 @@
             type="font"
             @delete="deleteResource(item, 'fonts')"
           />
-          <div v-if="!getFonts?.length" class="text-center">暂无字体~</div>
+          <div v-if="!getFonts?.length" class="text-center text-text-secondary">暂无字体~</div>
         </el-scrollbar>
       </div>
     </SplitterCollapseItem>
@@ -62,7 +62,7 @@
             type="other"
             @delete="deleteResource(item, 'others')"
           />
-          <div v-if="!getOthers?.length" class="text-center">暂无资源~</div>
+          <div v-if="!getOthers?.length" class="text-center text-text-secondary">暂无资源~</div>
         </el-scrollbar>
       </div>
     </SplitterCollapseItem>
@@ -140,7 +140,7 @@ const handleAddFont = async () => {
     filters: [
       {
         name: 'Fonts',
-        extensions: ['ttf', 'woff', 'otf']
+        extensions: ['ttf', 'woff']
       }
     ]
   })
@@ -187,7 +187,7 @@ const handleAddOther = async () => {
 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
+    projectStore.project?.resources[type].findIndex((item) => item.id === resource.id) ?? -1
   if (index > -1) {
     projectStore.project?.resources[type].splice(index, 1)
   }

+ 24 - 1
src/renderer/src/views/designer/sidebar/Schema.vue

@@ -1,9 +1,32 @@
 <template>
-  <MonacoEditor />
+  <div class="w-full h-full overflow-hidden flex flex-col">
+    <ViewTitle title="Schema" />
+    <div class="w-full flex-1 overflow-hidden">
+      <MonacoEditor
+        ref="editorRef"
+        value-format="json"
+        :config="{ minimap: { enabled: true } }"
+        :allow-fullscreen="false"
+      />
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
+import { ref, watch } from 'vue'
 import MonacoEditor from '@/components/MonacoEditor/index.vue'
+import { useProjectStore } from '@/store/modules/project'
+import ViewTitle from '@/components/ViewTitle/index.vue'
+
+const projectStore = useProjectStore()
+const editorRef = ref<InstanceType<typeof MonacoEditor>>()
+
+watch(
+  () => projectStore.project,
+  () => {
+    editorRef.value?.setValue(JSON.stringify(projectStore.project, null, 2))
+  }
+)
 </script>
 
 <style scoped></style>

+ 64 - 0
src/renderer/src/views/designer/sidebar/components/EditBinModal.vue

@@ -0,0 +1,64 @@
+<template>
+  <el-dialog title="编辑BIN" v-model="show" width="440px" align-center>
+    <el-form :model="formData" ref="form" hide-required-asterisk label-position="top">
+      <el-form-item
+        label="BIN名称"
+        prop="name"
+        :rules="[{ required: true, message: '名称不能为空', trigger: 'blur' }]"
+      >
+        <el-input v-model="formData.name"></el-input>
+      </el-form-item>
+      <el-form-item label="BIN路径">
+        <el-input v-model="formData.path"></el-input>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="show = false">取消</el-button>
+      <el-button type="primary" @click="handleSubmit">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type { FormInstance } from 'element-plus'
+import type { Bin } from '@/types/bins'
+
+import { ref, defineExpose } from 'vue'
+import { useProjectStore } from '@/store/modules/project'
+
+const show = ref(false)
+const form = ref<FormInstance>()
+const formData = ref<Bin>({
+  id: '',
+  name: '',
+  path: ''
+})
+const { project } = useProjectStore()
+defineExpose({
+  edit: (binId: string) => {
+    show.value = true
+    const bin = project?.bins.find((bin) => bin.id === binId)
+    if (bin) {
+      formData.value.id = bin.id
+      formData.value.name = bin.name
+      formData.value.path = bin.path
+    }
+  }
+})
+
+const handleSubmit = () => {
+  form.value?.validate((valid) => {
+    if (valid) {
+      project?.bins.find((bin) => {
+        if (bin.id === formData.value.id) {
+          bin.name = formData.value.name
+          bin.path = formData.value.path
+        }
+      })
+      show.value = false
+    }
+  })
+}
+</script>
+
+<style scoped></style>

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

@@ -0,0 +1,152 @@
+<template>
+  <el-dialog
+    v-model="show"
+    title="编辑字体"
+    width="880px"
+    :close-on-click-modal="false"
+    align-center
+  >
+    <!-- <div class="w-full text-10px text-text-secondary flex gap-20px my-12px">
+      <div class="felx-1">
+        <span>文件类型:</span>
+        <span>{{ formData.fielType }}</span>
+      </div>
+    </div> -->
+    <el-form
+      ref="form"
+      label-position="top"
+      :model="formData"
+      :rules="rules"
+      hide-required-asterisk
+    >
+      <el-form-item label="文件名称" prop="fileName">
+        <el-input v-model="formData.fileName" />
+      </el-form-item>
+      <el-form-item label="文件路径" prop="path">
+        <el-input v-model="formData.path" disabled />
+      </el-form-item>
+      <el-form-item label="文本范围" prop="range">
+        <div>
+          <el-radio-group v-model="all" @change="handleChangeAll">
+            <el-radio :value="true">字体全部</el-radio>
+            <el-radio :value="false">自定义</el-radio>
+          </el-radio-group>
+          <el-checkbox-group v-if="!all" v-model="formData.range">
+            <el-checkbox label="page">界面文本</el-checkbox>
+            <el-checkbox label="custom">自定义文本</el-checkbox>
+            <el-checkbox label="range">编码范围</el-checkbox>
+          </el-checkbox-group>
+        </div>
+      </el-form-item>
+      <el-form-item label="编码范围" prop="codeRange" v-if="formData.range.includes('range')">
+        <el-select-v2 v-model="formData.codeRange" :options="rangeOptions" multiple />
+      </el-form-item>
+      <el-form-item label="自定义文本" prop="extText" v-if="formData.range.includes('custom')">
+        <el-input type="textarea" v-model="formData.extText" placeholder="请输入..." />
+      </el-form-item>
+      <el-form-item label="BIN配置" prop="bin" v-if="projectStore.project?.bins?.length">
+        <div class="w-full flex gap-12px">
+          <el-select-v2
+            class="w-1"
+            v-model="formData.bin"
+            :options="projectStore.project.bins"
+            :props="{ label: 'name', value: 'id' }"
+          />
+          <el-button type="text" :disabled="!formData.bin" @click="editBin">
+            <LuPencilLine size="18" />
+          </el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="show = false">取消</el-button>
+      <el-button type="primary" @click="submit">确定</el-button>
+    </template>
+    <EditBinModal ref="editBinModalRef" />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type { FontResource } from '@/types/resource'
+import type { FormInstance } from 'element-plus'
+import { ref, defineExpose, defineEmits } from 'vue'
+import { useProjectStore } from '@/store/modules/project'
+import { klona } from 'klona'
+import { LuPencilLine } from 'vue-icons-plus/lu'
+import EditBinModal from './EditBinModal.vue'
+
+const rangeOptions = [
+  { label: 'ASCII', value: '0x0000-0x007f' },
+  { label: 'Latin-1补充字符', value: '0x0080-0x00FF' },
+  { label: '常用汉字', value: '0x4E00-0x9FA5' },
+  { label: '扩展汉字', value: '0x3400-0x4DBF' },
+  { label: '平假名', value: '0x3040-0x309F' },
+  { label: '片假名', value: '0x30A0-0x30FF' },
+  { label: '韩文', value: '0xAC00-0xD7AF' },
+  { label: '西里尔字母', value: '0x0400-0x04FF' },
+  { label: '希腊字母', value: '0x0370-0x03FF' },
+  { label: '阿拉伯语', value: '0x0600-0x06FF' },
+  { label: '通用标点', value: '0x2000-0x206F' },
+  { label: '货币符号', value: '0x20A0-0x20CF' },
+  { label: '箭头符号', value: '0x2190-0x21FF' },
+  { label: '图标字体', value: '0xF000-0xF8FF' }
+]
+
+const emit = defineEmits(['change'])
+const projectStore = useProjectStore()
+const show = ref(false)
+const form = ref<FormInstance>()
+const all = ref(false)
+const editBinModalRef = ref<InstanceType<typeof EditBinModal>>()
+const formData = ref<FontResource>({
+  id: '',
+  fileName: '',
+  fielType: '',
+  path: '',
+  bin: '',
+  extText: '',
+  range: [],
+  codeRange: []
+})
+
+const rules = {
+  fileName: [
+    {
+      required: true,
+      message: '请输入文件名称',
+      trigger: 'blur'
+    }
+  ],
+  alpha: [{ required: true, message: '请输入透明度', trigger: 'blur' }]
+}
+
+// 切换全部
+const handleChangeAll = (val: any) => {
+  formData.value.range = val ? ['all'] : []
+}
+
+defineExpose({
+  edit: async (resouce: FontResource) => {
+    show.value = true
+    formData.value = klona(resouce)
+    all.value = formData.value.range.includes('all')
+  }
+})
+
+// 编辑BIN
+const editBin = () => {
+  editBinModalRef.value?.edit(formData.value.bin)
+}
+
+// 提交
+const submit = () => {
+  form.value?.validate((valid) => {
+    if (valid) {
+      show.value = false
+      emit('change', formData.value)
+    }
+  })
+}
+</script>
+
+<style scoped></style>

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

@@ -0,0 +1,141 @@
+<template>
+  <el-dialog v-model="show" title="编辑图片" width="440px" align-center>
+    <div
+      class="w-full h-120px border-solid border-1 border-border flex items-center justify-center"
+    >
+      <img :src="imageInfo?.url" style="max-width: 80%; max-height: 80%" />
+    </div>
+    <div class="w-full text-10px text-text-secondary flex gap-20px my-12px">
+      <div class="felx-1">
+        <span>文件类型:</span>
+        <span>{{ imageInfo?.type }}</span>
+      </div>
+      <div class="felx-1">
+        <span>分辨率:</span>
+        <span>{{ imageInfo?.width ? `${imageInfo.width}x${imageInfo.height}` : '' }}</span>
+      </div>
+    </div>
+    <el-form
+      ref="form"
+      label-position="top"
+      :model="formData"
+      :rules="rules"
+      hide-required-asterisk
+    >
+      <el-form-item label="文件名称" prop="fileName">
+        <el-input v-model="formData.fileName" />
+      </el-form-item>
+      <el-form-item label="文件路径" prop="path">
+        <el-input v-model="formData.path" disabled />
+      </el-form-item>
+      <el-form-item label="压缩格式" prop="compressFormat">
+        <el-select v-model="formData.compressFormat">
+          <el-option label="无压缩" value="none"></el-option>
+          <el-option
+            v-for="item in projectStore.imageCompressFormat || []"
+            :label="item"
+            :value="item"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="透明度" prop="alpha">
+        <el-slider v-model="formData.alpha" :min="0" :max="255" show-input />
+      </el-form-item>
+      <el-form-item label="BIN配置" prop="bin" v-if="projectStore.project?.bins?.length">
+        <div class="w-full flex gap-12px">
+          <el-select-v2
+            class="w-1"
+            v-model="formData.bin"
+            :options="projectStore.project.bins"
+            :props="{ label: 'name', value: 'id' }"
+          />
+          <el-button type="text" :disabled="!formData.bin" @click="editBin">
+            <LuPencilLine size="18" />
+          </el-button>
+        </div>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="show = false">取消</el-button>
+      <el-button type="primary" @click="submit">确定</el-button>
+    </template>
+    <EditBinModal ref="editBinModalRef" />
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import type { ImageResource } from '@/types/resource'
+import type { FormInstance } from 'element-plus'
+import { ref, defineExpose, defineEmits } from 'vue'
+import { useProjectStore } from '@/store/modules/project'
+import { klona } from 'klona'
+import { getImageByPath } from '@/utils'
+import { LuPencilLine } from 'vue-icons-plus/lu'
+import EditBinModal from './EditBinModal.vue'
+
+const emit = defineEmits(['change'])
+const projectStore = useProjectStore()
+const show = ref(false)
+const form = ref<FormInstance>()
+const editBinModalRef = ref<InstanceType<typeof EditBinModal>>()
+const formData = ref<ImageResource>({
+  id: '',
+  fileName: '',
+  fielType: '',
+  path: '',
+  compressFormat: 'none',
+  alpha: 255,
+  bin: ''
+})
+
+const imageInfo = ref<{
+  url?: string
+  width?: number
+  height?: number
+  type?: string
+}>()
+
+const rules = {
+  fileName: [
+    {
+      required: true,
+      message: '请输入文件名称',
+      trigger: 'blur'
+    }
+  ],
+  alpha: [{ required: true, message: '请输入透明度', trigger: 'blur' }]
+}
+
+defineExpose({
+  edit: async (resouce: ImageResource) => {
+    show.value = true
+    formData.value = klona(resouce)
+
+    // 获取图片信息
+    const res = await getImageByPath(projectStore.projectPath + resouce.path)
+    imageInfo.value = {
+      url: res?.base64,
+      width: res?.dimensions.width,
+      height: res?.dimensions.height,
+      type: res?.dimensions.type
+    }
+  }
+})
+
+// 编辑BIN
+const editBin = () => {
+  editBinModalRef.value?.edit(formData.value.bin)
+}
+
+// 提交
+const submit = () => {
+  form.value?.validate((valid) => {
+    if (valid) {
+      show.value = false
+      emit('change', formData.value)
+    }
+  })
+}
+</script>
+
+<style scoped></style>

+ 119 - 10
src/renderer/src/views/designer/sidebar/components/ResourceItem.vue

@@ -1,7 +1,14 @@
 <template>
-  <p class="flex items-center justify-between gap-4px px-4 py-2 m-0 overflow-hidden group/item">
+  <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"
+  >
     <span v-if="type === 'font'" class="w-32px h-32px grid place-items-center">
-      <BsFiletypeTtf size="16px" />
+      <BsFiletypeTtf v-if="data.fielType === 'ttf'" size="16px" />
+      <BsFiletypeWoff v-else-if="data.fielType === 'woff'" size="16px" />
+      <img v-else :src="fontImg" class="w-16px h-16px" />
     </span>
     <span
       v-if="type === 'image'"
@@ -15,12 +22,19 @@
     <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 content="编辑" :append-to="listBoxRef">
+        <span v-if="type !== 'other'" class="cursor-pointer" @click="handleEdit"
+          ><LuPencilLine size="14px"
+        /></span>
       </el-tooltip>
-      <el-tooltip content="删除">
+      <el-tooltip content="删除" :append-to="listBoxRef">
         <span>
-          <el-popconfirm 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" /></span>
             </template>
@@ -29,27 +43,122 @@
       </el-tooltip>
     </span>
   </p>
+
+  <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>
+        <el-dropdown-item @click="copyName">复制名称</el-dropdown-item>
+      </el-dropdown-menu>
+    </template>
+  </el-dropdown>
+
+  <EditImageModal ref="editImageModalRef" @change="handleChangeResource" />
+  <EditFontModal ref="editFontModalRef" @change="handleChangeResource" />
 </template>
 
 <script setup lang="ts">
-import type { Resource } from '@/types/resource'
+import type { FontResource, ImageResource, Resource } from '@/types/resource'
+import type { DropdownInstance } from 'element-plus'
 
-import { defineProps, defineEmits } from 'vue'
+import { defineProps, defineEmits, ref } 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'
+import fontImg from '@/assets/font.svg'
+import EditImageModal from './EditImageModal.vue'
+import EditFontModal from './EditFontModal.vue'
+import { ElMessage } from 'element-plus'
 
 defineEmits<{
   delete: []
-  edit: []
 }>()
 
-defineProps<{
+const props = defineProps<{
   data: Resource
   type: 'image' | 'font' | 'other'
 }>()
+
+const listBoxRef = ref<HTMLElement>()
 const projectStore = useProjectStore()
+const editImageModalRef = ref<InstanceType<typeof EditImageModal>>()
+const editFontModalRef = ref<InstanceType<typeof EditFontModal>>()
+
+const dropdownRef = ref<DropdownInstance>()
+const position = ref({
+  top: 0,
+  left: 0,
+  bottom: 0,
+  right: 0
+} as DOMRect)
+
+const triggerRef = ref({
+  getBoundingClientRect: () => position.value
+})
+
+const handleClick = () => {
+  dropdownRef.value?.handleClose()
+}
+
+const handleContextmenu = (event: MouseEvent) => {
+  const { clientX, clientY } = event
+  position.value = DOMRect.fromRect({
+    x: clientX,
+    y: clientY
+  })
+  event.preventDefault()
+  dropdownRef.value?.handleOpen()
+}
+
+// 资源管理器中打开
+const openInExplorer = () => {
+  window.electron.ipcRenderer.invoke(
+    'open-file-in-explorer',
+    projectStore.projectPath + props.data.path
+  )
+}
+
+// 复制名称
+const copyName = () => {
+  navigator.clipboard.writeText(props.data.fileName)
+  ElMessage.success('复制成功')
+}
+
+// 打开编辑
+const handleEdit = () => {
+  if (props.type === 'image') {
+    editImageModalRef.value?.edit(props.data as ImageResource)
+  }
+  if (props.type === 'font') {
+    editFontModalRef.value?.edit(props.data as FontResource)
+  }
+}
+
+// 编辑完成
+const handleChangeResource = async (resouce: Resource) => {
+  Object.entries(resouce).forEach(([key, value]) => {
+    props.data[key] = value
+  })
+  if (resouce.fileName !== props.data.fileName) {
+    // 文件命名修改后,更新路径及本地资源
+    const sourcePath = projectStore.projectPath + props.data.path
+    const targetPath =
+      projectStore.projectPath + props.data.path.replace(props.data.fileName, resouce.fileName)
+    await window.electron.ipcRenderer.invoke('modify-file-name', sourcePath, targetPath)
+    props.data.path = props.data.path.replace(props.data.fileName, resouce.fileName)
+  }
+}
 </script>
 
 <style scoped></style>

+ 2 - 4
src/renderer/src/views/designer/sidebar/index.vue

@@ -46,10 +46,7 @@
     </div>
     <!-- 代码查看 -->
     <div class="flex-1 overflow-hidden" v-show="activeMenu === 'code'">
-      <SplitterCollapse>
-        <SplitterCollapseItem title="屏幕页面"> </SplitterCollapseItem>
-        <SplitterCollapseItem title="页面大纲"> </SplitterCollapseItem>
-      </SplitterCollapse>
+      <Method />
     </div>
     <!-- json预览 -->
     <div class="flex-1 overflow-auto" v-show="activeMenu === 'json'">
@@ -68,6 +65,7 @@ import Hierarchy from './Hierarchy.vue'
 import Libary from './Libary.vue'
 import Schema from './Schema.vue'
 import Resource from './Resource.vue'
+import Method from './Method.vue'
 
 const { t } = useI18n()