update
This commit is contained in:
1 parent
286dd8e386
commit
01085a3ba5
12 files changed
+1114
-189
No files matched your search
+5
-1
@@ -35,4 +35,8 @@ auto-imports.d.ts
|
|||||||
|
|
||||||
components.d.ts
|
components.d.ts
|
||||||
|
|
||||||
src/plugins/onnx_demo_*
|
# src/plugins/onnx_demo_*
|
||||||
|
|
||||||
|
public/test
|
||||||
|
|
||||||
|
doc
|
||||||
@@ -26,121 +26,3 @@
|
|||||||
## 碎碎念:
|
## 碎碎念:
|
||||||
|
|
||||||
写的真潦草
|
写的真潦草
|
||||||
|
|
||||||
## 让cursor生成了个架构图,可能不是很专业,不过我看了感觉没啥问题:
|
|
||||||
```
|
|
||||||
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ miaoDirectory前端架构 │
|
|
||||||
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
▲
|
|
||||||
│
|
|
||||||
│ HTTP API通信
|
|
||||||
│
|
|
||||||
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 后端服务 │
|
|
||||||
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
|
|
||||||
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 应用入口 (App.vue) │
|
|
||||||
│ ┌──────────────────────────┐ ┌────────────────────────────┐ ┌────────────────────────────────┐ │
|
|
||||||
│ │ 全局状态初始化 │ │ 全局消息提供器 │ │ 全局导航菜单 │ │
|
|
||||||
│ │ (初始化各个系统核心实例) │ │ (miaoAlertTipProvider) │ │ │ │
|
|
||||||
│ └──────────────┬───────────┘ └─────────────┬──────────────┘ └────────────────────────────────┘ │
|
|
||||||
│ │ │ │
|
|
||||||
│ ▼ ▼ │
|
|
||||||
│ ┌──────────────────────────┐ ┌────────────────────────────┐ │
|
|
||||||
│ │ 视图控制器渲染 │ │ 视图容器渲染 │ │
|
|
||||||
│ │ (ViewController) │ │ (ViewContainer) │ │
|
|
||||||
│ └──────────────────────────┘ └────────────────────────────┘ │
|
|
||||||
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
│ │
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 多页面系统 │
|
|
||||||
│ │
|
|
||||||
│ ┌──────────────────────────────────────────┐ ┌───────────────────────────────────────────────┐ │
|
|
||||||
│ │ VirtualPages类 │ │ VirtualPage类 │ │
|
|
||||||
│ │ ┌────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────┐ │ │
|
|
||||||
│ │ │ _views │ │ │ │ component: 页面使用的组件 │ │ │
|
|
||||||
│ │ │ (存储所有虚拟页面的响应式数组) │ │ │ │ name: 页面名 │ │ │
|
|
||||||
│ │ └────────────────────────────────────┘ │ │ │ visible: 是否可见 │ │ │
|
|
||||||
│ │ │ │ │ currentDirectories: 当前使用的目录 │ │ │
|
|
||||||
│ │ ┌────────────────────────────────────┐ │ │ │ currentFiles: 当前使用的文件 │ │ │
|
|
||||||
│ │ │ getIndex(): 获取页面索引 │ │ │ │ id: 唯一标识符 │ │ │
|
|
||||||
│ │ │ getView(): 根据ID获取页面 │ │ │ │ color: 页面颜色 │ │ │
|
|
||||||
│ │ │ push(): 添加新页面 │◄──┼────┼─┤ title: 页面标题 │ │ │
|
|
||||||
│ │ │ deleteView(): 删除页面 │ │ │ │ exitConfirm: 关闭时是否需要确认 │ │ │
|
|
||||||
│ │ │ find(): 查找页面 │ │ │ │ allowCopy: 是否允许复制页面 │ │ │
|
|
||||||
│ │ └────────────────────────────────────┘ │ │ └─────────────────────────────────────────┘ │ │
|
|
||||||
│ └──────────────────────────────────────────┘ │ │ │
|
|
||||||
│ ▲ │ ┌─────────────────────────────────────────┐ │ │
|
|
||||||
│ │ │ │ setTitle(): 设置标题 │ │ │
|
|
||||||
│ │ │ │ switchShow(): 切换显示状态 │ │ │
|
|
||||||
│ │ │ │ refresh(): 刷新页面 │ │ │
|
|
||||||
│ │ │ └─────────────────────────────────────────┘ │ │
|
|
||||||
│ │ └───────────────────────────────────────────────┘ │
|
|
||||||
│ │ ▲ │
|
|
||||||
│ │ │ │
|
|
||||||
│ useVirtualPages() │ │
|
|
||||||
│ (全局单例访问钩子) │ │
|
|
||||||
│ │ │
|
|
||||||
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
│ │
|
|
||||||
│ │
|
|
||||||
▼ │
|
|
||||||
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 插件系统 │
|
|
||||||
│ │
|
|
||||||
│ ┌──────────────────────────────────────────┐ ┌───────────────────────────────────────────────┐ │
|
|
||||||
│ │ PluginCenter类 │ │ 插件接口定义 │ │
|
|
||||||
│ │ ┌────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────┐ │ │
|
|
||||||
│ │ │ pluginsMap │ │ │ │ name: 插件名称 │ │ │
|
|
||||||
│ │ │ (存储已注册插件的映射表) │ │ │ │ icon: 插件图标 │ │ │
|
|
||||||
│ │ └────────────────────────────────────┘ │ │ │ disable: 是否禁用 │ │ │
|
|
||||||
│ │ │ │ │ group: 所属插件组 │ │ │
|
|
||||||
│ │ ┌────────────────────────────────────┐ │ │ │ priority: 优先级 │ │ │
|
|
||||||
│ │ │ register(): 注册插件 │ │ │ │ filter(): 适用性过滤函数 │ │ │
|
|
||||||
│ │ │ deregister(): 注销插件 │ │ │ │ func(): 插件主函数 │ │ │
|
|
||||||
│ │ │ registerComponent(): 注册组件插件 │ │ │ └─────────────────────────────────────────┘ │ │
|
|
||||||
│ │ │ getUsablePlugin(): 获取可用插件 │───┼────┼─► 创建虚拟页面 │ │
|
|
||||||
│ │ │ usePlugin(): 使用插件 │ │ │ │ │
|
|
||||||
│ │ └────────────────────────────────────┘ │ │ ┌─────────────────────────────────────────┐ │ │
|
|
||||||
│ └──────────────────────────────────────────┘ │ │ 插件分组(PluginGroup) │ │ │
|
|
||||||
│ ▲ │ │ │ │ │
|
|
||||||
│ │ │ │ default: 默认组 │ │ │
|
|
||||||
│ │ │ │ mainMenu: 主菜单组 │ │ │
|
|
||||||
│ │ │ │ immediately: 立即执行组 │ │ │
|
|
||||||
│ │ │ └─────────────────────────────────────────┘ │ │
|
|
||||||
│ │ └───────────────────────────────────────────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ usePluginCenter() │
|
|
||||||
│ (全局单例访问钩子) │
|
|
||||||
│ │
|
|
||||||
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ 虚拟文件系统 │
|
|
||||||
│ │
|
|
||||||
│ ┌──────────────────────────────────────────┐ ┌───────────────────────────────────────────────┐ │
|
|
||||||
│ │ VirtualDirectory类 │ │ VirtualFile类 │ │
|
|
||||||
│ │ ┌────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────┐ │ │
|
|
||||||
│ │ │ name: 目录名 │ │ │ │ name: 文件名 │ │ │
|
|
||||||
│ │ │ path: 完整路径 │ │ │ │ size: 文件大小 │ │ │
|
|
||||||
│ │ │ stats: 文件状态信息 │ │ │ │ stats: 文件状态信息 │ │ │
|
|
||||||
│ │ │ files: 文件列表 │◄──┼────┼─► parent: 父目录 │ │ │
|
|
||||||
│ │ │ directories: 子目录列表 │ │ │ │ id: 唯一标识符 │ │ │
|
|
||||||
│ │ │ parent?: 父目录 │ │ │ │ path: 完整路径(计算属性) │ │ │
|
|
||||||
│ │ └────────────────────────────────────┘ │ │ └─────────────────────────────────────────┘ │ │
|
|
||||||
│ └──────────────────────────────────────────┘ └───────────────────────────────────────────────┘ │
|
|
||||||
│ ▲ │
|
|
||||||
│ │ │
|
|
||||||
│ │ │
|
|
||||||
│ useRootVDirectory() │
|
|
||||||
│ (获取根目录实例钩子) │
|
|
||||||
│ │
|
|
||||||
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
@@ -11,14 +11,14 @@ cd ./src-server
|
|||||||
echo "building amd64 win"
|
echo "building amd64 win"
|
||||||
|
|
||||||
go env -w GOOS=windows GOARCH=amd64
|
go env -w GOOS=windows GOARCH=amd64
|
||||||
go build -o ../miao-directory-amd64-win.exe
|
go build -ldflags="-s -w" -o ../miao-directory-amd64-win.exe
|
||||||
|
|
||||||
echo "building amd64 linux"
|
echo "building amd64 linux"
|
||||||
|
|
||||||
go env -w GOOS=linux GOARCH=amd64
|
go env -w GOOS=linux GOARCH=amd64
|
||||||
go build -o ../miao-directory-amd64-linux
|
go build -ldflags="-s -w" -o ../miao-directory-amd64-linux
|
||||||
|
|
||||||
# go env -w GOOS=linux GOARCH=arm64
|
# go env -w GOOS=linux GOARCH=arm64
|
||||||
# go build -o ../miao-directory-arm64-linux
|
# go build -ldflags="-s -w" -o ../miao-directory-arm64-linux
|
||||||
|
|
||||||
echo "build finish"
|
echo "build finish"
|
||||||
@@ -72,6 +72,7 @@ func GetHandler(c *gin.Context) {
|
|||||||
switch ext {
|
switch ext {
|
||||||
case ".js":
|
case ".js":
|
||||||
case ".cjs":
|
case ".cjs":
|
||||||
|
case ".mjs":
|
||||||
c.Writer.Header().Set("Content-Type", "application/javascript")
|
c.Writer.Header().Set("Content-Type", "application/javascript")
|
||||||
case ".css":
|
case ".css":
|
||||||
c.Writer.Header().Set("Content-Type", "text/css")
|
c.Writer.Header().Set("Content-Type", "text/css")
|
||||||
@@ -79,6 +80,8 @@ func GetHandler(c *gin.Context) {
|
|||||||
c.Writer.Header().Set("Content-Type", "text/html")
|
c.Writer.Header().Set("Content-Type", "text/html")
|
||||||
case ".json":
|
case ".json":
|
||||||
c.Writer.Header().Set("Content-Type", "application/json")
|
c.Writer.Header().Set("Content-Type", "application/json")
|
||||||
|
case ".wasm":
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/wasm")
|
||||||
default:
|
default:
|
||||||
c.Writer.Header().Set("Content-Type", "application/octet-stream")
|
c.Writer.Header().Set("Content-Type", "application/octet-stream")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,31 @@ import config from '@/config'
|
|||||||
|
|
||||||
const miaoFetchApi = useMiaoFetchApi()
|
const miaoFetchApi = useMiaoFetchApi()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询事件直到完成
|
||||||
|
* @param eventId 事件ID
|
||||||
|
* @param maxRetries 最大重试次数
|
||||||
|
* @param interval 轮询间隔(毫秒)
|
||||||
|
* @returns 事件结果
|
||||||
|
*/
|
||||||
|
async function pollEventUntilComplete(eventId: number, maxRetries = 100, interval = 300) {
|
||||||
|
let _count = 0;
|
||||||
|
while (_count < maxRetries) {
|
||||||
|
try {
|
||||||
|
const { response } = miaoFetchApi.query(eventId);
|
||||||
|
const result = await response;
|
||||||
|
if (result.status === 'success' || result.status === 'failed') {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
_count++;
|
||||||
|
await new Promise(_ => setTimeout(_, interval));
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('poll timeout');
|
||||||
|
}
|
||||||
|
|
||||||
export class VirtualRemoteFile extends VirtualFileBase {
|
export class VirtualRemoteFile extends VirtualFileBase {
|
||||||
constructor(info: file, parent: VirtualDirectoryBase) {
|
constructor(info: file, parent: VirtualDirectoryBase) {
|
||||||
super(info.name, info.size, info.stats, parent)
|
super(info.name, info.size, info.stats, parent)
|
||||||
@@ -20,8 +45,13 @@ export class VirtualRemoteFile extends VirtualFileBase {
|
|||||||
retry: 5
|
retry: 5
|
||||||
});
|
});
|
||||||
const id = (await response).eventId;
|
const id = (await response).eventId;
|
||||||
const { response: res } = miaoFetchApi.query(id);
|
|
||||||
const eventResult = await res;
|
const eventResult = await pollEventUntilComplete(id);
|
||||||
|
|
||||||
|
if (eventResult.status === 'success' && this.parent) {
|
||||||
|
await this.parent.update(); // 操作成功后更新父目录
|
||||||
|
}
|
||||||
|
|
||||||
return eventResult.status === 'success';
|
return eventResult.status === 'success';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除文件失败:', error);
|
console.error('删除文件失败:', error);
|
||||||
@@ -39,8 +69,13 @@ export class VirtualRemoteFile extends VirtualFileBase {
|
|||||||
retry: 5
|
retry: 5
|
||||||
});
|
});
|
||||||
const id = (await response).eventId;
|
const id = (await response).eventId;
|
||||||
const { response: res } = miaoFetchApi.query(id);
|
|
||||||
const eventResult = await res;
|
const eventResult = await pollEventUntilComplete(id);
|
||||||
|
|
||||||
|
if (eventResult.status === 'success' && this.parent) {
|
||||||
|
await this.parent.update(); // 操作成功后更新父目录
|
||||||
|
}
|
||||||
|
|
||||||
return eventResult.status === 'success';
|
return eventResult.status === 'success';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重命名文件失败:', error);
|
console.error('重命名文件失败:', error);
|
||||||
@@ -57,12 +92,13 @@ export class VirtualRemoteFile extends VirtualFileBase {
|
|||||||
const _from = this.parent;
|
const _from = this.parent;
|
||||||
const { response } = miaoFetchApi.cut(this, targetDir);
|
const { response } = miaoFetchApi.cut(this, targetDir);
|
||||||
const id = (await response).eventId;
|
const id = (await response).eventId;
|
||||||
const { response: res } = miaoFetchApi.query(id);
|
|
||||||
const eventResult = await res;
|
const eventResult = await pollEventUntilComplete(id);
|
||||||
const success = eventResult.status === 'success';
|
const success = eventResult.status === 'success';
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
_from.update();
|
if (_from) await _from.update(); // 更新源目录
|
||||||
|
await targetDir.update(); // 更新目标目录
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
@@ -79,40 +115,87 @@ export class VirtualRemoteDirectory extends VirtualDirectoryBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get url(): string {
|
get url(): string {
|
||||||
return `${config.api.get}${this.path}/`
|
return `${config.api.get}${this.path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
updateContent(content: (file | directory)[]) {
|
updateContent(content: (file | directory)[]) {
|
||||||
this._isUpdated = true
|
this._isUpdated = true
|
||||||
|
|
||||||
// 创建名称到内容项的映射,O(n)
|
const files = content.filter(item => item.type === 'file') as file[];
|
||||||
const contentMap = new Map<string, file | directory>();
|
const directories = content.filter(item => item.type === 'directory') as directory[];
|
||||||
content.forEach(item => contentMap.set(item.name, item));
|
|
||||||
|
|
||||||
// 使用Set存储所有内容项的名称,用于O(1)查找,O(n)
|
this.diffFiles(files);
|
||||||
const itemNamesSet = new Set(content.map(v => v.name));
|
this.diffDirectories(directories);
|
||||||
|
}
|
||||||
|
|
||||||
// 删除不再存在的文件和目录,O(m)
|
/**
|
||||||
this.files = this.files.filter(file => itemNamesSet.has(file.name));
|
* 文件diff算法,类似Vue的diff
|
||||||
this.directories = this.directories.filter(dir => itemNamesSet.has(dir.name));
|
* @param newFiles 新文件列表
|
||||||
|
*/
|
||||||
|
private diffFiles(newFiles: file[]) {
|
||||||
|
if (this.files.length === 0) {
|
||||||
|
this.files = newFiles.map(file => new VirtualRemoteFile(file, this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 创建现有文件和目录的名称集合,O(m)
|
const oldFiles = [...this.files];
|
||||||
const existingFileNames = new Set(this.files.map(file => file.name));
|
const newFilesList: VirtualRemoteFile[] = [];
|
||||||
const existingDirNames = new Set(this.directories.map(dir => dir.name));
|
|
||||||
|
|
||||||
// 添加新文件和目录,O(n)
|
const oldKeyMap = new Map<string, VirtualRemoteFile>();
|
||||||
content.forEach(item => {
|
oldFiles.forEach(file => oldKeyMap.set(file.name, file as VirtualRemoteFile));
|
||||||
// 如果是文件且不存在于现有文件中
|
|
||||||
if (item.type === 'file' && !existingFileNames.has(item.name)) {
|
for (let i = 0; i < newFiles.length; i++) {
|
||||||
const nvf = new VirtualRemoteFile(item as file, this);
|
const newFile = newFiles[i];
|
||||||
this.files.push(nvf);
|
const key = newFile.name;
|
||||||
|
|
||||||
|
const oldFile = oldKeyMap.get(key);
|
||||||
|
|
||||||
|
if (oldFile) {
|
||||||
|
oldFile.size = newFile.size;
|
||||||
|
oldFile.stats = newFile.stats;
|
||||||
|
newFilesList.push(oldFile);
|
||||||
|
// 从旧映射中删除,表示已处理
|
||||||
|
oldKeyMap.delete(key);
|
||||||
|
} else {
|
||||||
|
newFilesList.push(new VirtualRemoteFile(newFile, this));
|
||||||
}
|
}
|
||||||
// 如果是目录且不存在于现有目录中
|
}
|
||||||
else if (item.type === 'directory' && !existingDirNames.has(item.name)) {
|
|
||||||
const nvd = new VirtualRemoteDirectory(item as directory, this);
|
this.files = newFilesList;
|
||||||
this.directories.push(nvd);
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目录diff算法,类似Vue的diff
|
||||||
|
* @param newDirs 新目录列表
|
||||||
|
*/
|
||||||
|
private diffDirectories(newDirs: directory[]) {
|
||||||
|
if (this.directories.length === 0) {
|
||||||
|
this.directories = newDirs.map(dir => new VirtualRemoteDirectory(dir, this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldDirs = [...this.directories];
|
||||||
|
const newDirsList: VirtualRemoteDirectory[] = [];
|
||||||
|
|
||||||
|
const oldKeyMap = new Map<string, VirtualRemoteDirectory>();
|
||||||
|
oldDirs.forEach(dir => oldKeyMap.set(dir.name, dir as VirtualRemoteDirectory));
|
||||||
|
|
||||||
|
for (let i = 0; i < newDirs.length; i++) {
|
||||||
|
const newDir = newDirs[i];
|
||||||
|
const key = newDir.name;
|
||||||
|
|
||||||
|
const oldDir = oldKeyMap.get(key);
|
||||||
|
|
||||||
|
if (oldDir) {
|
||||||
|
oldDir.stats = newDir.stats;
|
||||||
|
newDirsList.push(oldDir);
|
||||||
|
oldKeyMap.delete(key);
|
||||||
|
} else {
|
||||||
|
newDirsList.push(new VirtualRemoteDirectory(newDir, this));
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
this.directories = newDirsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(): Promise<void> {
|
async update(): Promise<void> {
|
||||||
@@ -126,8 +209,13 @@ export class VirtualRemoteDirectory extends VirtualDirectoryBase {
|
|||||||
retry: 5
|
retry: 5
|
||||||
});
|
});
|
||||||
const id = (await response).eventId;
|
const id = (await response).eventId;
|
||||||
const { response: res } = miaoFetchApi.query(id);
|
|
||||||
const eventResult = await res;
|
const eventResult = await pollEventUntilComplete(id);
|
||||||
|
|
||||||
|
if (eventResult.status === 'success' && this.parent) {
|
||||||
|
await this.parent.update(); // 操作成功后更新父目录
|
||||||
|
}
|
||||||
|
|
||||||
return eventResult.status === 'success';
|
return eventResult.status === 'success';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('删除目录失败:', error);
|
console.error('删除目录失败:', error);
|
||||||
@@ -145,8 +233,13 @@ export class VirtualRemoteDirectory extends VirtualDirectoryBase {
|
|||||||
retry: 5
|
retry: 5
|
||||||
});
|
});
|
||||||
const id = (await response).eventId;
|
const id = (await response).eventId;
|
||||||
const { response: res } = miaoFetchApi.query(id);
|
|
||||||
const eventResult = await res;
|
const eventResult = await pollEventUntilComplete(id);
|
||||||
|
|
||||||
|
if (eventResult.status === 'success' && this.parent) {
|
||||||
|
await this.parent.update(); // 操作成功后更新父目录
|
||||||
|
}
|
||||||
|
|
||||||
return eventResult.status === 'success';
|
return eventResult.status === 'success';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('重命名目录失败:', error);
|
console.error('重命名目录失败:', error);
|
||||||
@@ -168,12 +261,13 @@ export class VirtualRemoteDirectory extends VirtualDirectoryBase {
|
|||||||
const _from = this.parent;
|
const _from = this.parent;
|
||||||
const { response } = miaoFetchApi.cut(this, targetDir);
|
const { response } = miaoFetchApi.cut(this, targetDir);
|
||||||
const id = (await response).eventId;
|
const id = (await response).eventId;
|
||||||
const { response: res } = miaoFetchApi.query(id);
|
|
||||||
const eventResult = await res;
|
const eventResult = await pollEventUntilComplete(id);
|
||||||
const success = eventResult.status === 'success';
|
const success = eventResult.status === 'success';
|
||||||
|
|
||||||
if (success && _from) {
|
if (success) {
|
||||||
_from.update();
|
if (_from) await _from.update(); // 更新源目录
|
||||||
|
await targetDir.update(); // 更新目标目录
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="chat-main">
|
<div v-else class="chat-main">
|
||||||
<div class="chat-history" ref="chatHistoryRef">
|
<div class="chat-history" ref="chatHistoryRef">
|
||||||
<div v-for="(message, index) in chatHistory" :key="index"
|
<div v-for="(message, index) in chatHistory" :key="index"
|
||||||
:class="['message', message.role === 'user' ? 'user-message' : 'ai-message']">
|
:class="['message', message.role === 'user' ? 'user-message' : 'ai-message']">
|
||||||
<div class="message-content" v-if="message.role === 'user'">{{ message.content }}</div>
|
<div class="message-content" v-if="message.role === 'user'">{{ message.content }}</div>
|
||||||
<div class="message-content markdown-content" v-else v-html="renderMarkdown(message.content)"></div>
|
<div class="message-content markdown-content" v-else v-html="renderMarkdown(message.content)"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 实时生成的内容 -->
|
<!-- 实时生成的内容 -->
|
||||||
<div v-if="generatingMessage" class="message ai-message">
|
<div v-if="generatingMessage" class="message ai-message">
|
||||||
<div class="message-content markdown-content" v-html="renderMarkdown(currentGeneratedText)"></div>
|
<div class="message-content markdown-content" v-html="renderMarkdown(currentGeneratedText)"></div>
|
||||||
<div class="generating-indicator"><span>.</span><span>.</span><span>.</span></div>
|
<div class="generating-indicator"><span>.</span><span>.</span><span>.</span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input">
|
<div class="chat-input">
|
||||||
<textarea v-model="userInput" @keydown.enter.prevent="sendMessage" placeholder="输入消息..."
|
<textarea v-model="userInput" @keydown.enter.prevent="sendMessage" placeholder="输入消息..."
|
||||||
@@ -96,7 +96,7 @@ const initializeModel = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const modelFile = currentFiles.value[0];
|
const modelFile = currentFiles.value[0];
|
||||||
const modelName = modelFile.name.split('.model.bin')[0];
|
const modelName = modelFile.name.split('.model.bin')[0] && modelFile.name.split('.model.task')[0];
|
||||||
|
|
||||||
// 第一阶段:开始加载
|
// 第一阶段:开始加载
|
||||||
updateLoadingProgress(5, '正在加载MediaPipe LLM引擎...');
|
updateLoadingProgress(5, '正在加载MediaPipe LLM引擎...');
|
||||||
@@ -125,7 +125,7 @@ const initializeModel = async () => {
|
|||||||
// 初始化FilesetResolver
|
// 初始化FilesetResolver
|
||||||
updateLoadingProgress(20, '正在初始化MediaPipe文件解析器...');
|
updateLoadingProgress(20, '正在初始化MediaPipe文件解析器...');
|
||||||
const genai = await FilesetResolver.forGenAiTasks(
|
const genai = await FilesetResolver.forGenAiTasks(
|
||||||
currentDirectories.value[0].url ?? "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-genai@latest/wasm"
|
currentDirectories.value[0]?.url ?? "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-genai@latest/wasm"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 第二阶段:文件解析器加载完成
|
// 第二阶段:文件解析器加载完成
|
||||||
@@ -221,8 +221,8 @@ const sendMessage = async () => {
|
|||||||
// 累加部分结果并更新UI
|
// 累加部分结果并更新UI
|
||||||
currentGeneratedText.value += partialResult;
|
currentGeneratedText.value += partialResult;
|
||||||
const banStart = [',', ',']
|
const banStart = [',', ',']
|
||||||
for(const ban of banStart) {
|
for (const ban of banStart) {
|
||||||
if(currentGeneratedText.value.startsWith(ban)) {
|
if (currentGeneratedText.value.startsWith(ban)) {
|
||||||
currentGeneratedText.value = currentGeneratedText.value.slice(1)
|
currentGeneratedText.value = currentGeneratedText.value.slice(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,17 +283,23 @@ const cleanupResources = () => {
|
|||||||
|
|
||||||
// 释放LLM实例资源
|
// 释放LLM实例资源
|
||||||
if (llmInstance.value) {
|
if (llmInstance.value) {
|
||||||
try {
|
const releaseResources = async () => {
|
||||||
// 调用模型实例的清理方法(如果有)
|
try {
|
||||||
if (typeof llmInstance.value.close === 'function') {
|
// 调用模型实例的清理方法(如果有)
|
||||||
llmInstance.value.close();
|
if (typeof llmInstance.value.close === 'function') {
|
||||||
|
llmInstance.value.close();
|
||||||
|
}
|
||||||
|
// 置空实例引用
|
||||||
|
llmInstance.value = null;
|
||||||
|
console.log('组件卸载:已释放模型资源');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('释放模型资源时出错:', error);
|
||||||
|
setTimeout(() => {
|
||||||
|
releaseResources();
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
// 置空实例引用
|
|
||||||
llmInstance.value = null;
|
|
||||||
console.log('组件卸载:已释放模型资源');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('释放模型资源时出错:', error);
|
|
||||||
}
|
}
|
||||||
|
releaseResources();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -321,7 +327,6 @@ watch(() => currentFiles.value, () => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +363,7 @@ watch(() => currentFiles.value, () => {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const pluginConfig: registerComponentOption = {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for (let i of vFiles) {
|
for (let i of vFiles) {
|
||||||
for (let s of ['model.bin']) {
|
for (let s of ['model.bin', 'model.task']) {
|
||||||
if (i.name.endsWith(s)) {
|
if (i.name.endsWith(s)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { registerComponentOption } from '@/class/PluginCenter'
|
||||||
|
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
|
||||||
|
import { Analytics } from '@vicons/ionicons5'
|
||||||
|
|
||||||
|
const pluginConfig: registerComponentOption = {
|
||||||
|
key: 'onnx_demo_emotion',
|
||||||
|
name: 'ONNX DEMO EMOTION',
|
||||||
|
icon: Analytics,
|
||||||
|
getComponent: async () =>
|
||||||
|
(await import('@/plugins/onnx_demo_emotion/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
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
<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 })
|
||||||
|
|
||||||
|
// 1. 加载模型
|
||||||
|
const modelResponse = await fetch(onnxModel.value.url)
|
||||||
|
const modelBuffer = await modelResponse.arrayBuffer()
|
||||||
|
alertUpdate('正在创建推理会话...')
|
||||||
|
|
||||||
|
// 创建ONNX会话
|
||||||
|
const session = await ort.InferenceSession.create(modelBuffer)
|
||||||
|
|
||||||
|
// 2. 加载和预处理图像
|
||||||
|
alertUpdate('正在加载图像...')
|
||||||
|
const imgResponse = await fetch(inputImage.value.url)
|
||||||
|
const imgBlob = await imgResponse.blob()
|
||||||
|
|
||||||
|
// 将图像转换为适合模型的格式
|
||||||
|
alertUpdate('正在预处理图像...')
|
||||||
|
const size = 64
|
||||||
|
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 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const inputTensor = new ort.Tensor('float32', new Float32Array(greyScale), [1, 1, size, size])
|
||||||
|
|
||||||
|
alertUpdate('执行模型推理...')
|
||||||
|
|
||||||
|
// 3. 运行模型推理
|
||||||
|
const outputMap = await session.run({
|
||||||
|
// 这里的键名应该根据实际模型的输入名称进行调整
|
||||||
|
Input2505: inputTensor
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 处理模型输出
|
||||||
|
alertUpdate('处理模型输出结果...')
|
||||||
|
|
||||||
|
// 获取输出数据(假设输出张量名为"output",可能需要根据实际模型调整)
|
||||||
|
const outputTensor = Object.values(outputMap)[0]
|
||||||
|
// 将输出格式化为可读内容
|
||||||
|
const formattedOutput = JSON.stringify(
|
||||||
|
{
|
||||||
|
// shape: outputTensor.dims,
|
||||||
|
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)
|
||||||
|
// 如果传入了onnxruntime-web,则使用传入的,否则从jsdelivr加载
|
||||||
|
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 {
|
||||||
|
// 从jsdelivr加载
|
||||||
|
// @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>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { registerComponentOption } from '@/class/PluginCenter'
|
||||||
|
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
|
||||||
|
import { Analytics } from '@vicons/ionicons5'
|
||||||
|
|
||||||
|
const pluginConfig: registerComponentOption = {
|
||||||
|
key: 'onnx_demo_mnist',
|
||||||
|
name: 'ONNX DEMO MNIST',
|
||||||
|
icon: Analytics,
|
||||||
|
getComponent: async () =>
|
||||||
|
// @ts-ignore
|
||||||
|
(await import('@/plugins/onnx_demo_mnist/onnx_demo_mnist.vue')).default,
|
||||||
|
filter: (_vDirs: VirtualDirectory[], vFiles: VirtualFile[]) => {
|
||||||
|
if (vFiles.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否存在.onnx文件
|
||||||
|
for (let vf of vFiles) {
|
||||||
|
if (vf.name.endsWith('.onnx')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default pluginConfig
|
||||||
|
|
||||||
|
import { PluginExportType } from '@/types/type'
|
||||||
|
export const type = PluginExportType.component
|
||||||
@@ -0,0 +1,536 @@
|
|||||||
|
<template>
|
||||||
|
<miao-drop-handler @on-virtual-files="handleDrop">
|
||||||
|
<miao-message-provider ref="miaoMessageRef">
|
||||||
|
<div class="mnist-demo" ref="rootRef">
|
||||||
|
<n-card title="MNIST手写数字识别" class="mnist-demo-card">
|
||||||
|
<n-grid :cols="2" :x-gap="12">
|
||||||
|
<n-grid-item>
|
||||||
|
<div class="mnist-demo-section">
|
||||||
|
<h3>手写数字画板 (28×28像素)</h3>
|
||||||
|
<div class="canvas-container">
|
||||||
|
<canvas ref="drawingCanvasRef" width="28" height="28" class="drawing-canvas"
|
||||||
|
@mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing"
|
||||||
|
@mouseleave="stopDrawing" @touchstart.prevent="handleTouchStart"
|
||||||
|
@touchmove.prevent="handleTouchMove" @touchend.prevent="stopDrawing"></canvas>
|
||||||
|
<div class="canvas-controls">
|
||||||
|
<n-button @click="clearCanvas">清除</n-button>
|
||||||
|
<n-button type="primary" @click="recognizeDigit" :disabled="!onnxModel || isProcessing"
|
||||||
|
:loading="isProcessing">
|
||||||
|
识别
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item>
|
||||||
|
<div class="mnist-demo-section">
|
||||||
|
<h3>ONNX模型</h3>
|
||||||
|
<div v-if="onnxModel" class="mnist-demo-model-info">
|
||||||
|
<p>模型: {{ onnxModel.name }}</p>
|
||||||
|
<p>大小: {{ formatFileSize(onnxModel.size) }}</p>
|
||||||
|
</div>
|
||||||
|
<n-empty v-else description="无模型" />
|
||||||
|
|
||||||
|
<div class="mnist-demo-result" v-if="modelOutput">
|
||||||
|
<h3>识别结果:</h3>
|
||||||
|
<div class="result-container">
|
||||||
|
<div class="result-digit">{{ recognizedDigit }}</div>
|
||||||
|
<!-- <n-progress type="line" :percentage="confidence" :indicator-placement="'inside'" :color="resultColor" /> -->
|
||||||
|
<div class="confidence-text">置信度: {{ (confidence).toFixed(2) }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</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 } from 'vue'
|
||||||
|
// @ts-ignore
|
||||||
|
let ort: any;
|
||||||
|
|
||||||
|
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentDirectories = defineModel<VirtualDirectory[]>(
|
||||||
|
'currentDirectories',
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 画板常量
|
||||||
|
const GRID_SIZE = 28 // MNIST 模型的输入是 28x28
|
||||||
|
// const DISPLAY_SCALE = 10 // 显示缩放比例,用于CSS缩放
|
||||||
|
|
||||||
|
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
|
||||||
|
const rootRef = ref()
|
||||||
|
const drawingCanvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
const modelOutput = ref<number[] | null>(null)
|
||||||
|
const recognizedDigit = ref<number | null>(null)
|
||||||
|
const confidence = ref<number>(0)
|
||||||
|
const resultColor = ref('#18a058')
|
||||||
|
|
||||||
|
// 绘图相关状态
|
||||||
|
const isDrawing = ref(false)
|
||||||
|
const lastPos = ref<{ x: number, y: number } | null>(null)
|
||||||
|
const gridData = ref<number[][]>(Array(GRID_SIZE).fill(0).map(() => Array(GRID_SIZE).fill(0)))
|
||||||
|
|
||||||
|
// 获取ONNX模型文件
|
||||||
|
const onnxModel = computed(() => {
|
||||||
|
for (const file of currentFiles.value) {
|
||||||
|
if (file.name.toLowerCase().endsWith('.onnx')) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取Canvas上下文
|
||||||
|
const getContext = (): CanvasRenderingContext2D | null => {
|
||||||
|
const canvas = drawingCanvasRef.value
|
||||||
|
if (!canvas) return null
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return null
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将显示坐标转换为Canvas像素坐标
|
||||||
|
const convertCoordsToCanvas = (displayX: number, displayY: number): { x: number, y: number } => {
|
||||||
|
const canvas = drawingCanvasRef.value
|
||||||
|
if (!canvas) return { x: 0, y: 0 }
|
||||||
|
|
||||||
|
// 获取Canvas的CSS尺寸
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
|
||||||
|
// 计算缩放比例
|
||||||
|
const scaleX = canvas.width / rect.width
|
||||||
|
const scaleY = canvas.height / rect.height
|
||||||
|
|
||||||
|
// 将显示坐标转换为Canvas坐标
|
||||||
|
const canvasX = (displayX - rect.left) * scaleX
|
||||||
|
const canvasY = (displayY - rect.top) * scaleY
|
||||||
|
|
||||||
|
// 返回整数坐标(因为我们是28x28的像素级别操作)
|
||||||
|
return {
|
||||||
|
x: Math.floor(canvasX),
|
||||||
|
y: Math.floor(canvasY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 画板绘图相关函数
|
||||||
|
const clearCanvas = () => {
|
||||||
|
const ctx = getContext()
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
// 清空Canvas
|
||||||
|
ctx.fillStyle = 'white'
|
||||||
|
ctx.fillRect(0, 0, GRID_SIZE, GRID_SIZE)
|
||||||
|
|
||||||
|
// 清空数据
|
||||||
|
gridData.value = Array(GRID_SIZE).fill(0).map(() => Array(GRID_SIZE).fill(0))
|
||||||
|
|
||||||
|
// 清除结果
|
||||||
|
modelOutput.value = null
|
||||||
|
recognizedDigit.value = null
|
||||||
|
confidence.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始绘制
|
||||||
|
const startDrawing = (e: MouseEvent) => {
|
||||||
|
isDrawing.value = true
|
||||||
|
|
||||||
|
const { x, y } = convertCoordsToCanvas(e.clientX, e.clientY)
|
||||||
|
lastPos.value = { x, y }
|
||||||
|
drawPixel(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制
|
||||||
|
const draw = (e: MouseEvent) => {
|
||||||
|
if (!isDrawing.value || !lastPos.value) return
|
||||||
|
|
||||||
|
const { x, y } = convertCoordsToCanvas(e.clientX, e.clientY)
|
||||||
|
if (x === lastPos.value.x && y === lastPos.value.y) return // 避免在同一像素重复绘制
|
||||||
|
|
||||||
|
// 绘制当前像素
|
||||||
|
drawPixel(x, y)
|
||||||
|
|
||||||
|
// 绘制连续线条(Bresenham算法)
|
||||||
|
const { x: lastX, y: lastY } = lastPos.value
|
||||||
|
drawLine(lastX, lastY, x, y)
|
||||||
|
|
||||||
|
// 更新最后位置
|
||||||
|
lastPos.value = { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止绘制
|
||||||
|
const stopDrawing = () => {
|
||||||
|
isDrawing.value = false
|
||||||
|
lastPos.value = null
|
||||||
|
// 更新到gridData(因为我们在绘制时已经更新了,所以这里不需要额外操作)
|
||||||
|
|
||||||
|
// 如果有ONNX模型且当前没有在推理中,则自动进行推理
|
||||||
|
// if (onnxModel.value && !isProcessing.value) {
|
||||||
|
// recognizeDigit()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制单个像素
|
||||||
|
const drawPixel = (x: number, y: number) => {
|
||||||
|
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return
|
||||||
|
|
||||||
|
const ctx = getContext()
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
// 绘制到Canvas
|
||||||
|
ctx.fillStyle = 'black'
|
||||||
|
ctx.fillRect(x, y, 1, 1)
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
gridData.value[y][x] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Bresenham算法绘制线条
|
||||||
|
const drawLine = (x0: number, y0: number, x1: number, y1: number) => {
|
||||||
|
const dx = Math.abs(x1 - x0)
|
||||||
|
const dy = Math.abs(y1 - y0)
|
||||||
|
const sx = x0 < x1 ? 1 : -1
|
||||||
|
const sy = y0 < y1 ? 1 : -1
|
||||||
|
let err = dx - dy
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// 如果是终点,结束循环
|
||||||
|
if (x0 === x1 && y0 === y1) break
|
||||||
|
|
||||||
|
const e2 = 2 * err
|
||||||
|
if (e2 > -dy) {
|
||||||
|
err -= dy
|
||||||
|
x0 += sx
|
||||||
|
}
|
||||||
|
if (e2 < dx) {
|
||||||
|
err += dx
|
||||||
|
y0 += sy
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPixel(x0, y0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理触摸事件
|
||||||
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
|
if (e.touches.length !== 1) return
|
||||||
|
|
||||||
|
const touch = e.touches[0]
|
||||||
|
const { x, y } = convertCoordsToCanvas(touch.clientX, touch.clientY)
|
||||||
|
|
||||||
|
isDrawing.value = true
|
||||||
|
lastPos.value = { x, y }
|
||||||
|
drawPixel(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTouchMove = (e: TouchEvent) => {
|
||||||
|
if (!isDrawing.value || !lastPos.value) return
|
||||||
|
if (e.touches.length !== 1) return
|
||||||
|
|
||||||
|
const touch = e.touches[0]
|
||||||
|
const { x, y } = convertCoordsToCanvas(touch.clientX, touch.clientY)
|
||||||
|
|
||||||
|
if (x === lastPos.value.x && y === lastPos.value.y) return // 避免在同一像素重复绘制
|
||||||
|
|
||||||
|
// 绘制当前像素
|
||||||
|
drawPixel(x, y)
|
||||||
|
|
||||||
|
// 绘制连续线条
|
||||||
|
const { x: lastX, y: lastY } = lastPos.value
|
||||||
|
drawLine(lastX, lastY, x, y)
|
||||||
|
|
||||||
|
// 更新最后位置
|
||||||
|
lastPos.value = { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从Canvas更新gridData(在本实现中,我们在绘制时已经同步更新了gridData,这个函数保留用于检查)
|
||||||
|
const updateGridDataFromCanvas = () => {
|
||||||
|
const ctx = getContext()
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
// 从Canvas读取像素数据
|
||||||
|
const imageData = ctx.getImageData(0, 0, GRID_SIZE, GRID_SIZE)
|
||||||
|
const data = imageData.data
|
||||||
|
|
||||||
|
// 更新gridData
|
||||||
|
for (let y = 0; y < GRID_SIZE; y++) {
|
||||||
|
for (let x = 0; x < GRID_SIZE; x++) {
|
||||||
|
const pixelIndex = (y * GRID_SIZE + x) * 4
|
||||||
|
const r = data[pixelIndex]
|
||||||
|
const g = data[pixelIndex + 1]
|
||||||
|
const b = data[pixelIndex + 2]
|
||||||
|
|
||||||
|
// 简单阈值处理,非白即黑
|
||||||
|
gridData.value[y][x] = (r + g + b < 600) ? 1 : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件拖放
|
||||||
|
const handleDrop = (vFiles: VirtualFile[]) => {
|
||||||
|
for (const file of vFiles) {
|
||||||
|
if (file.name.toLowerCase().endsWith('.onnx')) {
|
||||||
|
currentFiles.value.unshift(file)
|
||||||
|
miaoMessageRef.value?.alertTip('已添加ONNX模型文件', { type: 'success', timeout: 2000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
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`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将grid数据预处理为MNIST输入格式
|
||||||
|
const preprocessGridData = (): Float32Array => {
|
||||||
|
// 转换为MNIST模型期望的格式 (1x1x28x28)
|
||||||
|
const inputData = new Float32Array(GRID_SIZE * GRID_SIZE)
|
||||||
|
|
||||||
|
for (let row = 0; row < GRID_SIZE; row++) {
|
||||||
|
for (let col = 0; col < GRID_SIZE; col++) {
|
||||||
|
// 计算一维索引
|
||||||
|
const index = row * GRID_SIZE + col
|
||||||
|
// 存储像素值(MNIST期望格式为:黑色笔画对应较高的值,白色背景对应较低的值)
|
||||||
|
inputData[index] = gridData.value[row][col]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 识别手写数字
|
||||||
|
const recognizeDigit = async () => {
|
||||||
|
if (!onnxModel.value) {
|
||||||
|
miaoMessageRef.value?.alertTip('请先选择ONNX模型文件', { type: 'error', timeout: 2000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在识别前检查并更新gridData
|
||||||
|
updateGridDataFromCanvas()
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
modelOutput.value = null
|
||||||
|
recognizedDigit.value = null
|
||||||
|
confidence.value = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 显示处理信息
|
||||||
|
const alertUpdate = miaoMessageRef.value!.alertTip('正在加载ONNX模型...', { type: 'info', timeout: 2000 })
|
||||||
|
|
||||||
|
// 1. 加载模型
|
||||||
|
const modelResponse = await fetch(onnxModel.value.url)
|
||||||
|
const modelBuffer = await modelResponse.arrayBuffer()
|
||||||
|
alertUpdate('正在创建推理会话...')
|
||||||
|
|
||||||
|
// 创建ONNX会话
|
||||||
|
const session = await ort.InferenceSession.create(modelBuffer)
|
||||||
|
|
||||||
|
// 2. 预处理画布数据
|
||||||
|
alertUpdate('正在预处理图像...')
|
||||||
|
const inputData = preprocessGridData()
|
||||||
|
|
||||||
|
// 创建输入张量
|
||||||
|
// MNIST模型通常期望形状为[1, 1, 28, 28]的输入
|
||||||
|
const inputTensor = new ort.Tensor('float32', inputData, [1, 1, 28, 28])
|
||||||
|
|
||||||
|
alertUpdate('执行模型推理...')
|
||||||
|
|
||||||
|
// 3. 运行模型推理 - 使用"Input3"作为输入名称
|
||||||
|
const outputs = await session.run({
|
||||||
|
// 使用正确的输入名称
|
||||||
|
Input3: inputTensor
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 处理模型输出
|
||||||
|
alertUpdate('处理模型输出结果...')
|
||||||
|
|
||||||
|
// 获取输出数据
|
||||||
|
const outputTensor = Object.values(outputs)[0]
|
||||||
|
const outputData = [...(outputTensor as { data: Float32Array }).data]
|
||||||
|
|
||||||
|
// 设置结果
|
||||||
|
modelOutput.value = outputData
|
||||||
|
|
||||||
|
// 找出最可能的数字(最大概率对应的索引)
|
||||||
|
let maxIndex = 0
|
||||||
|
let maxProb = outputData[0]
|
||||||
|
|
||||||
|
for (let i = 1; i < outputData.length; i++) {
|
||||||
|
if (outputData[i] > maxProb) {
|
||||||
|
maxProb = outputData[i]
|
||||||
|
maxIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recognizedDigit.value = maxIndex
|
||||||
|
|
||||||
|
// 计算置信度百分比
|
||||||
|
const softmaxSum = outputData.reduce((sum, val) => sum + Math.exp(val), 0)
|
||||||
|
const probability = Math.exp(maxProb) / softmaxSum
|
||||||
|
confidence.value = probability * 100
|
||||||
|
|
||||||
|
// 设置结果颜色
|
||||||
|
if (confidence.value > 90) {
|
||||||
|
resultColor.value = '#18a058' // 绿色
|
||||||
|
} else if (confidence.value > 70) {
|
||||||
|
resultColor.value = '#2080f0' // 蓝色
|
||||||
|
} else if (confidence.value > 50) {
|
||||||
|
resultColor.value = '#f0a020' // 橙色
|
||||||
|
} else {
|
||||||
|
resultColor.value = '#d03050' // 红色
|
||||||
|
}
|
||||||
|
|
||||||
|
alertUpdate('识别完成!', { type: 'success' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ONNX处理错误:', error)
|
||||||
|
miaoMessageRef.value?.alertTip(`处理出错: ${error}`, { type: 'error', timeout: 2000 })
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 如果传入了onnxruntime-web,则使用传入的,否则从jsdelivr加载
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
// 从jsdelivr加载
|
||||||
|
// @ts-ignore
|
||||||
|
const ortModule = await import(/* @vite-ignore */ 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.all.min.js');
|
||||||
|
ort = ortModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化绘图区域
|
||||||
|
if (drawingCanvasRef.value) {
|
||||||
|
initCanvas();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加初始化画布函数
|
||||||
|
const initCanvas = () => {
|
||||||
|
// 初始化Canvas
|
||||||
|
const ctx = getContext()
|
||||||
|
if (ctx) {
|
||||||
|
// 设置白色背景
|
||||||
|
ctx.fillStyle = 'white'
|
||||||
|
ctx.fillRect(0, 0, GRID_SIZE, GRID_SIZE)
|
||||||
|
|
||||||
|
// 设置抗锯齿(关闭,保证像素对齐)
|
||||||
|
ctx.imageSmoothingEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有ONNX模型
|
||||||
|
if (onnxModel.value) {
|
||||||
|
miaoMessageRef.value?.alertTip('已检测到ONNX模型文件,您可以绘制数字进行识别', { type: 'info', timeout: 2000 })
|
||||||
|
} else {
|
||||||
|
miaoMessageRef.value?.alertTip('请添加MNIST模型(.onnx)文件', { type: 'info', timeout: 2000 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mnist-demo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnist-demo-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnist-demo-section {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 320px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawing-canvas {
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
touch-action: none;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
/* 像素化渲染 */
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
/* 确保边缘清晰 */
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnist-demo-model-info {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mnist-demo-result {
|
||||||
|
margin-top: 16px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-digit {
|
||||||
|
font-size: 64px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confidence-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import VirtualDirectory, { Tree, VirtualFile } from '@/class/VirtualDirectory'
|
import { Tree } from '@/class/VirtualDirectory'
|
||||||
|
import { VirtualRemoteDirectory, VirtualRemoteFile } from '@/class/VirtualRemoteDirectory'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过VirtualDirectory的.tree重建文件夹树
|
* 通过VirtualDirectory的.tree重建文件夹树
|
||||||
@@ -6,8 +7,8 @@ import VirtualDirectory, { Tree, VirtualFile } from '@/class/VirtualDirectory'
|
|||||||
* @returns {any}
|
* @returns {any}
|
||||||
*/
|
*/
|
||||||
const buildFromTree = (data: Tree) => {
|
const buildFromTree = (data: Tree) => {
|
||||||
const root = new VirtualDirectory(data)
|
const root = new VirtualRemoteDirectory(data)
|
||||||
root.files = data.files?.map((v) => new VirtualFile(v, root))
|
root.files = data.files?.map((v) => new VirtualRemoteFile(v, root))
|
||||||
root.directories = data.directories?.map((v) => buildFromTree(v))
|
root.directories = data.directories?.map((v) => buildFromTree(v))
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user