feat(plugins): 添加ONNX模型演示插件并优化组件加载逻辑

refactor(component2plugin): 改进组件加载逻辑避免重复加载
style(components.d.ts): 清理未使用的组件类型声明
chore(package.json): 添加build:normal脚本
This commit is contained in:
539943419 committed 2025-08-22 01:20:48 +08:00
1 parent b8bea8e3bd
commit 97276224ee
8 files changed
+372 -8

No files matched your search

-3
View File
@@ -15,19 +15,16 @@ declare module 'vue' {
MiaoLazyDiv: typeof import('./src/components/miaoLazyDiv.vue')['default']
MiaoMask: typeof import('./src/components/miaoMask.vue')['default']
MiaoPopupInput: typeof import('./src/components/miaoPopupInput.vue')['default']
NAlert: typeof import('naive-ui')['NAlert']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NDropdown: typeof import('naive-ui')['NDropdown']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NGrid: typeof import('naive-ui')['NGrid']
NGridItem: typeof import('naive-ui')['NGridItem']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NQrCode: typeof import('naive-ui')['NQrCode']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace']
+1
View File
@@ -9,6 +9,7 @@
"build": "vue-tsc -b && vite build",
"build:auto": "pnpm run build && go env -w GOOS=windows GOARCH=amd64 && rm -rf ./src-server/web/* && cp -r ./dist/* ./src-server/web/ && cd ./src-server && go env -w GOOS=windows && go build -o ../miao-directory.exe",
"build:fast": "bash build.auto.release.sh -fast",
"build:normal": "bash build.auto.release.sh",
"preview": "vite preview",
"format": "prettier --write src/"
},
File renamed without changes.
+4 -3
View File
@@ -1,13 +1,14 @@
import { registerComponentOption } from '@/class/PluginCenter'
import FileTray from '@vicons/ionicons5/es/FileTray'
// import MiaoDirectory from '@/plugins/miaoDirectory/miaoDirectory.vue'
import MiaoDirectory from '@/plugins/miaoDirectory/miaoDirectory.vue'
const pluginConfig: registerComponentOption = {
key: 'miaoDirectory',
name: '文件夹',
icon: FileTray,
getComponent: async () =>
(await import('@/plugins/miaoDirectory/miaoDirectory.vue')).default,
// getComponent: async () =>
// (await import('@/plugins/miaoDirectory/miaoDirectory.vue')).default,
getComponent: async () => MiaoDirectory,
filter: (vDirs, vFiles) => {
if (vFiles.length) {
return false
+51
View File
@@ -0,0 +1,51 @@
import { registerComponentOption } from '@/class/PluginCenter'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import { Analytics } from '@vicons/ionicons5'
const pluginConfig: registerComponentOption = {
key: '45632456',
name: 'ONNX DEMO BIRD',
icon: Analytics,
getComponent: async () =>
(await import('./onnx_demo.vue')).default,
filter: (_vDirs: VirtualDirectory[], vFiles: VirtualFile[]) => {
if (vFiles.length === 0) {
return false
}
// 检查是否存在.onnx文件
let hasOnnxFile = false
// 检查是否存在图片文件
let hasImageFile = false
const imageSuffixList = [
'xbm', 'tif', 'pjp', 'svgz', 'jpg', 'jpeg', 'ico', 'tiff', 'gif',
'svg', 'jfif', 'webp', 'png', 'bmp', 'pjpeg', 'avif'
]
for (let vf of vFiles) {
if (vf.name.endsWith('.onnx')) {
hasOnnxFile = true
return true
}
for (let end of imageSuffixList) {
if (vf.name.endsWith(end)) {
hasImageFile = true
break
}
}
if (hasOnnxFile && hasImageFile) {
return true
}
}
return false
}
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
+306
View File
@@ -0,0 +1,306 @@
<template>
<miao-drop-handler @on-virtual-files="handleDrop">
<miao-message-provider ref="miaoMessageRef">
<div class="onnx-demo" ref="rootRef">
<n-card title="ONNX模型演示" class="onnx-demo-card">
<n-grid :cols="2" :x-gap="12">
<n-grid-item>
<div class="onnx-demo-section">
<h3>输入图像</h3>
<div v-if="inputImage" class="onnx-demo-image-container">
<img :src="inputImage.url" class="onnx-demo-image" />
<p>{{ inputImage.name }}</p>
</div>
<n-empty v-else description="无图像" />
</div>
</n-grid-item>
<n-grid-item>
<div class="onnx-demo-section">
<h3>ONNX模型</h3>
<div v-if="onnxModel" class="onnx-demo-model-info">
<p>模型: {{ onnxModel.name }}</p>
<p>大小: {{ formatFileSize(onnxModel.size) }}</p>
</div>
<n-empty v-else description="无模型" />
</div>
</n-grid-item>
</n-grid>
<!-- <div class="onnx-demo-control">
<n-button type="primary" @click="processImage" :disabled="!inputImage || !onnxModel || isProcessing"
:loading="isProcessing">
处理图像
</n-button>
</div> -->
<div class="onnx-demo-result" v-if="modelOutput">
<h3>模型输出:</h3>
<n-card embedded>
<pre>{{ modelOutput }}</pre>
</n-card>
</div>
</n-card>
</div>
</miao-message-provider>
</miao-drop-handler>
</template>
<script setup lang="ts">
import miaoDropHandler from '@/components/miaoDropHandler.vue'
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
import { VirtualFile } from '@/class/VirtualDirectory'
import VirtualDirectory from '@/class/VirtualDirectory'
import { onMounted, ref, computed, watch } from 'vue'
// @ts-ignore
let ort: any;
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const currentDirectories = defineModel<VirtualDirectory[]>(
'currentDirectories',
{
required: true
}
)
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
const rootRef = ref()
const isProcessing = ref(false)
const modelOutput = ref<string | null>(null)
// 获取第一个图像文件
const inputImage = computed(() => {
const imageSuffixList = [
'xbm', 'tif', 'pjp', 'svgz', 'jpg', 'jpeg', 'ico', 'tiff', 'gif',
'svg', 'jfif', 'webp', 'png', 'bmp', 'pjpeg', 'avif'
]
for (const file of currentFiles.value) {
for (const suffix of imageSuffixList) {
if (file.name.toLowerCase().endsWith(suffix)) {
return file
}
}
}
return null
})
// 获取ONNX模型文件
const onnxModel = computed(() => {
for (const file of currentFiles.value) {
if (file.name.toLowerCase().endsWith('.onnx')) {
return file
}
}
return null
})
const handleDrop = (vFiles: VirtualFile[]) => {
const imageSuffixList = [
'xbm', 'tif', 'pjp', 'svgz', 'jpg', 'jpeg', 'ico', 'tiff', 'gif',
'svg', 'jfif', 'webp', 'png', 'bmp', 'pjpeg', 'avif'
]
let imgFile: VirtualFile | null = null
out: for (const file of vFiles) {
for (const suffix of imageSuffixList) {
if (file.name.toLowerCase().endsWith(suffix)) {
imgFile = file
break out
}
}
}
if (imgFile) {
currentFiles.value.unshift(imgFile)
}
}
// 格式化文件大小
const formatFileSize = (size: number): string => {
if (size < 1024) {
return `${size} B`
} else if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(2)} KB`
} else {
return `${(size / (1024 * 1024)).toFixed(2)} MB`
}
}
const resizeImage = async (img: ImageBitmap, width: number, height: number): Promise<ImageBitmap> => {
const canvas = new OffscreenCanvas(width, height)
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx?.drawImage(img, 0, 0, width, height)
return await createImageBitmap(canvas)
}
// 处理图像
const processImage = async () => {
if (!inputImage.value || !onnxModel.value) {
miaoMessageRef.value?.alertTip('请选择图像和ONNX模型文件', { type: 'error', timeout: 2000 })
return
}
isProcessing.value = true
modelOutput.value = null
try {
const alertUpdate = miaoMessageRef.value!.alertTip('正在加载ONNX模型...', { type: 'info', timeout: 2000 })
const modelResponse = await fetch(onnxModel.value.url)
const modelBuffer = await modelResponse.arrayBuffer()
alertUpdate('正在创建推理会话...')
const session = await ort.InferenceSession.create(modelBuffer)
alertUpdate('正在加载图像...')
const imgResponse = await fetch(inputImage.value.url)
const imgBlob = await imgResponse.blob()
alertUpdate('正在预处理图像...')
const size = 448
const img = await createImageBitmap(imgBlob)
const data = await resizeImage(img, size, size)
const canvas = new OffscreenCanvas(size, size)
const ctx = canvas.getContext('2d') as unknown as CanvasRenderingContext2D
ctx.drawImage(data, 0, 0, size, size)
const imageData = ctx.getImageData(0, 0, size, size).data
const newImageData = new Uint8ClampedArray(size * size * 3)
for (let i = 0; i < size * size; i++) {
newImageData[i * 3] = imageData[i * 4]
newImageData[i * 3 + 1] = imageData[i * 4 + 1]
newImageData[i * 3 + 2] = imageData[i * 4 + 2]
}
// const greyScale = [];
// for (let i = 0; i < imageData.length; i += 4) {
// greyScale.push(
// (imageData[i] * 0.299 + imageData[i + 1] * 0.587 + imageData[i + 2] * 0.114 - 127.5) /
// 127.5
// );
// }
// debugger
const inputTensor = new ort.Tensor('float32', new Float32Array(newImageData), [1, 3, size, size])
alertUpdate('执行模型推理...')
const outputMap = await session.run({
input: inputTensor
})
alertUpdate('处理模型输出结果...')
const outputTensor = Object.values(outputMap)[0]
const formattedOutput = JSON.stringify(
{
data: [...[...(outputTensor as { data: Float32Array }).data].entries()].sort((a, b) => b[1] - a[1]).map(([index, value]) => ({ index, value }))
},
null,
2
)
modelOutput.value = formattedOutput
alertUpdate('处理完成!', { type: 'success' })
} catch (error) {
console.error('ONNX处理错误:', error)
miaoMessageRef.value?.alertTip(`处理出错`, { type: 'error', timeout: 2000 })
modelOutput.value = `错误: ${error}`
} finally {
isProcessing.value = false
}
}
onMounted(async () => {
console.log('currentDirectories.value[0].url', currentDirectories.value[0]?.url)
if (currentDirectories.value && currentDirectories.value.length > 0 && currentDirectories.value[0].url) {
const ortModule = await import(/* @vite-ignore */ `${currentDirectories.value[0].url}dist/ort.all.min.mjs`);
ort = ortModule;
console.log('ort', ort)
} else {
// @ts-ignore
const ortModule = await import(/* @vite-ignore */ 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.all.min.js');
ort = ortModule;
}
await initModel();
})
const initModel = async () => {
if (inputImage.value && onnxModel.value) {
miaoMessageRef.value?.alertTip('已检测到图像和ONNX模型,可以进行处理', { type: 'info', timeout: 2000 })
}
watch(() => inputImage.value, () => {
processImage()
}, {
immediate: true
})
}
</script>
<style scoped>
.onnx-demo {
width: 100%;
height: 100%;
padding: 16px;
overflow: auto;
}
.onnx-demo-card {
margin-bottom: 16px;
}
.onnx-demo-section {
padding: 8px;
text-align: center;
border: 1px dashed #ccc;
border-radius: 4px;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: center;
}
.onnx-demo-image-container {
display: flex;
flex-direction: column;
align-items: center;
}
.onnx-demo-image {
max-width: 100%;
max-height: 180px;
object-fit: contain;
}
.onnx-demo-model-info {
text-align: left;
width: 100%;
}
.onnx-demo-control {
display: flex;
justify-content: center;
margin: 16px 0;
}
.onnx-demo-result {
margin-top: 16px;
border-top: 1px solid #eee;
padding-top: 16px;
}
.onnx-demo-result pre {
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow: auto;
background-color: #f5f5f5;
padding: 8px;
border-radius: 4px;
}
</style>
+10 -2
View File
@@ -3,6 +3,7 @@ import VirtualDirectory, { VirtualFile } from "@/class/VirtualDirectory"
import VirtualPage from "@/class/VirtualPage"
import usePluginCenter from "@/hooks/usePluginCenter"
import useVirtualPages from "@/hooks/useVirtualPages"
import { Component } from "vue"
export const componentOption2pluginOption = (option: registerComponentOption) => {
const {
@@ -17,7 +18,8 @@ export const componentOption2pluginOption = (option: registerComponentOption) =>
single = false,
priority = 0
} = option
let _component: any
let _component: Component
let _componentPromise: Promise<Component>
const views = useVirtualPages()
const pluginCenter = usePluginCenter()
return {
@@ -36,14 +38,20 @@ export const componentOption2pluginOption = (option: registerComponentOption) =>
}
if (_component === undefined) {
if (_componentPromise !== undefined) {
await _componentPromise
}
else {
const setAlertTip = pluginCenter.globalAlertTip(`加载 ${name} 插件中`)
_component = await getComponent()
_componentPromise = getComponent()
_component = await _componentPromise
setAlertTip &&
setAlertTip(`加载 ${name} 插件完成`, {
type: 'success',
timeout: 2000
})
}
}
const _findRes = views.find(_component) as VirtualPage[]
if (single && _findRes.length !== 0) {
const view = _findRes[0]