110 Commits
Author SHA1 Message Date
539943419 9ee472e5de ci: 重构构建流程以使用output目录存储产物
修改构建脚本和CI配置,将所有构建产物统一输出到output目录
更新.gitignore添加output目录忽略
调整CI工作流中的文件路径匹配规则
2025-08-23 01:47:42 +08:00
539943419 f341159f1a ci(workflow): 更新 GitHub Actions 发布配置和构建脚本
更新 action-gh-release 从 v1 到 v2
优化构建脚本,增加错误处理和命令检查
使用更安全的脚本编写方式,避免全局变量污染
改进跨平台构建的兼容性和可维护性
2025-08-22 01:54:01 +08:00
539943419 97276224ee feat(plugins): 添加ONNX模型演示插件并优化组件加载逻辑
refactor(component2plugin): 改进组件加载逻辑避免重复加载
style(components.d.ts): 清理未使用的组件类型声明
chore(package.json): 添加build:normal脚本
2025-08-22 01:20:48 +08:00
539943419 b8bea8e3bd update: 文件管理器导航栏长文件名缩略处理 2025-04-09 16:16:49 +08:00
539943419 d3e19d4855 feat: 后端添加键值对存储支持。前端添加一个相应的插件 2025-04-01 20:19:32 +08:00
539943419 6f42c634ff fix: 修正复制链接插件和复制插件冲突的问题 2025-03-31 22:42:54 +08:00
539943419 5d0abf5ac9 feat: 添加FPS插件,显示帧率 2025-03-31 17:13:51 +08:00
539943419 938269bd9f feat: 优化文件管理器插件文件展示排序 2025-03-27 22:28:21 +08:00
539943419 cb8198d1bb feat: 添加复制相关逻辑,修复文件夹剪切无法正常执行的错误 2025-03-24 02:11:20 +08:00
539943419 089dc68419 feat: 添加更多构建版本 2025-03-23 12:46:16 +08:00
539943419 eaef1895d3 fix: 修复构建arm架构版本失败的问题 2025-03-23 12:21:55 +08:00
539943419 e6874d967a feat: 优化构建流程 2025-03-23 11:54:52 +08:00
539943419 4a128e1aa1 fix: 修复Github Action构建linux版本时出现的问题 2025-03-23 03:40:52 +08:00
539943419 444eeaaa19 fix: 应该是最后一次修bug了 2025-03-23 03:24:58 +08:00
539943419 d4c1392b44 fix: 继续修复 2025-03-23 03:19:30 +08:00
539943419 fd928c9aad fix: 继续修复github action的问题。 2025-03-23 03:14:36 +08:00
539943419 bc61e92507 fix: 修改自动构建时发生的类型错误问题。 2025-03-23 02:40:26 +08:00
539943419 37f8a26132 feat: 优化构建流程,实现自动构建 2025-03-23 02:37:41 +08:00
539943419 62eba84702 fix: 修复构建时upx设置问题 2025-03-22 16:07:16 +08:00
539943419 3c3ce44c45 feat: 优化分享功能的展示效果 2025-03-22 16:02:42 +08:00
539943419 f9b93624e4 feat: 添加基于webview实现的包含GUI的版本,添加通过upx进行压缩的版本,优化分享功能。 2025-03-22 15:47:35 +08:00
539943419 7f26b46cde fix: 修复一个由naive-ui与tailwindcss冲突导致的样式问题 2025-03-22 14:56:10 +08:00
539943419 802bbc182d feat: update 2025-03-22 14:28:49 +08:00
539943419 01085a3ba5 update 2025-03-21 11:55:33 +08:00
539943419 286dd8e386 feat: 重构VirtualFile和VirtualDirectory的实现 2025-03-17 20:56:44 +08:00
539943419 42479f2265 feat: 优化拖动组件逻辑,加入tailwind支持,更新readme 2025-03-17 20:23:33 +08:00
539943419 34d7c7df30 feat: 添加一个无关痛痒的浏览器llm的支持,添加自动导入, 2025-03-12 15:03:19 +08:00
539943419 81360c377c fix: 修复服务端请求head返回404的问题 2025-02-11 12:00:45 +08:00
539943419 081b724513 fix: 再次再次修复因为margin导致文件夹目录滑动卡顿的现象 2024-12-25 23:22:33 +08:00
539943419 3b8c0398c1 feat: 更新打包脚本 2024-12-25 22:57:24 +08:00
539943419 d19cce76d2 feat: 为文件夹插件导航栏添加滚动条以及鼠标滚轮相关事件处理 2024-12-25 22:42:12 +08:00
539943419 8ef243f790 feat: 拆分App.vue 2024-12-25 21:26:22 +08:00
539943419 8605bd19ea feat: 修改代码风格 2024-12-25 19:56:25 +08:00
539943419 c6999647c3 feat: 优化项目代码结构 2024-12-25 19:47:52 +08:00
539943419 f33e9386cd feat: 添加prettier格式化代码 2024-12-25 19:44:18 +08:00
539943419 5823d5876b style: 音乐插件样式调整 2024-12-25 19:11:39 +08:00
539943419 011a3356b7 fix: 代码优化 2024-12-24 14:30:21 +08:00
539943419 4aba71d451 feat: 使App.vue和VirtualDirectory解耦 2024-12-24 14:19:08 +08:00
539943419 6d550505a0 feat: 优化部分代码实现,添加导入外部插件功能 2024-12-24 13:26:51 +08:00
539943419 d6816925ed feat: 合并服务端程序代码至主分支 2024-12-19 22:32:27 +08:00
539943419 47590ec0db fix: 修复视频插件无法播放mkv封装格式的视频的问题 2024-12-19 22:28:01 +08:00
539943419 8f8d098faa feat: 优化插件导入逻辑,视频插件在移动端进入锁定状态时隐藏上下梯度渐变以防止遮挡视频 2024-12-16 20:00:40 +08:00
539943419 bfc841fac2 fix 2024-11-22 13:40:29 +08:00
539943419 e8dd9ccf11 style: 加点特效(未完成) 2024-10-08 01:23:26 +08:00
539943419 d89978a44f feat: 一个妥协的拖动方案,暂时用用得了 2024-10-01 03:07:43 +08:00
539943419 5a843bf24a feat: 尝试适配移动端,虽然目前尚未实现。 2024-10-01 02:41:42 +08:00
539943419 e876c117c6 fix: 修复歌单顺序错误的问题 2024-10-01 00:15:11 +08:00
539943419 483a573f06 feat: 音乐插件保存歌单添加防抖。修复音乐插件全屏显示异常的问题。 2024-09-30 23:38:00 +08:00
539943419 90a068939d feat: 添加主页插件支持 2024-09-30 23:21:05 +08:00
539943419 1f42785b4a feat: 部分完成音乐播放器 2024-09-30 17:07:11 +08:00
539943419 3b09d4c1cb feat: 引入rollup-plugin-visualizer 2024-09-30 09:58:33 +08:00
539943419 948b5f23d1 fix: 修复歌单显示不全的问题 2024-09-30 09:39:32 +08:00
539943419 4722a2d157 feat: 依然没有解决移动端视频播放问题。明天再解决吧 2024-09-30 00:03:19 +08:00
539943419 65c17a8ce3 feat: 视频插件移动端优化 2024-09-29 20:27:42 +08:00
539943419 cba20150be style: ... 2024-09-29 17:57:20 +08:00
539943419 7b52a7a62e fix 2024-09-29 17:16:45 +08:00
539943419 2e75a35d10 feat: 歌单加了个顶部栏 2024-09-29 17:14:02 +08:00
539943419 ae476413df fix: 调整目录结构 2024-09-29 13:58:02 +08:00
539943419 dcdb3a1ab8 feat: 修改拖动有效判定条件。歌单添加播放mv按钮。歌单仅为1首时也能唤起。 2024-09-29 13:26:34 +08:00
539943419 baec1856c9 fix: 歌单排序拖动hover样式修复 2024-09-29 12:18:18 +08:00
539943419 a03623a0a0 fix 2024-09-29 11:53:34 +08:00
539943419 aec7fcabc9 feat: 也不知道改了啥 2024-09-29 11:50:39 +08:00
539943419 9611e9f4ed fix: 1.取消歌单懒加载 2.修改vite打包时chunk文件命名规则 2024-09-29 11:00:35 +08:00
539943419 53ab0ad45b feat: update... 2024-09-29 01:29:26 +08:00
539943419 641700d730 feat: 优化lodash导入 2024-09-27 22:07:38 +08:00
539943419 aa61489517 feat: 修改部分插件优先级,修改分包规则 2024-09-27 21:28:07 +08:00
539943419 171af1219e feat: 歌单内元素懒加载 2024-09-27 18:23:40 +08:00
539943419 cdc0bce108 feat: 优化体验 2024-09-27 17:15:40 +08:00
539943419 c6b20b5b64 feat: 音乐插件拖动添加动画效果 2024-09-27 16:58:26 +08:00
539943419 37791ab04e feat: 添加插件优先级 2024-09-27 16:34:06 +08:00
539943419 e155524e3e style: 样式修改 2024-09-27 16:08:34 +08:00
539943419 dafe494ce9 style: 样式调整 2024-09-27 15:51:52 +08:00
539943419 55381df4f6 feat: 歌单支持拖动排序 2024-09-27 15:03:43 +08:00
539943419 38e1b098d2 feat: 1. 更新音乐插件 2. 组件注册插件增加单插件模式 2024-09-27 14:04:41 +08:00
539943419 4b5b7956c7 feat: 用aplayer随便做了一个音乐插件 2024-09-27 00:42:15 +08:00
539943419 5173b28ec5 feat: 优化视频插件 2024-09-26 20:38:57 +08:00
539943419 8d738ef3f9 fix: 修正视频插件样式全屏不生效的问题 2024-09-26 17:57:14 +08:00
539943419 96ca8fcf4c Merge branch 'main' of http://git.miaospring.top:3000/539943419/miaoDirectory 2024-09-26 17:02:41 +08:00
539943419 54c48c0f8f feat: 为xgplayer增加一个类似于bilibili的锁定的插件 2024-09-26 17:02:28 +08:00
539943419 f2ff05f4e3 feat: 初步实现基于iframe的插件的部分逻辑。 2024-09-12 03:31:20 +08:00
539943419 ff7d0d8d51 feat: 添加一个复制文件链接到剪切板的插件 2024-09-08 05:57:13 +08:00
539943419 0e631a6f34 feat: miaoDirectory中选项栏增加多选相关操作 2024-09-05 12:46:09 +08:00
539943419 0ab58b24d9 fix: 修正了图片浏览插件高度异常的问题 2024-09-05 00:28:58 +08:00
539943419 b0cf5b312a feat: 插件加载提示只在第一次加载时弹出(后面插件会缓存下来) 2024-09-05 00:12:35 +08:00
539943419 ec4ed10afd feat: 1.插件加载时会有全局提示。2.重命名miaoMessageProvider为miaoAlertTipProvider 2024-09-05 00:10:36 +08:00
539943419 3a18bb3cbd feat: 1.现在miaoFetchApi.getFile始终返回Promise<uint8array>. 2.增加文件加/解密插件,样式还没写,加密功能是挺正常的。3.加了个插件模板 2024-09-04 17:30:22 +08:00
539943419 282bf327bb feat: 插件注册增加disable和exitConfirm 2024-09-04 14:19:11 +08:00
539943419 4d2bd3867f feat: 重新组织代码结构 2024-09-04 11:47:14 +08:00
539943419 afb2440d8b feat: 增加测试导入插件(已禁用), 更新vue版本 2024-09-04 10:38:19 +08:00
539943419 8b55ac2854 feat: 使用vue3.5稳定版。增加一个未完成的导入插件的插件。 2024-09-04 00:02:24 +08:00
539943419 f0748997be fix: .. 2024-09-03 09:44:53 +08:00
539943419 b2c5c7cc6d feat: 1.升级vue版本,2.修正tabs在剩余2个时动画不正确的问题 2024-09-03 09:27:51 +08:00
539943419 d16f11c263 feat: codemirror插件的请求禁止使用缓存 2024-09-02 23:13:27 +08:00
539943419 0f6e817966 feat: 1.优化images样式表现。2.增加markdown浏览插件 2024-09-02 21:04:07 +08:00
539943419 6b8f7aa6b4 feat: 1.给插件新增一些属性。2.重构图片浏览组件,根据时间分组浏览 2024-09-02 17:59:49 +08:00
539943419 78dac7482c update readme 2024-09-02 00:56:05 +08:00
539943419 9aec221ea3 feat: 重构部分组件,使miaoDirectoryItem更加通用 2024-09-01 14:45:29 +08:00
539943419 938dde4c1c feat: 屏蔽miaoDirectory底部工具栏,更新readme计划书 2024-09-01 04:18:41 +08:00
539943419 8e884eb178 feat: 优化代码表现 2024-09-01 03:39:50 +08:00
539943419 8c65e66ab6 style: 微调样式 2024-08-31 17:30:21 +08:00
539943419 a34e27cc65 feat: 添加miaoMessageProvider 2024-08-31 17:19:19 +08:00
539943419 de2e53cbcf fix: 1.修复了图片插件可用性判断的问题。2.修复了code插件样式错误的问题 2024-08-31 14:18:39 +08:00
539943419 b3c6dad1b1 fix: 修复了miaoDirectory在特殊情况下显示异常的问题。 2024-08-31 00:12:56 +08:00
539943419 fcb2fe8f7f fix: 1.修复了切换文件夹后无法自动滚动到顶部的问题。2.修复了图标路径错误的问题。 2024-08-30 23:56:20 +08:00
539943419 c02f6743e3 feat: 1.拆分miaoDirectory,右上角退出按钮改为小菜单,增加搜索,屏蔽文件夹等功能。2.更新readme 2024-08-30 23:29:28 +08:00
539943419 3e24d4a363 fix: 修正miaoDirectoryItem部分图片文件图标显示错误问题 2024-08-30 21:25:40 +08:00
539943419 7574acdad2 feat: 1.优化了miaoDirectory中多选的性能表现。2.图片浏览组件新增拖入虚拟文件/文件夹相关处理 2024-08-30 20:58:18 +08:00
539943419 704c80e019 feat: 1.通过懒加载大大优化miaoDirectory的使用体验。2.现在切换文件夹时会自动清空多选文件列表。 2024-08-30 16:41:04 +08:00
539943419 210e251b23 feat: 添加一个没啥功能的图片浏览插件 2024-08-30 05:15:32 +08:00
539943419 93ca2a29a6 fix: 修正大量拼写错误 2024-08-30 03:12:18 +08:00
154 changed files with 16439 additions and 5505 deletions

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
name: 构建并发布应用
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
name: 构建和发布
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 安装pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: 安装前端依赖
run: pnpm install
- name: 显示依赖包信息
run: |
ls -la node_modules/aplayer
ls -la node_modules/APlayer || echo "大写的APlayer不存在"
- name: 构建前端
run: pnpm run build
- name: 设置Go环境
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: 安装UPX
run: sudo apt-get update && sudo apt-get install -y upx
- name: 构建应用
run: bash build.auto.release.sh -full
- name: 显示构建产物
run: |
ls -la output || true
find output -type f -name "miao-directory*" || true
- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: miao-directory-all
path: |
output/**
- name: 生成版本号
id: version
run: |
echo "date=$(date +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: 创建自动发布
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
uses: softprops/action-gh-release@v2
with:
name: Auto-Build-${{ steps.version.outputs.date }}-${{ steps.version.outputs.sha_short }}
tag_name: auto-${{ steps.version.outputs.date }}-${{ steps.version.outputs.sha_short }}
draft: false
prerelease: false
files: |
output/**
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+56 -2
View File
@@ -9,6 +9,7 @@ lerna-debug.log*
node_modules node_modules
dist dist
output/
dist-ssr dist-ssr
*.local *.local
@@ -23,7 +24,60 @@ dist-ssr
*.sln *.sln
*.sw? *.sw?
.vscode .vscode
serve
miao-directory.exe miao-directory.exe
miao-directory-amd64-linux miao-directory-amd64-linux
miao-directory-amd64-win.exe miao-directory-amd64-win.exe
miao-directory-amd64-win.gui.exe
miao-directory-amd64-win.upx.exe
miao-directory-amd64-win.gui.upx.exe
miao-directory.exe~
miao-directory-amd64-linux~
miao-directory-amd64-win.exe~
miao-directory-amd64-win.gui.exe~
miao-directory-amd64-win.upx.exe~
miao-directory-amd64-win.gui.upx.exe~
# Linux 压缩版本
miao-directory-amd64-linux.upx
miao-directory-amd64-linux.upx~
# Linux ARM64 版本
miao-directory-arm64-linux
miao-directory-arm64-linux~
miao-directory-arm64-linux.upx
miao-directory-arm64-linux.upx~
# macOS AMD64 版本
miao-directory-amd64-darwin
miao-directory-amd64-darwin~
miao-directory-amd64-darwin.upx
miao-directory-amd64-darwin.upx~
# macOS ARM64 版本
miao-directory-arm64-darwin
miao-directory-arm64-darwin~
miao-directory-arm64-darwin.upx
miao-directory-arm64-darwin.upx~
# Windows ARM64 版本
miao-directory-arm64-win.exe
miao-directory-arm64-win.exe~
# 带x-前缀的版本
miao-directory-x-*
test
stats.html
src-server/web/*
# auto-imports.d.ts
# components.d.ts
# src/plugins/onnx_demo_*
public/test
doc
+104
View File
@@ -0,0 +1,104 @@
export default {
/**
* 换行宽度,当代码宽度达到多少时换行
* @default 80
* @type {number}
*/
printWidth: 80,
/**
* 缩进的空格数量
* @default 2
* @type {number}
*/
tabWidth: 3,
/**
* 是否使用制表符代替空格
* @default false
* @type {boolean}
*/
useTabs: false,
/**
* 是否在代码块结尾加上分号
* @default true
* @type {boolean}
*/
semi: false,
/**
* 是否使用单引号替代双引号
* @default false
* @type {boolean}
*/
singleQuote: true,
/**
* 对象属性的引号处理
* @default "as-needed"
* @type {"as-needed"|"consistent"|"preserve"}
*/
quoteProps: 'as-needed',
/**
* jsx中是否使用单引号替代双引号
* @default false
* @type {boolean}
*/
jsxSingleQuote: true,
/**
* 在jsx中使用是否单引号代替双引号
* @default false
* @type {boolean}
*/
/**
* 末尾是否加上逗号
* @default "es5"
* @type {"es5"|"none"|"all"}
*/
trailingComma: 'none',
/**
* 在对象,数组括号与文字之间加空格 "{ foo: bar }"
* @default true
* @type {boolean}
*/
bracketSpacing: true,
/**
* 把多行HTML (HTML, JSX, Vue, Angular)元素的>放在最后一行的末尾,而不是单独放在下一行(不适用于自关闭元素)。
* @default false
* @type {boolean}
*/
bracketSameLine: true,
/**
* 当箭头函数只有一个参数是否加括号
* @default "always"
* @type {"always"|"avoid"}
*/
arrowParens: 'always',
/**
* 为HTML、Vue、Angular和Handlebars指定全局空格敏感性
* @default "css"
* @type {"css"|"strict"|"ignore"}
*/
htmlWhitespaceSensitivity: 'ignore',
/**
* 是否缩进Vue文件中的<script>和<style>标记内的代码。有些人(比如Vue的创建者)不使用缩进来保存缩进级别,但这可能会破坏编辑器中的代码折叠。
* @default "always"
* @type {"always"|"avoid"}
*/
vueIndentScriptAndStyle: false,
/**
* 文件结束符
* @default "lf"
* @type {"lf"|"crlf"|"cr"|"auto"}
*/
endOfLine: 'crlf',
/**
* 因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
*/
proseWrap: 'never',
// 是否使用根目录下的EditorConfig配置文件
useEditorConfig: false,
/**
* HTML\VUE\JSX每行只有单个属性
* @default true
* @type {boolean}
*/
// singleAttributePerLine: true,
// disableLanguages: ['html']
}
+28 -11
View File
@@ -1,11 +1,28 @@
todo: 标签页,miaoDirectory切换动画 [finish] ## TODO list:
todo: 重构virtualPage,所有page以插件形式使用 [finish]
todo: miaoDirectory插件化,为后续插件功能开发做准备 [finish] | 计划 | 状态 |
todo: miaoPopupInput, 弹出一个输入框 [finish] |---|---|
todo: miaoDirectory的重命名功能 [finish] | 拆分miaoDirectory | ✅ |
todo: miaoDirectory的剪切功能 [finish] | 完善视频插件 | ✅ |
todo: miaoDirectory的上传按钮 [finish] | 完善PDF插件 | ✅ |
todo: miaoDirectory的新建文件/文件夹功能 [finish] | 完善Code插件 | ✅ |
todo: miaoDirectory的多选功能 [finish] | 实现miaoDirectory的搜索功能 | ✅ |
todo: VirtualDirectory更新时更新文件信息 | 创建音乐播放器组件(仿QQ音乐移动端吧) | ✅ |
todo: miaoDirectory的复制文件/文件夹功能 | plugin增加些属性,icon, disable, group之类的 | ✅ |
| 实现miaoDirectory的排序功能 | ✅ |
| 重构主界面,拆分主界面组件的实现 | ✅ |
| 重构miaoFetchApi.getFile, 增加进度 | ❌ |
| 完善miaoDirectory的搜索功能 | ❌ |
| 完善图片浏览插件 | ❌ |
| VirtualDirectory更新时更新文件信息 | ❌ |
| miaoDirectory的复制文件/文件夹功能 | ❌ |
| 重构miaoDropHandler的实现 | ❌ |
| 长期计划 |
|---|
| 无 |
## 碎碎念:
写的真潦草
+75
View File
@@ -0,0 +1,75 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useDialog: typeof import('naive-ui')['useDialog']
const useId: typeof import('vue')['useId']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useModel: typeof import('vue')['useModel']
const useNotification: typeof import('naive-ui')['useNotification']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+258
View File
@@ -0,0 +1,258 @@
#!/bin/bash
set -Eeuo pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUT_DIR="$SCRIPT_DIR/output"
# 解析命令行参数
FULL_BUILD=false
FAST_COMPRESSION=false
for arg in "$@"; do
case $arg in
-full)
FULL_BUILD=true
;;
-fast)
FAST_COMPRESSION=true
;;
esac
done
# 检查UPX是否存在
check_upx() {
if command -v upx &> /dev/null; then
echo "UPX已找到,将执行可执行文件压缩"
return 0
else
echo "UPX未找到,跳过压缩步骤"
return 1
fi
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "$1 未找到,请安装后再试"
exit 1
fi
}
# 清理已有构建产物
echo "清理已有构建产物..."
mkdir -p "$OUT_DIR"
# 基础前缀
PREFIX="miao-directory"
# 基础构建产物
BASE_ARCHOS=(
"amd64-win.exe"
"amd64-win.upx.exe"
"amd64-win.gui.exe"
"amd64-win.gui.upx.exe"
"amd64-linux"
"amd64-linux.upx"
)
# 清理基础构建产物
for suffix in "${BASE_ARCHOS[@]}"; do
[ -f "$OUT_DIR/$PREFIX-$suffix" ] && rm "$OUT_DIR/$PREFIX-$suffix"
done
# 如果是完整构建,清理额外架构产物
if [ "$FULL_BUILD" = true ]; then
echo "完整构建模式,清理额外架构产物..."
# 无需添加x-前缀的特殊版本
SPECIAL_ARCHOS=(
"arm64-linux"
"arm64-linux.upx"
"amd64-darwin"
"amd64-darwin.upx"
"arm64-darwin"
"arm64-darwin.upx"
"arm64-win.exe"
)
# 清理特殊版本产物
for suffix in "${SPECIAL_ARCHOS[@]}"; do
[ -f "$OUT_DIR/$PREFIX-$suffix" ] && rm "$OUT_DIR/$PREFIX-$suffix"
done
# 处理带x-前缀的不同系统和架构
X_OS_ARCHS=(
# FreeBSD系列
"freebsd:amd64,arm64,386"
# OpenBSD系列
"openbsd:amd64,arm64,386"
# Windows额外架构
"windows:arm64"
# Linux额外架构
"linux:386,mips,mips64,mips64le,ppc64,ppc64le,riscv64,s390x"
)
# 清理带x-前缀的版本产物
for os_arch in "${X_OS_ARCHS[@]}"; do
# 分离操作系统和架构列表
IFS=":" read -r os archs <<< "$os_arch"
# 处理每个架构
IFS="," read -ra arch_array <<< "$archs"
for arch in "${arch_array[@]}"; do
file="$PREFIX-x-$arch-$os"
[ -f "$OUT_DIR/$file" ] && rm "$OUT_DIR/$file"
done
done
fi
echo "构建前端..."
require_cmd pnpm
require_cmd go
pnpm run build
echo "复制前端文件到服务端..."
rm -rf ./src-server/web/*
mkdir -p ./src-server/web/
if compgen -G "./dist/*" > /dev/null; then
cp -r ./dist/* ./src-server/web/
fi
cd "$SCRIPT_DIR/src-server"
# ===== 构建阶段 =====
echo "===== 开始构建阶段 ====="
# 基础构建 - 始终构建的版本
echo "构建基础版本..."
# Windows AMD64 无GUI
echo "构建 Win AMD64 无GUI 版本"
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o "$OUT_DIR/miao-directory-amd64-win.exe"
# Linux AMD64 无GUI
echo "构建 Linux AMD64 无GUI 版本"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o "$OUT_DIR/miao-directory-amd64-linux"
chmod +x "$OUT_DIR/miao-directory-amd64-linux"
# Windows AMD64 GUI
echo "构建 Win AMD64 GUI 版本"
GOOS=windows GOARCH=amd64 go build -tags webview -ldflags="-s -w -H=windowsgui" -o "$OUT_DIR/miao-directory-amd64-win.gui.exe"
# 完整构建 - 额外的系统和架构
if [ "$FULL_BUILD" = true ]; then
echo "构建额外架构版本..."
# 备份resource.syso文件(如果存在)
if [ -f "resource.syso" ]; then
echo "备份resource.syso文件"
mv resource.syso resource.syso.bak
fi
: # CGO 在每次构建调用中禁用,无需全局 export
# 定义操作系统和架构映射
# 在工具函数处增加大小写格式化,避免使用 Bash 4 的 ${var^}/${var^^}
uc_first() {
local s="$1"
local first="${s:0:1}"
local rest="${s:1}"
printf "%s%s" "$(printf "%s" "$first" | tr '[:lower:]' '[:upper:]')" "$rest"
}
to_upper() {
printf "%s" "$1" | tr '[:lower:]' '[:upper:]'
}
OS_LIST=(linux darwin win freebsd openbsd)
for os in "${OS_LIST[@]}"; do
echo "构建 $(uc_first "$os") 系列版本..."
case "$os" in
linux) archs=(arm64 386 mips mips64 mips64le ppc64 ppc64le riscv64 s390x) ;;
darwin) archs=(amd64 arm64) ;;
win) archs=(arm64) ;;
freebsd) archs=(amd64 arm64 386) ;;
openbsd) archs=(amd64 arm64 386) ;;
*) archs=() ;;
esac
for arch in "${archs[@]}"; do
# 设置输出文件名后缀
suffix=""
if [ "$os" = "win" ]; then
suffix=".exe"
fi
echo "构建 $(uc_first "$os") $(to_upper "$arch") 版本"
# 根据条件设置不同的输出文件名格式
if [[ "$os" == "darwin" || ("$os" == "linux" && "$arch" == "arm64") || ("$os" == "win" && "$arch" == "arm64") ]]; then
output_file="$OUT_DIR/miao-directory-${arch}-${os}${suffix}"
else
output_file="$OUT_DIR/miao-directory-x-${arch}-${os}${suffix}"
fi
GOOS_VAR="$os"
if [ "$os" = "win" ]; then
GOOS_VAR="windows"
fi
CGO_ENABLED=0 GOOS="$GOOS_VAR" GOARCH="$arch" go build -ldflags="-s -w" -o "$output_file"
if [ "$os" != "win" ]; then
chmod +x "$output_file"
fi
done
done
# 恢复resource.syso文件
if [ -f "resource.syso.bak" ]; then
echo "恢复resource.syso文件"
mv resource.syso.bak resource.syso
fi
: # 无需恢复 CGO,全程使用内联变量
fi
# ===== 压缩阶段 =====
echo "===== 开始压缩阶段 ====="
# 检查是否可以进行UPX压缩
if check_upx; then
echo "开始压缩所有可执行文件..."
# 设置压缩级别
COMPRESSION_LEVEL="--ultra-brute"
if [ "$FAST_COMPRESSION" = true ]; then
echo "使用快速压缩模式"
COMPRESSION_LEVEL="--fast"
else
echo "使用极限压缩模式"
fi
# 压缩基础版本
echo "压缩基础版本..."
# 压缩Windows无GUI版本
echo "正在压缩Win AMD64无GUI版本"
upx $COMPRESSION_LEVEL -o "$OUT_DIR/miao-directory-amd64-win.upx.exe" "$OUT_DIR/miao-directory-amd64-win.exe"
# 压缩Linux AMD64版本
echo "正在压缩Linux AMD64版本"
upx $COMPRESSION_LEVEL -o "$OUT_DIR/miao-directory-amd64-linux.upx" "$OUT_DIR/miao-directory-amd64-linux"
# 压缩Windows GUI版本
echo "正在压缩Win AMD64 GUI版本"
upx $COMPRESSION_LEVEL -o "$OUT_DIR/miao-directory-amd64-win.gui.upx.exe" "$OUT_DIR/miao-directory-amd64-win.gui.exe"
# 压缩额外架构版本
if [ "$FULL_BUILD" = true ]; then
echo "压缩额外架构版本..."
# 压缩Linux ARM64版本
echo "正在压缩Linux ARM64版本"
upx $COMPRESSION_LEVEL -o "$OUT_DIR/miao-directory-arm64-linux.upx" "$OUT_DIR/miao-directory-arm64-linux"
fi
echo "所有压缩完成"
fi
echo "构建完成"
echo "构建模式: $([ "$FULL_BUILD" = true ] && echo "完整构建" || echo "基础构建")"
echo "压缩模式: $([ "$FAST_COMPRESSION" = true ] && echo "快速压缩" || echo "极限压缩")"
+38
View File
@@ -0,0 +1,38 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
MiaoAlertTipProvider: typeof import('./src/components/miaoAlertTipProvider.vue')['default']
MiaoContextMenu: typeof import('./src/components/miaoContextMenu.vue')['default']
MiaoDraggableDiv: typeof import('./src/components/miaoDraggableDiv.vue')['default']
MiaoDropHandler: typeof import('./src/components/miaoDropHandler.vue')['default']
MiaoLazyDiv: typeof import('./src/components/miaoLazyDiv.vue')['default']
MiaoMask: typeof import('./src/components/miaoMask.vue')['default']
MiaoPopupInput: typeof import('./src/components/miaoPopupInput.vue')['default']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
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']
NQrCode: typeof import('naive-ui')['NQrCode']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
ShareQrCodeCard: typeof import('./src/components/APP/ShareQrCodeCard.vue')['default']
ViewContainer: typeof import('./src/components/APP/ViewContainer.vue')['default']
ViewController: typeof import('./src/components/APP/ViewController.vue')['default']
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="./public/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="./icon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MiaoDirectory</title> <title>MiaoDirectory</title>
</head> </head>
+25 -4
View File
@@ -7,9 +7,11 @@
"dev": "vite", "dev": "vite",
"dev:host": "vite --host 0.0.0.0", "dev:host": "vite --host 0.0.0.0",
"build": "vue-tsc -b && vite build", "build": "vue-tsc -b && vite build",
"build:auto": "pnpm run build && rm -rf ./serve/web/* && cp -r ./dist/* ./serve/web/ && cd ./serve && go build && mv ./miao-directory.exe ../", "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:auto:release": "pnpm run build && rm -rf ./serve/web/* && cp -r ./dist/* ./serve/web/ && cd ./serve && go env -w GOOS=windows GOARCH=amd64 && go build -o ../miao-directory-amd64-win.exe && go env -w GOOS=linux GOARCH=amd64 && go build -o ../miao-directory-amd64-linux", "build:fast": "bash build.auto.release.sh -fast",
"preview": "vite preview" "build:normal": "bash build.auto.release.sh",
"preview": "vite preview",
"format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-go": "^6.0.1", "@codemirror/lang-go": "^6.0.1",
@@ -23,24 +25,43 @@
"@codemirror/lang-vue": "^0.1.3", "@codemirror/lang-vue": "^0.1.3",
"@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-xml": "^6.1.0",
"@codemirror/lang-yaml": "^6.1.1", "@codemirror/lang-yaml": "^6.1.1",
"@mediapipe/tasks-genai": "^0.10.22",
"@tailwindcss/vite": "^4.0.13",
"@types/sortablejs": "^1.15.8",
"@vicons/ionicons5": "^0.12.0", "@vicons/ionicons5": "^0.12.0",
"aes-js": "^3.1.2",
"aplayer": "^1.10.1",
"axios": "^1.7.3", "axios": "^1.7.3",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"copy-to-clipboard": "^3.3.3",
"drag-drop-touch": "^1.3.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"naive-ui": "^2.39.0", "naive-ui": "^2.39.0",
"pdfjs-dist": "^4.5.136", "pdfjs-dist": "^4.5.136",
"sass": "^1.77.8", "sass": "^1.77.8",
"vue": "^3.4.35", "sortablejs": "^1.15.3",
"tailwindcss": "^4.0.13",
"vue": "3.5.0",
"vue-codemirror": "^6.1.1", "vue-codemirror": "^6.1.1",
"vue-draggable-plus": "^0.5.3",
"xgplayer": "^3.0.20", "xgplayer": "^3.0.20",
"xgplayer-subtitles": "^3.0.20" "xgplayer-subtitles": "^3.0.20"
}, },
"devDependencies": { "devDependencies": {
"@types/aes-js": "^3.1.4",
"@types/lodash": "^4.17.7", "@types/lodash": "^4.17.7",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.1.0", "@types/node": "^22.1.0",
"@types/pdfjs-dist": "^2.10.378", "@types/pdfjs-dist": "^2.10.378",
"@vitejs/plugin-vue": "^5.1.2", "@vitejs/plugin-vue": "^5.1.2",
"prettier": "^3.4.2",
"prettier-plugin-vue": "^1.1.6",
"rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^5.4.0", "vite": "^5.4.0",
"vite-plugin-singlefile": "^2.0.2", "vite-plugin-singlefile": "^2.0.2",
"vue-tsc": "^2.0.29" "vue-tsc": "^2.0.29"
+1034 -85
View File
File diff suppressed because it is too large. Load diff
+76
View File
@@ -0,0 +1,76 @@
package api
import (
"net"
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
)
// AddressesHandler 返回服务器所有可用的访问地址
func AddressesHandler(serverPort string) gin.HandlerFunc {
return func(c *gin.Context) {
addresses := []gin.H{}
addresses = append(addresses, gin.H{
"title": "localhost",
"url": "http://localhost" + serverPort,
})
host, _ := os.Hostname()
addrs, _ := net.LookupHost(host)
for _, addr := range addrs {
if strings.Contains(addr, "%") {
addr = strings.Split(addr, "%")[0]
}
var url string
var title string
if strings.Contains(addr, ":") {
url = "http://[" + addr + "]" + serverPort
title = "IPv6 Address (" + addr + ")"
} else {
url = "http://" + addr + serverPort
title = "IPv4 Address (" + addr + ")"
}
addresses = append(addresses, gin.H{
"title": title,
"url": url,
})
}
c.JSON(http.StatusOK, addresses)
}
}
// 判断是否是私有IP地址
func isPrivateIP(ip string) bool {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return false
}
// 检查常见的私有IP范围
// 10.0.0.0/8
if ipAddr[0] == 10 {
return true
}
// 172.16.0.0/12
if ipAddr[0] == 172 && ipAddr[1] >= 16 && ipAddr[1] <= 31 {
return true
}
// 192.168.0.0/16
if ipAddr[0] == 192 && ipAddr[1] == 168 {
return true
}
// 127.0.0.0/8 (本地回环)
if ipAddr[0] == 127 {
return true
}
return false
}
+3
View File
@@ -0,0 +1,3 @@
package api
var staticPath = "./"
+106
View File
@@ -0,0 +1,106 @@
package api
import (
"io"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
"miao-directory/fsOperateEventCenter"
)
func CopyHandler(fsOperateEventCenter *fsOperateEventCenter.FsOperateEventCenter) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
NewPath string `json:"newPath"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
decodedPath, err := filepath.Abs(filepath.Join(staticPath, c.Param("path")))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to access path"})
return
}
newPath := filepath.Join(staticPath, req.NewPath, filepath.Base(decodedPath))
eventId := fsOperateEventCenter.Push(func() error {
// 检查源路径是文件还是目录
fileInfo, err := os.Stat(decodedPath)
if err != nil {
return err
}
if fileInfo.IsDir() {
return copyDir(decodedPath, newPath)
}
return copyFile(decodedPath, newPath)
})
c.JSON(http.StatusOK, gin.H{"eventId": eventId})
}
}
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return err
}
return destFile.Sync()
}
// 复制目录及其内容
func copyDir(src, dst string) error {
// 获取源目录信息
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
// 创建目标目录
if err = os.MkdirAll(dst, srcInfo.Mode()); err != nil {
return err
}
// 读取源目录内容
entries, err := os.ReadDir(src)
if err != nil {
return err
}
// 遍历并复制每个条目
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
// 递归复制子目录
if err = copyDir(srcPath, dstPath); err != nil {
return err
}
} else {
// 复制文件
if err = copyFile(srcPath, dstPath); err != nil {
return err
}
}
}
return nil
}
+36
View File
@@ -0,0 +1,36 @@
package api
import (
// "fmt"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
"miao-directory/fsOperateEventCenter"
)
func CutHandler(fsOperateEventCenter *fsOperateEventCenter.FsOperateEventCenter) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
NewPath string `json:"newPath"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
decodedPath, err := filepath.Abs(filepath.Join(staticPath, c.Param("path")))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to access path"})
return
}
newPath := filepath.Join(staticPath, req.NewPath, filepath.Base(decodedPath))
eventId := fsOperateEventCenter.Push(func() error {
return os.Rename(decodedPath, newPath)
})
c.JSON(http.StatusOK, gin.H{"eventId": eventId})
}
}
+26
View File
@@ -0,0 +1,26 @@
package api
import (
// "fmt"
"miao-directory/fsOperateEventCenter"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
)
func DeleteHandler(fsOperateEventCenter *fsOperateEventCenter.FsOperateEventCenter) gin.HandlerFunc {
return func(c *gin.Context) {
decodedPath, err := filepath.Abs(filepath.Join(staticPath, c.Param("path")))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to access path"})
return
}
eventId := fsOperateEventCenter.Push(func() error {
return os.RemoveAll(decodedPath)
})
c.JSON(http.StatusOK, gin.H{"eventId": eventId})
}
}
+91
View File
@@ -0,0 +1,91 @@
package api
import (
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
)
func GetHandler(c *gin.Context) {
decodedPath, err := filepath.Abs(filepath.Join(staticPath, c.Param("path")))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to access path"})
return
}
fileInfo, err := os.Stat(decodedPath)
if os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "Path not found"})
return
}
if fileInfo.IsDir() {
files, err := ioutil.ReadDir(decodedPath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to scan directory"})
return
}
var fileDetails []gin.H
for _, file := range files {
fileDetails = append(fileDetails, gin.H{
"name": file.Name(),
"size": file.Size(),
"type": func() string {
if file.IsDir() {
return "directory"
}
return "file"
}(),
"stats": gin.H{
"atimeMs": file.ModTime().UnixNano() / 1e6,
"birthtimeMs": file.ModTime().UnixNano() / 1e6,
"ctimeMs": file.ModTime().UnixNano() / 1e6,
"mtimeMs": file.ModTime().UnixNano() / 1e6,
},
})
}
if len(fileDetails) == 0 {
c.JSON(http.StatusOK, []gin.H{})
} else {
c.JSON(http.StatusOK, fileDetails)
}
} else {
if strings.HasSuffix(decodedPath, "index.html") {
f, err := os.Open(decodedPath)
if err != nil {
c.Writer.WriteHeader(http.StatusInternalServerError)
c.Writer.Write([]byte("Internal Server Error"))
return
}
http.ServeContent(c.Writer, c.Request, "index.html", time.Now(), f)
} else {
// if c.Query("import") != "" {
// c.Writer.Header().Set("Content-Type", "application/javascript")
// }
ext := filepath.Ext(decodedPath)
switch ext {
case ".js":
case ".cjs":
case ".mjs":
c.Writer.Header().Set("Content-Type", "application/javascript")
case ".css":
c.Writer.Header().Set("Content-Type", "text/css")
case ".html":
c.Writer.Header().Set("Content-Type", "text/html")
case ".json":
c.Writer.Header().Set("Content-Type", "application/json")
case ".wasm":
c.Writer.Header().Set("Content-Type", "application/wasm")
default:
c.Writer.Header().Set("Content-Type", "application/octet-stream")
}
c.File(decodedPath)
}
}
}
+142
View File
@@ -0,0 +1,142 @@
package api
import (
"crypto/md5"
"encoding/hex"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
)
// 内存存储
var memoryStorage = make(map[string][]byte)
// MapSetHandler 处理 /map/set/*key 请求
func MapSetHandler(c *gin.Context) {
key := c.Param("key")
if key == "" || key == "/" {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": "Key is required"})
return
}
key = key[1:] // 移除开头的"/"
// 获取disk参数
diskParam := c.DefaultQuery("disk", "0")
// 读取请求体数据
data, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": "Failed to read request body"})
return
}
if diskParam == "1" {
// 保存到文件系统
cachePath := filepath.Join(staticPath, ".miaodir_cache")
// 确保缓存目录存在
if err := os.MkdirAll(cachePath, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": "Failed to create cache directory"})
return
}
// 生成文件名
hasher := md5.New()
hasher.Write([]byte(key))
filename := hex.EncodeToString(hasher.Sum(nil))
// 写入文件
filePath := filepath.Join(cachePath, filename)
if err := ioutil.WriteFile(filePath, data, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": "Failed to write file"})
return
}
} else {
// 保存到内存
memoryStorage[key] = data
}
c.JSON(http.StatusOK, gin.H{"message": "success"})
}
// MapGetHandler 处理 /map/get/*key 请求
func MapGetHandler(c *gin.Context) {
key := c.Param("key")
if key == "" || key == "/" {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": "Key is required"})
return
}
key = key[1:] // 移除开头的"/"
// 获取disk参数
diskParam := c.DefaultQuery("disk", "0")
var data []byte
var err error
if diskParam == "1" {
// 从文件系统获取
cachePath := filepath.Join(staticPath, ".miaodir_cache")
// 生成文件名
hasher := md5.New()
hasher.Write([]byte(key))
filename := hex.EncodeToString(hasher.Sum(nil))
// 读取文件
filePath := filepath.Join(cachePath, filename)
data, err = ioutil.ReadFile(filePath)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"message": "failed", "error": "Key not found"})
return
}
} else {
// 从内存获取
var exists bool
data, exists = memoryStorage[key]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"message": "failed", "error": "Key not found"})
return
}
}
c.Data(http.StatusOK, "application/octet-stream", data)
}
// MapDeleteHandler 处理 /map/delete/*key 请求
func MapDeleteHandler(c *gin.Context) {
key := c.Param("key")
if key == "" || key == "/" {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": "Key is required"})
return
}
key = key[1:] // 移除开头的"/"
// 获取disk参数
diskParam := c.DefaultQuery("disk", "0")
if diskParam == "1" {
// 从文件系统删除
cachePath := filepath.Join(staticPath, ".miaodir_cache")
// 生成文件名
hasher := md5.New()
hasher.Write([]byte(key))
filename := hex.EncodeToString(hasher.Sum(nil))
// 删除文件
filePath := filepath.Join(cachePath, filename)
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": "Failed to delete file"})
return
}
} else {
// 从内存删除
delete(memoryStorage, key)
}
c.JSON(http.StatusOK, gin.H{"message": "success"})
}
+34
View File
@@ -0,0 +1,34 @@
package api
import (
"github.com/gin-gonic/gin"
"net/http"
"os"
"path/filepath"
)
func MkdirHandler(c *gin.Context) {
var request struct {
FolderName string `json:"folderName"`
}
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": err.Error()})
return
}
decodedPath, err := filepath.Abs(filepath.Join(staticPath, c.Param("path")))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": "Unable to access path"})
return
}
mkdirPath := filepath.Join(decodedPath, request.FolderName)
if err := os.MkdirAll(mkdirPath, os.ModePerm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "success"})
}
+24
View File
@@ -0,0 +1,24 @@
package api
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"miao-directory/fsOperateEventCenter"
)
func QueryHandler(fsOperateEventCenter *fsOperateEventCenter.FsOperateEventCenter) gin.HandlerFunc {
return func(c *gin.Context) {
idStr := c.Query("id")
id, err := strconv.ParseInt(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid id"})
return
}
event := fsOperateEventCenter.Query(int32(id))
c.JSON(http.StatusOK, event)
}
}
+36
View File
@@ -0,0 +1,36 @@
package api
import (
// "fmt"
"net/http"
"os"
"path/filepath"
"github.com/gin-gonic/gin"
"miao-directory/fsOperateEventCenter"
)
func RenameHandler(fsOperateEventCenter *fsOperateEventCenter.FsOperateEventCenter) gin.HandlerFunc {
return func(c *gin.Context) {
var req struct {
NewName string `json:"newName"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
decodedPath, err := filepath.Abs(filepath.Join(staticPath, c.Param("path")))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Unable to access path"})
return
}
newTargetPath := filepath.Join(filepath.Dir(decodedPath), req.NewName)
eventId := fsOperateEventCenter.Push(func() error {
return os.Rename(decodedPath, newTargetPath)
})
c.JSON(http.StatusOK, gin.H{"eventId": eventId})
}
}
+58
View File
@@ -0,0 +1,58 @@
package api
import (
"github.com/gin-gonic/gin"
"net/http"
"os"
"path/filepath"
"io/ioutil"
)
func UploadHandler(c *gin.Context) {
fileName := c.PostForm("fileName")
operateType := c.PostForm("operateType")
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "failed", "error": err.Error()})
return
}
decodedPath, err := filepath.Abs(filepath.Join(staticPath, c.Param("path")))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": "Unable to access path"})
return
}
savePath := filepath.Join(decodedPath, fileName)
fileData, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": err.Error()})
return
}
defer fileData.Close()
data, err := ioutil.ReadAll(fileData)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": err.Error()})
return
}
if operateType == "write" {
err = ioutil.WriteFile(savePath, data, 0644)
} else {
f, err := os.OpenFile(savePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": err.Error()})
return
}
defer f.Close()
_, err = f.Write(data)
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": "failed", "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "success"})
}
@@ -0,0 +1,93 @@
package fsOperateEventCenter
import (
// "fmt"
"math/rand"
"sync"
"time"
)
type FsOperateEventStatus string
const (
FsOperateEventStatusProgress FsOperateEventStatus = "progress"
FsOperateEventStatusSuccess FsOperateEventStatus = "success"
FsOperateEventStatusFailed FsOperateEventStatus = "failed"
FsOperateEventStatusUnknown FsOperateEventStatus = "unknown"
)
type FsOperateEvent struct {
Status FsOperateEventStatus `json:"status"`
FailedEvent error `json:"failedEvent"`
UID int32 `json:"uid"`
}
type FsOperateEventCenter struct {
eventMap sync.Map
}
func NewFsOperateEventCenter() *FsOperateEventCenter {
return &FsOperateEventCenter{}
}
func (c *FsOperateEventCenter) Push(event func() error) int32 {
ev := &FsOperateEvent{
Status: FsOperateEventStatusProgress,
UID: generateId(),
}
wrapper := &eventWrapper{
event: ev,
timer: time.AfterFunc(time.Hour, func() {
c.Query(ev.UID)
}),
}
c.eventMap.Store(ev.UID, wrapper)
go func() {
if err := event(); err != nil {
wrapper.event.Status = FsOperateEventStatusFailed
wrapper.event.FailedEvent = err
} else {
wrapper.event.Status = FsOperateEventStatusSuccess
}
}()
return ev.UID
}
type eventWrapper struct {
event *FsOperateEvent
timer *time.Timer
}
func (c *FsOperateEventCenter) Query(id int32) *FsOperateEvent {
val, ok := c.eventMap.Load(id)
// fmt.Printf("Query ID: %d; ok: %t; val: %v\n", id, ok, val)
// c.eventMap.Range(func(key, value interface{}) bool {
// fmt.Printf("key: %d (type: %T), id: %d (type: %T), bool: %t\n", key, key, id, id, id == key)
// event := value.(*eventWrapper).event
// fmt.Printf("Event UID: %d, Status: %s\n", event.UID, event.Status)
// return true
// })
if !ok {
return &FsOperateEvent{
Status: FsOperateEventStatusUnknown,
}
}
wrapper := val.(*eventWrapper)
if wrapper.event.Status == FsOperateEventStatusSuccess || wrapper.event.Status == FsOperateEventStatusFailed {
wrapper.timer.Stop()
c.eventMap.Delete(id)
}
return wrapper.event
}
func generateId() int32 {
return rand.Int31()
}
+39
View File
@@ -0,0 +1,39 @@
module miao-directory
go 1.23.0
require (
github.com/akavel/rsrc v0.10.2 // indirect
github.com/bytedance/sonic v1.12.1 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gin-contrib/gzip v1.0.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/jchv/go-webview2 v0.0.0-20221223143126-dc24628cff85 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/josephspurrier/goversioninfo v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/webview/webview v0.0.0-20250319170551-f1f42f0f164e // indirect
golang.org/x/arch v0.9.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+97
View File
@@ -0,0 +1,97 @@
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24=
github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jchv/go-webview2 v0.0.0-20221223143126-dc24628cff85 h1:t6lhRbwURcdWgp8OsJlq6sOfWMOXP21YuiCicjutHv4=
github.com/jchv/go-webview2 v0.0.0-20221223143126-dc24628cff85/go.mod h1:/BNVc0Sw3Wj6Sz9uSxPwhCEUhhWs92hPde75K2YV24A=
github.com/jchv/go-winloader v0.0.0-20200815041850-dec1ee9a7fd5/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8=
github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/webview/webview v0.0.0-20250319170551-f1f42f0f164e h1:Kz8a4/xfC3vH5sXgPfNfwu2qwwTUrD79N0bFRS6E4tI=
github.com/webview/webview v0.0.0-20250319170551-f1f42f0f164e/go.mod h1:Zk81X+8/mp/MNoeJXXK4Noydn8pcea09e00dFfAwrxg=
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

+108
View File
@@ -0,0 +1,108 @@
//go:generate goversioninfo -icon=./icon.ico
package main
import (
"embed"
"fmt"
"net"
"net/http"
"os"
"strings"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"miao-directory/api"
"miao-directory/fsOperateEventCenter"
)
//go:embed all:web/*
var webFS embed.FS
var port = ":17705"
func main() {
gin.SetMode(gin.ReleaseMode)
fsOperateEventCenter := fsOperateEventCenter.NewFsOperateEventCenter()
router := gin.Default()
router.Use(corsMiddleware())
router.GET("/query", api.QueryHandler(fsOperateEventCenter))
router.GET("/get/*path", api.GetHandler)
router.HEAD("/get/*path", api.GetHandler)
router.POST("/mkdir/*path", api.MkdirHandler)
router.POST("/upload/*path", api.UploadHandler)
router.POST("/cut/*path", api.CutHandler(fsOperateEventCenter))
router.POST("/copy/*path", api.CopyHandler(fsOperateEventCenter))
router.POST("/rename/*path", api.RenameHandler(fsOperateEventCenter))
router.POST("/delete/*path", api.DeleteHandler(fsOperateEventCenter))
// 添加获取所有可用地址的API
router.GET("/addresses", api.AddressesHandler(port))
// 添加map相关API
router.POST("/map/set/*key", api.MapSetHandler)
router.GET("/map/get/*key", api.MapGetHandler)
router.POST("/map/delete/*key", api.MapDeleteHandler)
// Redirect from '/' to '/web/'
router.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/web/")
})
// Serve static files from the embedded filesystem with gzip compression
staticFS := http.FS(webFS)
staticGroup := router.Group("/web")
staticGroup.Use(gzip.Gzip(gzip.DefaultCompression))
staticGroup.GET("/*filepath", func(c *gin.Context) {
http.FileServer(staticFS).ServeHTTP(c.Writer, c.Request)
})
address := "[::]" + port
printListeningAddresses(port)
// 在非阻塞的goroutine中启动服务器
go func() {
if err := router.Run(address); err != nil {
fmt.Printf("服务器启动失败: %v\n", err)
os.Exit(1)
}
}()
// 如果启用了webview,则启动webview窗口
if useWebView {
startWebView(port)
} else {
// 如果不使用webview,则阻塞主线程,保持服务器运行
select {}
}
}
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Next()
}
}
func printListeningAddresses(port string) {
host, _ := os.Hostname()
addrs, _ := net.LookupHost(host)
fmt.Printf("Listening on:\n")
fmt.Printf(" ➜ Local: http://localhost%s\n", port)
for _, addr := range addrs {
if strings.Contains(addr, "%") {
addr = strings.Split(addr, "%")[0]
}
if strings.Contains(addr, ":") {
// IPv6 address
fmt.Printf(" ➜ Network: http://[%s]%s\n", addr, port)
} else {
// IPv4 address
fmt.Printf(" ➜ Network: http://%s%s\n", addr, port)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
# miao-directory的后端部分
* 原有的node后端不再维护。
* ./web文件夹中的内容自己到main分支去打包一个出来
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
{
"IconPath": "icon.ico"
}
+38
View File
@@ -0,0 +1,38 @@
//go:build webview
// +build webview
package main
import (
"fmt"
"github.com/jchv/go-webview2"
)
// 启动WebView窗口
func startWebView(port string) {
// 构建本地服务URL
serverURL := fmt.Sprintf("http://localhost%s", port)
// 创建webview窗口
w := webview2.NewWithOptions(webview2.WebViewOptions{
Debug: false,
AutoFocus: true,
WindowOptions: webview2.WindowOptions{
Title: "MiaoDirectory Server",
Width: 1024,
Height: 768,
Center: true,
},
})
defer w.Destroy()
// 导航到本地服务器
w.Navigate(serverURL)
// 启动webview窗口
w.Run()
}
// 定义是否使用WebView的变量
var useWebView = true
+12
View File
@@ -0,0 +1,12 @@
//go:build !webview
// +build !webview
package main
// 定义是否使用WebView的变量
var useWebView = false
// 空函数,在非webview版本中不执行任何操作
func startWebView(port string) {
// 在非webview版本中不做任何事情
}
+121 -372
View File
@@ -1,413 +1,162 @@
<script setup lang="ts"> <script setup lang="ts">
import { h, onMounted, reactive, ref, shallowRef } from 'vue'
import { NIcon, NScrollbar, NDropdown, NQrCode } from 'naive-ui'
import { import {
EyeOffOutline, computed,
EyeOutline, onMounted,
CloseOutline, provide,
CopyOutline, ref,
EllipsisVertical, shallowRef,
QrCodeOutline useTemplateRef
} from '@vicons/ionicons5' } from 'vue'
import miaoDirectory from '@/views/miaoDirectory.vue' import { QrCodeOutline, ExtensionPuzzleOutline } from '@vicons/ionicons5'
import VirtualDirectory, { VirtualFile } from './class/VirtualDirectory' import type VirtualDirectory from './class/VirtualDirectory'
import type { VirtualFile } from './class/VirtualDirectory'
import MiaoMask from './components/miaoMask.vue' import MiaoMask from './components/miaoMask.vue'
import config from './config' import MiaoMessageProvider from './components/miaoAlertTipProvider.vue'
// import config from './config'
import useVirtualPages from './hooks/useVirtualPages' import useVirtualPages from './hooks/useVirtualPages'
import init from './hooks/init' import init from './hooks/init'
import usePluginCenter from './hooks/usePluginCenter'
import PluginCenter, { PluginGroup } from './class/PluginCenter'
import { renderIcon } from './utils/miaoTools'
import { DropdownMixedOption } from 'naive-ui/es/dropdown/src/interface'
import useRootVDirectory from './hooks/useRootVDirectory'
import ViewController from '@/components/APP/ViewController.vue'
import ViewContainer from '@/components/APP/ViewContainer.vue'
import ShareQrCodeCard from './components/APP/ShareQrCodeCard.vue'
const { baseUrl } = config // const { baseUrl } = config
const showModal = ref<boolean>(false) const showModal = ref<boolean>(false)
const viewRef = ref<HTMLDivElement>()
const modalData = shallowRef<{ const modalData = shallowRef<{
component: any component: any
props: any props: any
}>() }>()
const messageProviderRef = useTemplateRef('messageProviderRef')
const views = useVirtualPages() const views = useVirtualPages()
let pluginCenter = ref<PluginCenter>()
const rootDirectory = useRootVDirectory()
const rootDirectory = reactive(
new VirtualDirectory({
name: '根目录',
stats: {
atimeMs: 0,
birthtimeMs: 0,
ctimeMs: 0,
mtimeMs: 0
}
})
)
// component就是一个vue组件,类似于miaoDirectory // component就是一个vue组件,类似于miaoDirectory
const createView = ( const createView = (
component: any, component: any,
currentDirectorys?: VirtualDirectory[], name: string,
currentFiles?: VirtualFile[], currentDirectories?: VirtualDirectory[],
index?: number currentFiles?: VirtualFile[],
index?: number
) => { ) => {
views.push(component, currentDirectorys, currentFiles, index) views.push(component, name, currentDirectories, currentFiles, { index })
} }
const deleteView = (index: number) => { const deleteView = (index: number) => {
views.deleteView(index) views.deleteView(index)
if (views.length === 0) {
createView(miaoDirectory, [rootDirectory])
}
} }
// 点击标题是隐藏其他标签或显示其他标签,还挺好用的 // 点击标题是隐藏其他标签或显示其他标签,还挺好用的
const handleClickTitle = (index: number) => { const handleClickTitle = (index: number) => {
let count = views._views.filter((v) => v.visible).length let count = views._views.filter((v) => v.visible).length
let op = !views._views[index].visible || count > 1 ? 'hideOthers' : 'showOthers' let op =
let _index = 0 !views._views[index].visible || count > 1 ? 'hideOthers' : 'showOthers'
while (_index < views._views.length) { let _index = 0
if (_index !== index) { while (_index < views._views.length) {
views._views[_index].visible = op === 'hideOthers' ? false : true if (_index !== index) {
} else { views._views[_index].visible = op === 'hideOthers' ? false : true
views._views[_index].visible = true } else {
} views._views[_index].visible = true
_index += 1 }
} _index += 1
}
} }
// 顶部菜单相关 const openMenuOption = computed(
function renderIcon(icon: any) { () =>
return () => { [
return h(NIcon, null, { {
default: () => h(icon) label: '分享',
}) key: 'share',
} icon: renderIcon(QrCodeOutline)
} },
pluginCenter.value
const openMenuOption = [ ? {
{ label: '插件',
label: '分享', key: 'plugin',
key: 'share', children: [
icon: renderIcon(QrCodeOutline) ...pluginCenter.value
} .getUsablePlugin([], [], {
] group: [PluginGroup.mainMenu]
})
const handleMenuSelect = (key: string) => { .map((v) => ({
if (key === 'share') { label: `${v.name}`,
showModal.value = true key: `plugin:${v.key}`,
modalData.value = { icon: v.icon ? renderIcon(v.icon) : undefined
component: NQrCode, }))
props: { ],
value: baseUrl, icon: renderIcon(ExtensionPuzzleOutline)
size: 300,
errorCorrectionLevel: 'H'
} }
} : undefined
} ].filter((v) => v) as DropdownMixedOption[]
)
const handleMenuSelect = async (key: string) => {
if (key === 'share') {
// 立即显示模态框
showModal.value = true
// 设置模态框数据,将serverAddressesService传递给组件
modalData.value = {
component: ShareQrCodeCard,
props: {}
}
} else if (key.startsWith('plugin:')) {
const pluginName = key.split(':')[1]
pluginCenter.value && pluginCenter.value.usePlugin(pluginName, [], [])
}
} }
onMounted(() => { const handleModalClose = () => {
init() showModal.value = false
createView(miaoDirectory, [rootDirectory]) nextTick(() => {
modalData.value = undefined
})
}
onMounted(async () => {
// @ts-ignore
await init(messageProviderRef.value?.alertTip)
pluginCenter.value = usePluginCenter()
}) })
// @ts-ignore
provide('globalAlertTip', messageProviderRef.value?.alertTip)
provide('rootDirectory', rootDirectory)
</script> </script>
<template> <template>
<div class="view"> <div class="view" ref="viewRef">
<div class="view-controller"> <miao-message-provider ref="messageProviderRef">
<n-scrollbar x-scrollable> <ViewController :views="views" :handleClickTitle="handleClickTitle" :createView="createView"
<div class="tabs-container"> :deleteView="deleteView" :openMenuOption="openMenuOption" :handleMenuSelect="handleMenuSelect" />
<transition-group name="tab"> <ViewContainer :views="views" :deleteView="deleteView" :handleClickTitle="handleClickTitle" />
<div </miao-message-provider>
class="tab" </div>
v-for="(view, index) of views._views" <!-- 模态框展示,用来显示分享二维码,设置菜单之类的东西 -->
:key="view.uid"> <MiaoMask v-model:show="showModal" @click="handleModalClose">
<span <component @click="(e: any) => e.stopPropagation()" :is="modalData?.component" v-bind="modalData?.props">
class="tab-title" </component>
:title="view.title" </MiaoMask>
@click="handleClickTitle(index)">
{{
view.title.length > 5 && 0
? `${view.title.substring(0, 5)}...`
: view.title
}}
</span>
<div
class="tab-point"
:style="{ backgroundColor: view.color }"></div>
<div class="tab-control">
<n-icon
class="tag-control-icon icon"
@click="view.switchShow()">
<EyeOutline
v-show="view.visible"
class="icon-inner" />
<EyeOffOutline
v-show="!view.visible"
class="icon-inner" />
</n-icon>
<n-icon
class="tag-control-icon icon"
@click="
createView(
view.component,
view.currentDirectorys,
view.currentFiles,
index + 1
)
">
<CopyOutline />
</n-icon>
<n-icon
class="tag-control-icon icon"
@click="deleteView(index)">
<CloseOutline class="icon-inner" />
</n-icon>
</div>
</div>
</transition-group>
</div>
</n-scrollbar>
<div class="view-controller-menu">
<div class="view-controller-menu-item">
<n-dropdown
trigger="click"
:options="openMenuOption"
@select="handleMenuSelect">
<n-icon class="icon">
<EllipsisVertical class="icon-inner" />
</n-icon>
</n-dropdown>
</div>
</div>
</div>
<div class="view-container">
<transition-group name="page">
<div
class="view-container-item"
v-for="(view, index) of views._views"
v-show="view.visible"
:key="view.uid">
<component
:is="view.component"
v-model:current-directorys="view.currentDirectorys"
v-model:current-files="view.currentFiles"
:id="view.uid"
:color="view.color"
@exit="deleteView(index)"></component>
</div>
</transition-group>
</div>
</div>
<!-- 模态框展示,用来显示分享二维码,设置菜单之类的东西 -->
<MiaoMask v-model:show="showModal" @click="showModal = false">
<component
@click="(e:any) => e.stopPropagation()"
:is="modalData?.component"
v-bind="modalData?.props"></component>
</MiaoMask>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@use "styles/default.scss";
$controller-height: 25px; $controller-height: 25px;
$tag-width: 170px; $tag-width: 170px;
.view { .view {
position: relative; background-color: #ffffff;
height: 100%; position: relative;
width: 100%; height: 100%;
width: 100%;
.view-controller {
display: flex;
height: $controller-height;
width: 100%;
background-color: #dadada;
// overflow: hidden;
.tabs-container {
display: flex;
margin-left: 5px;
flex: 1;
// overflow-x: scroll;
.tab {
position: relative;
width: $tag-width;
height: $controller-height;
margin: 0 5px;
// flex: 1;
display: flex;
border-radius: 5px 5px 0 0;
align-items: center;
.tab-title {
// font-family: monospace;
// font-weight: bolder;
margin-left: 4px;
width: 90px;
user-select: none;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
span {
user-select: none;
cursor: pointer;
}
}
.tab-point {
height: 5px;
aspect-ratio: 1;
border-radius: 66px;
margin-left: 5px;
}
.tab-control {
position: absolute;
right: 10px;
display: flex;
align-items: center;
.tab-control-icon:not(:last-child) {
margin-right: 5px;
}
}
&:not(:last-child)::after {
content: '';
height: calc($controller-height * 0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
right: 0px;
}
}
}
.view-controller-menu {
position: relative;
display: flex;
align-items: center;
padding: 0 5px;
.view-controller-menu-item {
height: 100%;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
}
&::before {
content: '';
height: calc($controller-height * 0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
left: 2px;
}
}
.icon {
height: calc($controller-height * 0.7);
width: calc($controller-height * 0.7);
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background-color: rgba(128, 128, 128, 0);
transition: background-color 0.3s;
&:hover {
background-color: rgba(128, 128, 128, 0.4);
}
}
}
.view-container {
position: relative;
display: flex;
// flex-direction: column;
height: calc(100% - $controller-height);
overflow-y: hidden;
overflow-x: overlay;
.view-container-item {
flex: 1;
height: 100%;
min-width: 200px;
}
&::-webkit-scrollbar {
position: absolute;
width: 4px;
}
// 滚动条轨道
&::-webkit-scrollbar-track {
// background: rgb(239, 239, 239);
border-radius: 2px;
}
// 小滑块
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
// border-radius: 5px;
}
&::-webkit-scrollbar-thumb:hover {
background: #999999;
}
}
}
// transition动画相关
.view {
.view-controller {
.tabs-container {
.tab-move,
.tab-enter-active {
transition: all 0.2s ease;
}
.tab-leave-active {
transition: none;
}
.tab-enter-from {
opacity: 0;
transform: translateY(-30px);
}
// .tab-leave-to {
// opacity: 0;
// transform: translateY(-80px) translateX(calc($tag-width + 10px));
// }
.tab-leave-active {
position: absolute;
}
}
}
// .view-container {
// .page-move {
// transition: all 0.2s ease;
// }
// .page-enter-active {
// }
// .page-leave-active {
// transition: none;
// }
// .page-enter-from {
// opacity: 0;
// }
// .page-leave-active {
// position: absolute;
// }
// }
} }
</style> </style>
+181 -61
View File
@@ -1,75 +1,195 @@
import useVirtualPages from '@/hooks/useVirtualPages' import useVirtualPages from '@/hooks/useVirtualPages'
import VirtualDirectory, { VirtualFile } from './VirtualDirectory' import VirtualDirectory, { VirtualFile } from './VirtualDirectory'
import { ShallowRef, shallowRef } from 'vue'
import { alertTipType, usePluginHooksType } from '@/types/type'
import VirtualPage, { VirtualPages } from './VirtualPage'
import useRootVDirectory from '@/hooks/useRootVDirectory'
type plugin = { export enum PluginGroup {
// 用于显示的名字 default = 'default',
name: string mainMenu = 'mainMenu',
// 判断是否可以使用这个组件 // 导入的时候会立即执行函数一次
// 使用组件即调用下面的func immediately = 'immediately'
filter: (VDirectorys: VirtualDirectory[], VFiles: VirtualFile[]) => boolean }
func: (VDirectorys: VirtualDirectory[], VFiles: VirtualFile[]) => void
export interface registerComponentOption {
key: string
name: string
icon?: any
getComponent: () => Promise<any>
group?: PluginGroup[]
filter: (VDirectories: VirtualDirectory[], VFiles: VirtualFile[]) => boolean
disable?: boolean
exitConfirm?: boolean
// 若为true,则只会创建一个页面,当存在已创建页面则会直接将VFile和VDir加到curr里面
single?: boolean
priority?: number
}
export type Plugin = {
// 用于显示的名字
name: string
// 代表插件的icon
icon?: ShallowRef<any>
// 是否禁用插件
disable: boolean
// 插件所属的group,用于区分插件使用的场合
group: PluginGroup[]
// 插件优先级
priority: number
// 判断在未禁用时是否可以使用这个组件
// 使用组件即调用下面的func
filter: (
VDirectories: VirtualDirectory[],
VFiles: VirtualFile[],
VPages: VirtualPages
) => boolean
func: (
VDirectories: VirtualDirectory[],
VFiles: VirtualFile[],
hooks: usePluginHooksType,
...args: any[]
) => void
} }
export default class PluginCenter { export default class PluginCenter {
constructor() { constructor(globalAlertTip: alertTipType) {
this.pluginsMap = {} this.pluginsMap = {}
} this.globalAlertTip = globalAlertTip
pluginsMap: { }
[key: string]: plugin pluginsMap: {
} [key: string]: Plugin
}
register(key: string, plugin: plugin) { globalAlertTip: alertTipType
this.pluginsMap[key] = plugin
}
/** register(key: string, plugin: Plugin) {
* 便捷地注册一个组件为插件,触发时直接用新页面打开 this.pluginsMap[key] = plugin
*/ if (plugin.group.includes(PluginGroup.immediately)) {
registerComponent( this.usePlugin(key, [], [], this.usePluginHook)
key: string, }
name: string, }
getComponent: () => Promise<any>,
filter: ( deregister(key: string) {
VDirectorys: VirtualDirectory[], delete this.pluginsMap[key]
}
/**
* 便捷地注册一个组件为插件,触发时直接用新页面打开,页面名为key
*/
registerComponent(option: registerComponentOption): void {
const {
key,
name,
getComponent,
icon,
filter,
group = [PluginGroup.default],
disable = false,
exitConfirm = false,
single = false,
priority = 0
} = option
let _component: any
const views = useVirtualPages()
this.register(key, {
name,
filter,
icon: shallowRef(icon),
group,
disable,
priority,
func: async (
VDirectories: VirtualDirectory[],
VFiles: VirtualFile[] VFiles: VirtualFile[]
) => boolean ) => {
): void { if (filter(VDirectories, VFiles) === false) {
let _component: any return
const views = useVirtualPages()
this.register(key, {
name,
filter,
func: async (
VDirectorys: VirtualDirectory[],
VFiles: VirtualFile[]
) => {
if (filter(VDirectorys, VFiles) === false) {
return
}
if (_component === undefined) _component = await getComponent()
views.push(_component, VDirectorys, VFiles)
} }
})
}
getUsablePlugin(VDirectorys: VirtualDirectory[], VFiles: VirtualFile[]) { if (_component === undefined) {
const usableList = [] const setAlertTip = this.globalAlertTip(`加载 ${name} 插件中`)
for (let key of Object.keys(this.pluginsMap)) { _component = await getComponent()
if (this.pluginsMap[key].filter(VDirectorys, VFiles)) { setAlertTip &&
usableList.push(key) setAlertTip(`加载 ${name} 插件完成`, {
type: 'success',
timeout: 2000
})
} }
} const _findRes = views.find(_component) as VirtualPage[]
return usableList.map((key) => ({ if (single && _findRes.length !== 0) {
key, const view = _findRes[0]
...this.pluginsMap[key] view.currentFiles = [...view.currentFiles, ...VFiles]
})) view.currentDirectories = [
} ...view.currentDirectories,
...VDirectories
]
} else {
views.push(_component, key, VDirectories, VFiles, {
VirtualPageOption: {
exitConfirm,
allowCopy: single === false
}
})
}
}
})
}
usePlugin( getUsablePlugin(
key: string, VDirectories: VirtualDirectory[],
VDirectorys: VirtualDirectory[], VFiles: VirtualFile[],
VFiles: VirtualFile[] option?: {
) { group?: PluginGroup[]
this.pluginsMap[key].func(VDirectorys, VFiles) }
} ) {
const { group = [PluginGroup.default] } = option ?? {}
const usableList = []
const views = useVirtualPages()
for (let key of Object.keys(this.pluginsMap)) {
const _plugin = this.pluginsMap[key]
if (
!_plugin.disable &&
_plugin.group.filter((v) => group.includes(v)).length > 0 &&
_plugin.filter(VDirectories, VFiles, views)
) {
usableList.push(key)
}
}
const res = usableList.map((key) => ({
key,
...this.pluginsMap[key]
}))
res.sort((a, b) => b.priority - a.priority)
return res
}
get usePluginHook(): usePluginHooksType {
return {
globalAlertTip: this.globalAlertTip,
getViews: useVirtualPages,
getPluginCenter: () => this,
getRootVDirectory: () => useRootVDirectory()
}
}
// usePlugin(
// key: string,
// VDirectories?: VirtualDirectory[],
// VFiles?: VirtualFile[],
// ...args: any[]
// ): void
usePlugin(
key: string,
VDirectories?: VirtualDirectory[],
VFiles?: VirtualFile[],
...args: any[]
) {
this.pluginsMap[key].func(
VDirectories ?? [],
VFiles ?? [],
this.usePluginHook,
...args
)
}
} }
+154 -150
View File
@@ -1,165 +1,169 @@
import generateId from "@/hooks/generateId"; import generateId from '@/utils/generateId'
import useMiaoFetchApi from "@/hooks/useMiaoFetchApi"; import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import VirtualDirectory from "./VirtualDirectory"; import VirtualDirectory from './VirtualDirectory'
const miaoFetchApi = useMiaoFetchApi(); const miaoFetchApi = useMiaoFetchApi()
export class UploadQueueItem { export class UploadQueueItem {
constructor( constructor(
file: File, file: File,
targetDirectory: VirtualDirectory, targetDirectory: VirtualDirectory,
config?: { config?: {
onSuccess?: () => void; onSuccess?: () => void
onFailed?: () => void; onFailed?: () => void
} }
) { ) {
this.file = file; this.file = file
this.targetDirectory = targetDirectory; this.targetDirectory = targetDirectory
this.uid = generateId(); this.uid = generateId()
this.status = "waiting"; this.status = 'waiting'
this.process = 0; this.process = 0
this.onSuccess = config && config.onSuccess; this.onSuccess = config && config.onSuccess
this.onFailed = config && config.onFailed; this.onFailed = config && config.onFailed
} }
uid: number; uid: number
file: File; file: File
targetDirectory: VirtualDirectory; targetDirectory: VirtualDirectory
status: "waiting" | "uploading" | "success" | "failed"; status: 'waiting' | 'uploading' | 'success' | 'failed'
process: number; process: number
failedReason: unknown | undefined; failedReason: unknown | undefined
onSuccess?: () => void; onSuccess?: () => void
onFailed?: () => void; onFailed?: () => void
_abort: (() => void | undefined) | undefined; _abort: (() => void | undefined) | undefined
_status: Promise<any> | undefined; _status: Promise<any> | undefined
/** /**
* 开始上传文件 * 开始上传文件
*/ */
startUpload() { startUpload() {
if (this.status === "uploading") { if (this.status === 'uploading') {
return; return
} }
const { abort, response } = miaoFetchApi.upload(this.file, this.targetDirectory, { const { abort, response } = miaoFetchApi.upload(
onProcess: percent => { this.file,
this.process = percent; this.targetDirectory,
} {
}); onProcess: (percent) => {
this._abort = abort; this.process = percent
this._status = response; }
this.status = "uploading"; }
response )
.then(() => { this._abort = abort
this.status = "success"; this._status = response
this.onSuccess && this.onSuccess(); this.status = 'uploading'
this._abort = undefined; response
this._status = undefined; .then(() => {
}) this.status = 'success'
.catch(e => { this.onSuccess && this.onSuccess()
this.status = "failed"; this._abort = undefined
this.failedReason = e; this._status = undefined
this.onFailed && this.onFailed(); })
}); .catch((e) => {
} this.status = 'failed'
this.failedReason = e
this.onFailed && this.onFailed()
})
}
/** /**
* 终止上传文件 * 终止上传文件
*/ */
abortUpload() { abortUpload() {
this._abort && this._abort(); this._abort && this._abort()
this.status = "failed"; this.status = 'failed'
this.failedReason = "abort"; this.failedReason = 'abort'
} }
} }
export default class UploadQueue { export default class UploadQueue {
constructor() { constructor() {
this.uid = generateId(); this.uid = generateId()
this.queue = []; this.queue = []
this.mode = "serial"; this.mode = 'serial'
} }
uid: number; uid: number
queue: UploadQueueItem[]; queue: UploadQueueItem[]
current: UploadQueueItem | undefined; current: UploadQueueItem | undefined
// 未来再去实现,目前就是串行的 // 未来再去实现,目前就是串行的
// 好像也不用去实现 // 好像也不用去实现
mode: "serial" | "parallel"; mode: 'serial' | 'parallel'
get status() { get status() {
for (let item of this.queue) { for (let item of this.queue) {
if (item.status === "uploading") { if (item.status === 'uploading') {
return "busy"; return 'busy'
} }
} }
return "free"; return 'free'
} }
/** /**
* 获取下一个需要被上传的item * 获取下一个需要被上传的item
*/ */
get nextUplpadItem() { get nextUplpadItem() {
for (let item of this.queue) { for (let item of this.queue) {
if (item.status === "waiting") { if (item.status === 'waiting') {
return item; return item
} }
} }
} }
/** /**
* 开始串行的上传循环 * 开始串行的上传循环
*/ */
loop() { loop() {
if (this.status === "busy") { if (this.status === 'busy') {
return; return
} }
if (this.nextUplpadItem) { if (this.nextUplpadItem) {
this.nextUplpadItem.startUpload(); this.nextUplpadItem.startUpload()
} }
} }
/** /**
* 向上传文件队列输送一批新的文件 * 向上传文件队列输送一批新的文件
* @param {any} files 需要上传的文件 * @param {any} files 需要上传的文件
* @param {any} targetDirectory 目标文件夹 * @param {any} targetDirectory 目标文件夹
* @param {any} option 每个文件上传完成以后都会触发一遍的回调函数 * @param {any} option 每个文件上传完成以后都会触发一遍的回调函数
* @returns {any} * @returns {any}
*/ */
push( push(
files: File[], files: File[],
targetDirectory: VirtualDirectory, targetDirectory: VirtualDirectory,
option?: { option?: {
/** /**
* 每个文件上传完成以后都会触发一遍的回调函数 * 每个文件上传完成以后都会触发一遍的回调函数
*/ */
onFinish?: () => void; onFinish?: () => void
/** /**
* 是否立即开始上传,若否则按队列顺序一个一个上传 * 是否立即开始上传,若否则按队列顺序一个一个上传
*/ */
immediately?: boolean; immediately?: boolean
} }
) { ) {
const onFinish = option?.onFinish; const onFinish = option?.onFinish
const immediately = option?.immediately ?? false; const immediately = option?.immediately ?? false
const uploadQueue = files.map(file => { const uploadQueue = files.map((file) => {
return new UploadQueueItem(file, targetDirectory, { return new UploadQueueItem(file, targetDirectory, {
onSuccess: () => { onSuccess: () => {
this.loop(); this.loop()
onFinish && onFinish(); onFinish && onFinish()
}, },
onFailed: () => { onFailed: () => {
this.loop(); this.loop()
} }
}); })
}); })
this.queue.push(...uploadQueue); this.queue.push(...uploadQueue)
if (!immediately) { if (!immediately) {
this.loop(); this.loop()
} else { } else {
uploadQueue.forEach(item => { uploadQueue.forEach((item) => {
item.startUpload(); item.startUpload()
}); })
} }
return uploadQueue; return uploadQueue
} }
} }
+206 -140
View File
@@ -1,149 +1,215 @@
import config from "@/config"; import generateId from '@/utils/generateId'
import generateId from "@/hooks/generateId"; import type { stats, file, directory } from '@/types/type.ts'
import useMiaoFetchApi from "@/hooks/useMiaoFetchApi";
// import { debounce } from "lodash";
import type { stats, file, directory } from "@/types/type.ts";
const miaoFetchApi = useMiaoFetchApi();
export class VirtualFile { // const vDirectoryMap = new Map<string, VirtualDirectory>()
constructor(info: file, parent: VirtualDirectory) { // const vFileMap = new Map<string, VirtualFile>()
this.name = info.name;
this.size = info.size;
this.stats = info.stats;
this.parent = parent;
this.type = "file";
this.uid = generateId();
}
name: string;
type: "file";
size: number;
stats: stats;
parent: VirtualDirectory;
uid: number;
get path() {
return `${this.parent.path}${this.name}`;
}
get url() { export interface Tree {
return `${config.api.get}${this.parent.path}${this.name}` name: string
} type: 'directory'
path: string
stats: stats
files: {
name: string
type: 'file'
size: number
stats: stats
url: string
}[]
directories: Tree[]
} }
class VirtualDirectory { export abstract class VirtualFileBase {
constructor(info: directory, parent?: VirtualDirectory) { name: string
this.name = info.name; type: 'file' = 'file'
this.type = "directory"; size: number
this.stats = info.stats; stats: stats
this.parent = parent ?? undefined; parent: VirtualDirectoryBase
this.uid = generateId(); id: number
} storage: { [key: string]: any } = {}
// 文件夹名
name: string;
// 子文件
files?: VirtualFile[];
// 子目录
directorys?: VirtualDirectory[];
// 没啥意义,标注一下就是了
type: "directory";
// 文件夹创建时间等杂项属性
stats: stats;
// 父文件夹
parent?: VirtualDirectory;
uid: number; constructor(name: string, size: number, stats: stats, parent: VirtualDirectoryBase) {
this.name = name
this.size = size
this.stats = stats
this.parent = parent
this.id = generateId()
}
/** get path() {
* 获取相对于根目录的路径的层数 return `${this.parent.path}${this.name}`
* 用于目录下文件定位 }
* @returns {string}
*/
get layer(): number {
return this.parent ? this.parent.layer + 1 : 0;
}
/**
* 相对于根目录的路径, 如: "/", "/123/321/"
* 用于目录下文件定位
* @returns {string}
*/
get path(): string {
if (!this.parent) {
return "/";
}
return `${this.parent.path}${this.name}/`;
}
/** abstract get url(): string;
* 获取从根目录开始的按顺序的文件夹
* @returns {VirtualDirectory[]} /**
*/ * 删除文件
get getParents(): VirtualDirectory[] { * @returns {Promise<boolean>} 删除是否成功
if (!this.parent) { */
return [this]; abstract delete(): Promise<boolean>;
}
return [...this.parent.getParents, this]; /**
} * 重命名文件
* @param {string} newName 新文件名
/** * @returns {Promise<boolean>} 重命名是否成功
* 获取 layer 级父文件夹, 如getParent(1)就返回父级 */
* @param {number} layer 需要跳转的层数 abstract rename(newName: string): Promise<boolean>;
* @returns {VirtualDirectory}
*/ /**
getParent(layer: number): VirtualDirectory { * 移动文件到目标目录
if (layer <= 0 || !this.parent) { * @param {VirtualDirectoryBase} targetDir 目标目录
return this; * @returns {Promise<boolean>} 移动是否成功
} */
return this.parent.getParent(layer - 1); abstract moveTo(targetDir: VirtualDirectoryBase): Promise<boolean>;
}
/**
updateContent(content: (file | directory)[]) { * 复制文件到目标目录
this.files = this.files ?? []; * @param {VirtualDirectoryBase} targetDir 目标目录
this.directorys = this.directorys ?? []; * @returns {Promise<boolean>} 复制是否成功
const fileNames: string[] = this.files.map(v => v.name); */
const dirNames: string[] = this.directorys.map(v => v.name); abstract copyTo(targetDir: VirtualDirectoryBase): Promise<boolean>;
const itemNames: string[] = content.map(v => v.name);
// 删去远程端已不存在的文件与文件夹
const _delete = (target: (VirtualFile | VirtualDirectory)[]) => {
let index = 0;
while (index < target.length) {
if (!itemNames.includes(target[index].name)) {
target.splice(index, 1);
} else {
index += 1;
}
}
};
_delete(this.files);
_delete(this.directorys);
// 加入新的文件与文件夹
for (let item of content) {
if (item.type === "file") {
if (!fileNames.includes(item.name)) {
this.files.push(new VirtualFile(item, this));
}
} else if (item.type === "directory") {
if (!dirNames.includes(item.name)) {
this.directorys.push(new VirtualDirectory(item, this));
}
}
}
}
hasChild(item: VirtualDirectory | VirtualFile): boolean {
if (item.type === "directory") {
return this.directorys?.includes(item) ?? false;
} else {
return this.files?.includes(item) ?? false;
}
}
/**
* 调用后重新向远端拉取文件夹信息并进行更新
* @returns {void}
*/
async update(): Promise<void> {
const data: (file | directory)[] = await miaoFetchApi.get(this);
this.updateContent(data);
}
} }
export default VirtualDirectory;
export abstract class VirtualDirectoryBase {
// 文件夹名
name: string
// 子文件
files: VirtualFileBase[] = []
// 子目录
directories: VirtualDirectoryBase[] = []
// 没啥意义,标注一下就是了
type: 'directory' = 'directory'
// 文件夹创建时间等杂项属性
stats: stats
// 父文件夹
parent?: VirtualDirectoryBase
_isUpdated: boolean = false
id: number
// 用来存储一些标记信息啥的
storage: { [key: string]: any } = {}
constructor(name: string, stats: stats, parent?: VirtualDirectoryBase) {
this.name = name
this.stats = stats
this.parent = parent
this.id = generateId()
}
/**
* 获取相对于根目录的路径的层数
* 用于目录下文件定位
* @returns {string}
*/
get layer(): number {
return this.parent ? this.parent.layer + 1 : 0
}
/**
* 相对于根目录的路径, 如: "/", "/123/321/"
* 用于目录下文件定位
* @returns {string}
*/
get path(): string {
if (!this.parent) {
return '/'
}
return `${this.parent.path}${this.name}/`
}
abstract get url(): string;
/**
* 获取从根目录开始的按顺序的文件夹
* @returns {VirtualDirectoryBase[]}
*/
get getParents(): VirtualDirectoryBase[] {
if (!this.parent) {
return [this]
}
return [...this.parent.getParents, this]
}
/**
* 获取当前文件夹是否已经请求过至少一次数据
*/
get isUpdated() {
return this._isUpdated
}
/**
* 获取 layer 级父文件夹, 如getParent(1)就返回父级
* @param {number} layer 需要跳转的层数
* @returns {VirtualDirectoryBase}
*/
getParent(layer: number): VirtualDirectoryBase {
if (layer <= 0 || !this.parent) {
return this
}
return this.parent.getParent(layer - 1)
}
get tree(): Tree {
return {
name: this.name,
type: this.type,
path: this.path,
stats: this.stats,
files:
this.files?.map((v) => ({
name: v.name,
type: v.type,
size: v.size,
stats: v.stats,
url: v.url
})) ?? [],
directories: this.directories?.map((v) => v.tree) ?? []
}
}
hasChild(item: VirtualDirectoryBase | VirtualFileBase): boolean {
if (item.type === 'directory') {
return this.directories?.includes(item as VirtualDirectoryBase) ?? false
} else {
return this.files?.includes(item as VirtualFileBase) ?? false
}
}
abstract updateContent(content: (file | directory)[]): void;
abstract update(): Promise<void>;
/**
* 删除目录
* @returns {Promise<boolean>} 删除是否成功
*/
abstract delete(): Promise<boolean>;
/**
* 重命名目录
* @param {string} newName 新目录名
* @returns {Promise<boolean>} 重命名是否成功
*/
abstract rename(newName: string): Promise<boolean>;
/**
* 移动目录到目标目录
* @param {VirtualDirectoryBase} targetDir 目标目录
* @returns {Promise<boolean>} 移动是否成功
*/
abstract mv(targetDir: VirtualDirectoryBase): Promise<boolean>;
/**
* 复制目录到目标目录
* @param {VirtualDirectoryBase} targetDir 目标目录
* @returns {Promise<boolean>} 复制是否成功
*/
abstract copy(targetDir: VirtualDirectoryBase): Promise<boolean>;
}
export abstract class VirtualFile extends VirtualFileBase {}
export abstract class VirtualDirectory extends VirtualDirectoryBase {}
export default VirtualDirectory
+168 -108
View File
@@ -1,123 +1,183 @@
import { reactive, shallowRef } from "vue"; import { Component, reactive, shallowRef } from 'vue'
import VirtualDirectory, { VirtualFile } from "./VirtualDirectory"; import VirtualDirectory, { VirtualFile } from './VirtualDirectory'
import generateId from "@/hooks/generateId"; import generateId from '@/utils/generateId'
export interface VirtualPageOption {
exitConfirm?: boolean
allowCopy?: boolean
}
export interface ViewsPushOption {
index?: number
VirtualPageOption?: VirtualPageOption
}
const getRandomWebSafeColor = (() => { const getRandomWebSafeColor = (() => {
const webSafeColors = ["#009900", "#999900", "#009999", "#990099", "#0000ee"]; const webSafeColors = ['#009900', '#999900', '#009999', '#990099', '#0000ee']
let counter = 0; let counter = 0
return () => { return () => {
const color = webSafeColors[counter % webSafeColors.length]; const color = webSafeColors[counter % webSafeColors.length]
counter += 1; counter += 1
return color; return color
}; }
})(); })()
export default class VirtualPage { export default class VirtualPage {
constructor(component: any, initObjects?: { directorys?: VirtualDirectory[]; files?: VirtualFile[] }) { constructor(
this.component = shallowRef(component); component: Component,
this.currentDirectorys = initObjects?.directorys ?? []; initObjects: {
this.currentFiles = initObjects?.files ?? []; name: string
this.visible = true; directories?: VirtualDirectory[]
this.uid = generateId(); files?: VirtualFile[]
this.refreshKey = Math.random(); option?: VirtualPageOption
this.color = getRandomWebSafeColor(); }
} ) {
// 页面使用的组件 this.name = initObjects?.name
component: any; this.component = shallowRef(component)
// 是否可见 this.currentDirectories = initObjects?.directories ?? []
visible: boolean; this.currentFiles = initObjects?.files ?? []
// 当前正在使用的文件夹 this.visible = true
currentDirectorys: VirtualDirectory[]; this.id = generateId()
// 当前正在使用的文件 this.refreshKey = Math.random()
currentFiles: VirtualFile[]; this.color = getRandomWebSafeColor()
// 区分每一个页面的唯一id this.exitConfirm = initObjects?.option?.exitConfirm ?? false
uid: number; this.allowCopy = initObjects?.option?.allowCopy ?? true
// 当refreshKey改变时需要刷新页面,目前还没接入刷新 this.pageStorage = {}
refreshKey: number; }
// 为了区分,每个虚拟页面都会分配一个color,这个color会体现在tab上 // 页面使用的组件
color: string; component: Component
// 用于展示的title // 页面名
_title?: string; name: string
// 是否可见
visible: boolean
// 当前正在使用的文件夹
currentDirectories: VirtualDirectory[]
// 当前正在使用的文件
currentFiles: VirtualFile[]
// 页面数据存储
pageStorage: { [key: string]: any }
// 区分每一个页面的唯一id
id: number
// 当refreshKey改变时需要刷新页面,目前还没接入刷新
refreshKey: number
// 为了区分,每个虚拟页面都会分配一个color,这个color会体现在tab上
color: string
// 用于展示的title
_title?: string
// 关闭时是否需要确认(未实现)
exitConfirm: boolean
// 是否允许复制页面
allowCopy: boolean
get title() { get title() {
if (this._title) { if (this._title) {
return this._title; return this._title
} }
return this?.currentDirectorys[0]?.name ?? this?.currentFiles[0]?.name ?? "未命名标签"; return (
} this?.currentDirectories[0]?.name ??
this?.currentFiles[0]?.name ??
'未命名标签'
)
}
setTitle(title: string) { setTitle(title: string) {
this._title = title; this._title = title
} }
// 切换显示状态 // 切换显示状态
switchShow() { switchShow() {
this.visible = !this.visible; this.visible = !this.visible
} }
// 刷新,不过好像没有用得到的时候吧 // 刷新,不过好像没有用得到的时候吧
// 事实上也不应该用得到,因为更新应该是响应式的 // 事实上也不应该用得到,因为更新应该是响应式的
refresh() { refresh() {
this.refreshKey = Math.random(); this.refreshKey = Math.random()
} }
} }
export class VirtualPages { export class VirtualPages {
constructor() { constructor() {
this._views = reactive<VirtualPage[]>([]); this._views = reactive<VirtualPage[]>([])
} }
_views: VirtualPage[]; _views: VirtualPage[]
get length() { get length() {
return this._views.length; return this._views.length
} }
/** /**
* 查询一个页面位于_views中的次序 * 查询一个页面位于_views中的次序
* @param {any} id 要查询的页面的id * @param {any} id 要查询的页面的id
* @returns {any} * @returns {any}
*/ */
getIndex(id: number) { getIndex(id: number) {
for (let i in this._views) { for (let i in this._views) {
if (this._views[i].uid === id) { if (this._views[i].id === id) {
return parseInt(i); return parseInt(i)
} }
} }
return -1; return -1
} }
/** /**
* 根据id得到view * 根据id得到view
* @param {any} id 要查询的页面的id * @param {any} id 要查询的页面的id
*/ */
getView(id: number) { getView(id: number) {
for (let v of this._views) { for (let v of this._views) {
if (v.uid === id) { if (v.id === id) {
return v; return v
} }
} }
return undefined; return undefined
} }
/** /**
* 推入一个新的页面 * 推入一个新的页面
*/ */
push(component: any, currentDirectorys?: VirtualDirectory[], currentFiles?: VirtualFile[], index?: number) { push(
index ?? (index = this.length); component: Component,
this._views.splice( name: string,
index, currentDirectories?: VirtualDirectory[],
0, currentFiles?: VirtualFile[],
reactive<VirtualPage>( option?: ViewsPushOption
new VirtualPage(component, { ) {
directorys: [...(currentDirectorys ?? [])], const { index = this.length, VirtualPageOption } = option ?? {}
files: [...(currentFiles ?? [])] this._views.splice(
}) index,
) 0,
); reactive<VirtualPage>(
} new VirtualPage(component, {
/** name,
* 根据次序删除一个页面 directories: [...(currentDirectories ?? [])],
*/ files: [...(currentFiles ?? [])],
deleteView(index: number) { option: VirtualPageOption
this._views.splice(index, 1); })
} )
)
}
/**
* 根据次序删除一个页面
*/
deleteView(index: number) {
this._views.splice(index, 1)
}
/**
* 返回第一个使用此组件的页面
*/
find(component: Component): VirtualPage[]
/**
* 通过页面名查找
*/
find(name: string): VirtualPage[]
find(input: unknown) {
// console.log(this)
if (typeof input === 'string') {
return this._views.filter((v) => v.component.name === input)
} else {
return this._views.filter((v) => v.component === input)
}
}
} }
+339
View File
@@ -0,0 +1,339 @@
import { file, directory } from "@/types/type";
import { VirtualFileBase, VirtualDirectoryBase } from "./VirtualDirectory";
import useMiaoFetchApi from "@/hooks/useMiaoFetchApi";
import config from '@/config'
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 {
constructor(info: file, parent: VirtualDirectoryBase) {
super(info.name, info.size, info.stats, parent)
}
get url() {
return `${config.api.get}${this.parent.path}${this.name}`
}
async delete(): Promise<boolean> {
try {
const { response } = miaoFetchApi.delete(this, {
retry: 5
});
const id = (await response).eventId;
const eventResult = await pollEventUntilComplete(id);
if (eventResult.status === 'success' && this.parent) {
await this.parent.update(); // 操作成功后更新父目录
}
return eventResult.status === 'success';
} catch (error) {
console.error('删除文件失败:', error);
return false;
}
}
async rename(newName: string): Promise<boolean> {
if (!newName) {
return false;
}
try {
const { response } = miaoFetchApi.rename(this, newName, {
retry: 5
});
const id = (await response).eventId;
const eventResult = await pollEventUntilComplete(id);
if (eventResult.status === 'success' && this.parent) {
await this.parent.update(); // 操作成功后更新父目录
}
return eventResult.status === 'success';
} catch (error) {
console.error('重命名文件失败:', error);
return false;
}
}
async moveTo(targetDir: VirtualDirectoryBase): Promise<boolean> {
if (this.parent === targetDir) {
return false; // 已经在目标目录中
}
try {
const _from = this.parent;
const { response } = miaoFetchApi.cut(this, targetDir);
const id = (await response).eventId;
const eventResult = await pollEventUntilComplete(id);
const success = eventResult.status === 'success';
if (success) {
if (_from) await _from.update(); // 更新源目录
await targetDir.update(); // 更新目标目录
}
return success;
} catch (error) {
console.error('移动文件失败:', error);
return false;
}
}
async copyTo(targetDir: VirtualDirectoryBase): Promise<boolean> {
// 检查目标目录是否已存在同名文件
const existingFile = targetDir.files.find(file => file.name === this.name);
if (existingFile) {
// 存在同名文件,拒绝操作并显示提示
console.error(`目标目录已存在同名文件: ${this.name}`);
alert(`复制失败:目标目录已存在同名文件"${this.name}"`);
return false;
}
try {
const { response } = miaoFetchApi.copy(this, targetDir);
const id = (await response).eventId;
const eventResult = await pollEventUntilComplete(id);
const success = eventResult.status === 'success';
if (success) {
await targetDir.update(); // 更新目标目录
}
return success;
} catch (error) {
console.error('复制文件失败:', error);
return false;
}
}
}
export class VirtualRemoteDirectory extends VirtualDirectoryBase {
constructor(info: directory, parent?: VirtualDirectoryBase) {
super(info.name, info.stats, parent)
}
get url(): string {
return `${config.api.get}${this.path}`
}
updateContent(content: (file | directory)[]) {
this._isUpdated = true
const files = content.filter(item => item.type === 'file') as file[];
const directories = content.filter(item => item.type === 'directory') as directory[];
this.diffFiles(files);
this.diffDirectories(directories);
}
/**
* 文件diff算法,类似Vue的diff
* @param newFiles 新文件列表
*/
private diffFiles(newFiles: file[]) {
if (this.files.length === 0) {
this.files = newFiles.map(file => new VirtualRemoteFile(file, this));
return;
}
const oldFiles = [...this.files];
const newFilesList: VirtualRemoteFile[] = [];
const oldKeyMap = new Map<string, VirtualRemoteFile>();
oldFiles.forEach(file => oldKeyMap.set(file.name, file as VirtualRemoteFile));
for (let i = 0; i < newFiles.length; i++) {
const newFile = newFiles[i];
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));
}
}
this.files = newFilesList;
}
/**
* 目录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> {
const data: (file | directory)[] = await miaoFetchApi.get(this)
this.updateContent(data)
}
async delete(): Promise<boolean> {
try {
const { response } = miaoFetchApi.delete(this, {
retry: 5
});
const id = (await response).eventId;
const eventResult = await pollEventUntilComplete(id);
if (eventResult.status === 'success' && this.parent) {
await this.parent.update(); // 操作成功后更新父目录
}
return eventResult.status === 'success';
} catch (error) {
console.error('删除目录失败:', error);
return false;
}
}
async rename(newName: string): Promise<boolean> {
if (!newName) {
return false;
}
try {
const { response } = miaoFetchApi.rename(this, newName, {
retry: 5
});
const id = (await response).eventId;
const eventResult = await pollEventUntilComplete(id);
if (eventResult.status === 'success' && this.parent) {
await this.parent.update(); // 操作成功后更新父目录
}
return eventResult.status === 'success';
} catch (error) {
console.error('重命名目录失败:', error);
return false;
}
}
async mv(targetDir: VirtualDirectoryBase): Promise<boolean> {
if (targetDir === this || targetDir.getParents.includes(this)) {
return false;
}
if (this.parent === targetDir) {
return false; // 已经在目标目录中
}
try {
const _from = this.parent;
const { response } = miaoFetchApi.cut(this, targetDir);
const id = (await response).eventId;
const eventResult = await pollEventUntilComplete(id);
const success = eventResult.status === 'success';
if (success) {
if (_from) await _from.update(); // 更新源目录
await targetDir.update(); // 更新目标目录
}
return success;
} catch (error) {
console.error('移动目录失败:', error);
return false;
}
}
async copy(targetDir: VirtualDirectoryBase): Promise<boolean> {
if (targetDir === this || targetDir.getParents.includes(this)) {
return false;
}
// 检查目标目录是否已存在同名目录
const existingDir = targetDir.directories.find(dir => dir.name === this.name);
if (existingDir) {
// 存在同名目录,拒绝操作并显示提示
console.error(`目标位置已存在同名目录: ${this.name}`);
alert(`复制失败:目标位置已存在同名目录"${this.name}"`);
return false;
}
try {
const { response } = miaoFetchApi.copy(this, targetDir);
const id = (await response).eventId;
const eventResult = await pollEventUntilComplete(id);
const success = eventResult.status === 'success';
if (success) {
// 不需要更新源目录,因为复制不会改变源目录
await targetDir.update(); // 更新目标目录
}
return success;
} catch (error) {
console.error('复制目录失败:', error);
return false;
}
}
}
+145
View File
@@ -0,0 +1,145 @@
<template>
<n-card title="分享链接" class="share-card">
<miao-message-provider ref="messageProviderRef">
<n-scrollbar style="max-height: 60vh;">
<n-space vertical>
<!-- <n-text>扫描下方二维码访问不同设备上的应用</n-text>
<n-divider /> -->
<n-spin :show="loading">
<div class="grid-container">
<div v-for="(item, index) in shareLinks" :key="index" class="grid-item">
<n-card embedded class="qrcode-card">
<template #header>
<div class="card-header bg-zinc-100 p-3 rounded-md">
<n-text>{{ item.title.split('(')[0] }}</n-text>
<n-tooltip trigger="hover" placement="top">
<template #trigger>
<n-button quaternary circle size="small" @click="copyUrl(item.url)">
<template #icon>
<n-icon><copy-outline /></n-icon>
</template>
</n-button>
</template>
复制链接
</n-tooltip>
</div>
</template>
<div class="qrcode-container">
<n-qr-code :value="item.url" :size="200" error-correction-level="H"
style="box-sizing: content-box;" />
</div>
<template #footer>
<div @click="openUrl(item.url)" class="cursor-pointer hover:text-blue-500 hover:underline">
<n-ellipsis style="max-width: 100%" :tooltip="{ maxWidth: 300 }">
{{ item.url }}
</n-ellipsis>
</div>
</template>
</n-card>
</div>
</div>
</n-spin>
</n-space>
</n-scrollbar>
</miao-message-provider>
</n-card>
</template>
<script setup lang="ts">
import { CopyOutline } from '@vicons/ionicons5'
import { onMounted, ref, useTemplateRef } from 'vue'
import useServerAddresses from '@/hooks/useServerAddresses'
import MiaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
// 服务器地址服务
const serverAddressesService = useServerAddresses()
interface ShareLink {
title: string
url: string
}
// 加载状态
const loading = ref(true)
// 分享链接
const shareLinks = ref<ShareLink[]>([])
// 使用本地消息提示组件
const messageProviderRef = useTemplateRef<InstanceType<typeof MiaoMessageProvider>>('messageProviderRef')
// 组件挂载后获取地址列表
onMounted(async () => {
try {
await serverAddressesService.fetchAddresses()
shareLinks.value = serverAddressesService.addresses.value
} catch (error) {
// 使用本地的alertTip
messageProviderRef.value?.alertTip('获取分享链接失败', {
type: 'error',
timeout: 3000
})
console.error('获取分享链接失败:', error)
} finally {
loading.value = false
}
})
/**
* 复制URL到剪贴板
*/
const copyUrl = (url: string) => {
navigator.clipboard.writeText(url).then(
() => {
// 使用本地的alertTip
messageProviderRef.value?.alertTip('链接已复制到剪贴板', {
type: 'success',
timeout: 3000
})
},
(err) => {
console.error('复制失败:', err)
// 使用本地的alertTip
messageProviderRef.value?.alertTip('复制失败', {
type: 'error',
timeout: 3000
})
}
)
}
const openUrl = (url: string) => {
window.open(url, '_blank')
}
</script>
<style scoped>
.share-card {
min-width: 280px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 8px;
}
.qrcode-card {
text-align: center;
}
.qrcode-container {
display: flex;
justify-content: center;
padding: 8px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>
+79
View File
@@ -0,0 +1,79 @@
<script setup lang="ts">
const props = defineProps({
views: {
type: Object,
required: true
},
deleteView: {
type: Function,
required: true
}
})
</script>
<template>
<div class="view-container">
<div
class="view-container-item"
v-for="(view, index) of props.views._views"
:class="!view.visible ? 'view-container-item-hidden' : ''"
:key="view.id">
<component
:is="view.component"
v-model:current-directories="view.currentDirectories"
v-model:current-files="view.currentFiles"
:id="view.id"
:color="view.color"
:view="view"
:views="props.views"
@exit="props.deleteView(index)"></component>
</div>
</div>
</template>
<style lang="scss" scoped>
$controller-height: 25px;
.view-container {
position: relative;
display: flex;
height: calc(100% - $controller-height);
overflow-y: hidden;
overflow-x: overlay;
.view-container-item {
flex: 1;
height: 100%;
min-width: 180px;
opacity: 1;
transition:
flex 0.25s ease,
opacity 0.25s ease;
}
.view-container-item-hidden {
flex: 0;
min-width: 0px;
opacity: 0;
}
&::-webkit-scrollbar {
position: absolute;
width: 4px;
}
// 滚动条轨道
&::-webkit-scrollbar-track {
border-radius: 2px;
}
// 小滑块
&::-webkit-scrollbar-thumb {
background: #e0e0e0;
}
&::-webkit-scrollbar-thumb:hover {
background: #999999;
}
}
</style>
+233
View File
@@ -0,0 +1,233 @@
<script setup lang="ts">
import { NIcon, NScrollbar, NDropdown } from 'naive-ui'
import {
EyeOffOutline,
EyeOutline,
CloseOutline,
CopyOutline,
EllipsisVertical
} from '@vicons/ionicons5'
import type { DropdownMixedOption } from 'naive-ui/es/dropdown/src/interface'
import type { VirtualPages } from '@/class/VirtualPage'
import type VirtualDirectory from '@/class/VirtualDirectory'
import type { VirtualFile } from '@/class/VirtualDirectory'
const props = defineProps<{
views: VirtualPages
handleClickTitle: (index: number) => void
createView: (
component: any,
name: string,
currentDirectories?: VirtualDirectory[],
currentFiles?: VirtualFile[],
index?: number
) => void
deleteView: (index: number) => void
openMenuOption: DropdownMixedOption[]
handleMenuSelect: (key: string) => any
}>()
</script>
<template>
<div class="view-controller">
<n-scrollbar x-scrollable>
<div class="tabs-container">
<transition-group name="tab">
<div
class="tab"
v-for="(view, index) of props.views._views"
:key="view.id">
<span
class="tab-title"
:title="view.title"
@click="props.handleClickTitle(index)">
{{
view.title.length > 5 && 0
? `${view.title.substring(0, 5)}...`
: view.title
}}
</span>
<div class="tab-control">
<div
class="tab-point"
:style="{ backgroundColor: view.color }"></div>
<n-icon
class="tag-control-icon icon"
@click="view.switchShow()">
<EyeOutline v-show="view.visible" class="icon-inner" />
<EyeOffOutline
v-show="!view.visible"
class="icon-inner" />
</n-icon>
<n-icon
class="tag-control-icon icon"
@click="
props.createView(
view.component,
view.name,
view.currentDirectories,
view.currentFiles,
index + 1
)
"
v-if="view.allowCopy">
<CopyOutline />
</n-icon>
<n-icon
class="tag-control-icon icon"
@click="props.deleteView(index)">
<CloseOutline class="icon-inner" />
</n-icon>
</div>
</div>
</transition-group>
</div>
</n-scrollbar>
<div class="view-controller-menu">
<div class="view-controller-menu-item">
<n-dropdown
trigger="click"
:options="props.openMenuOption"
@select="props.handleMenuSelect">
<n-icon class="icon">
<EllipsisVertical class="icon-inner" />
</n-icon>
</n-dropdown>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
$controller-height: 25px;
$tag-width: 170px;
.view-controller {
display: flex;
height: $controller-height;
width: 100%;
background-color: #dadada;
.tabs-container {
display: flex;
margin-left: 5px;
flex: 1;
.tab {
position: relative;
width: $tag-width;
height: $controller-height;
margin: 0 5px;
display: flex;
border-radius: 5px 5px 0 0;
align-items: center;
.tab-title {
margin-left: 4px;
width: 90px;
user-select: none;
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
span {
user-select: none;
cursor: pointer;
}
}
.tab-point {
height: 5px;
aspect-ratio: 1;
border-radius: 66px;
margin-right: 5px;
}
.tab-control {
position: absolute;
right: 10px;
display: flex;
align-items: center;
.tab-control-icon:not(:last-child) {
margin-right: 5px;
}
}
&:not(:last-child)::after {
content: '';
height: calc($controller-height * 0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
right: 0px;
}
}
}
.view-controller-menu {
position: relative;
display: flex;
align-items: center;
padding: 0 5px;
.view-controller-menu-item {
height: 100%;
aspect-ratio: 1;
display: flex;
justify-content: center;
align-items: center;
}
&::before {
content: '';
height: calc($controller-height * 0.6);
width: 1px;
border-right: 1px solid black;
position: absolute;
left: 2px;
}
}
.icon {
height: calc($controller-height * 0.7);
width: calc($controller-height * 0.7);
border-radius: 5px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background-color: rgba(128, 128, 128, 0);
transition: background-color 0.3s;
&:hover {
background-color: rgba(128, 128, 128, 0.4);
}
}
}
// transition动画相关
.view-controller {
.tabs-container {
.tab-move,
.tab-enter-active {
transition: all 0.2s ease;
}
.tab-leave-active {
opacity: 0;
transition: none;
}
.tab-enter-from {
opacity: 0;
transform: translateY(-30px);
}
.tab-leave-active {
position: absolute;
}
}
}
</style>
+158
View File
@@ -0,0 +1,158 @@
<template>
<div class="miao-message-provider" ref="rootRef">
<slot></slot>
<transition-group
name="message-fade"
tag="div"
class="miao-message-provider-container">
<div
class="miao-message"
v-for="msg in messageList"
:key="msg.id"
:style="{ maxWidth: maxMessageWidth + 'px' }">
<n-icon class="miao-message-item" size="18" :color="msg.iconColor">
<component :is="msg.icon"></component>
</n-icon>
<div class="miao-message-item miao-message-item-content">
{{ msg.content }}
</div>
</div>
</transition-group>
</div>
</template>
<script setup lang="ts">
import generateId from '@/utils/generateId'
import { NIcon } from 'naive-ui'
import { computed, onMounted, ref, shallowReactive } from 'vue'
import {
InformationCircle,
CheckmarkCircle,
CloseCircle
} from '@vicons/ionicons5'
import { AlertTipMessageOptions } from '@/types/type'
interface Message {
icon: any
timer: any
iconColor: string
content: string
}
const iconMap = {
info: { icon: InformationCircle, color: 'blue' },
success: { icon: CheckmarkCircle, color: 'green' },
error: { icon: CloseCircle, color: 'red' }
}
const messageMap = shallowReactive<Record<string, Message | undefined>>({})
const rootRef = ref<HTMLDivElement>()
const maxMessageWidth = ref<number>(-1)
const messageList = computed(() => {
return Object.keys(messageMap).map((v) => ({
id: v,
...messageMap[v]
}))
})
const alertTip = (content: string, options?: AlertTipMessageOptions) => {
let timer: any
const { timeout = 0, type = 'info', id = generateId() } = options ?? {}
if (timeout > 0) {
messageMap[id]?.timer && clearTimeout(messageMap[id]?.timer)
timer = setTimeout(() => {
delete messageMap[id]
}, timeout)
}
messageMap[id] = {
icon: iconMap[type].icon,
iconColor: iconMap[type].color,
content,
timer
}
return (newContent: string, newOptions?: AlertTipMessageOptions) => {
newOptions = newOptions ?? {}
newOptions.id = id
alertTip(newContent, newOptions)
}
}
onMounted(async () => {
// const set = message('test', { timeout: 5000 })
// setTimeout(() => {
// set('miao', { type: 'success' })
// }, 1000);
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
maxMessageWidth.value = entry.contentRect.width * 0.8
}
})
if (rootRef.value) {
observer.observe(rootRef.value)
}
})
defineExpose({ alertTip })
</script>
<style scoped lang="scss">
.miao-message-provider {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
.miao-message-provider-container {
position: absolute;
width: 0;
height: 0;
left: 50%;
top: 30px;
display: flex;
flex-direction: column;
align-items: center;
overflow: visible;
z-index: 70582140539943419;
.miao-message {
display: flex;
align-items: center;
width: max-content;
background-color: rgb(249, 249, 249);
padding: 5px 10px;
background-color: rgb(255 255 255);
box-shadow:
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
border-radius: 5px;
user-select: none;
margin-top: 10px;
.miao-message-item {
margin: 0 5px;
}
.miao-message-item-content {
white-space: pre-wrap;
}
}
}
.message-fade-move,
.message-fade-enter-active,
.message-fade-leave-active {
transition: all 0.5s ease;
}
.message-fade-enter-from,
.message-fade-leave-to {
opacity: 0;
transform: translateX(30px);
}
.message-fade-leave-active {
position: absolute;
}
}
</style>
+44 -34
View File
@@ -1,26 +1,37 @@
<!-- 基于naiveui的下拉菜单实现的自定义下拉菜单 --> <!-- 基于naiveui的下拉菜单实现的自定义下拉菜单 -->
<!-- 支持长按和右键呼出下拉菜单 --> <!-- 支持长按和右键呼出下拉菜单 -->
<template> <template>
<div class="main" @contextmenu="handleContextMenu" @mouseleave="handleMainMouseLeave"> <div
<slot></slot> class="main"
</div> @contextmenu="handleContextMenu"
<n-dropdown placement="bottom-start" trigger="manual" :x="x" :y="y" :options="props.options" @mouseleave="handleMainMouseLeave">
:show="showContextMenu > 0" :on-clickoutside="onClickoutside" @select="handleSelect" @mouseleave=" <slot></slot>
showContextMenu -= 1" @mouseenter="showContextMenu += 1" /> </div>
<n-dropdown
placement="bottom-start"
trigger="manual"
:x="x"
:y="y"
:options="props.options"
:show="showContextMenu > 0"
:on-clickoutside="onClickoutside"
@select="handleSelect"
@mouseleave="showContextMenu -= 1"
@mouseenter="showContextMenu += 1" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NDropdown } from 'naive-ui/es/dropdown'; import { NDropdown } from 'naive-ui/es/dropdown'
import { DropdownOption } from 'naive-ui/es/dropdown'; import { DropdownOption } from 'naive-ui/es/dropdown'
import { nextTick, ref } from 'vue'; import { nextTick, ref } from 'vue'
const props = defineProps<{ const props = defineProps<{
options: DropdownOption[] options: DropdownOption[]
// 长按呼出右键菜单的时延,undefined时不生效 // 长按呼出右键菜单的时延,undefined时不生效
touchTimeOut?: number touchTimeOut?: number
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
select: [key: string] select: [key: string]
}>() }>()
const x = ref(0) const x = ref(0)
@@ -30,31 +41,31 @@ const showContextMenu = ref(0)
// let timer_touch: string | number | NodeJS.Timeout | undefined // let timer_touch: string | number | NodeJS.Timeout | undefined
const onClickoutside = async () => { const onClickoutside = async () => {
showContextMenu.value = 0 showContextMenu.value = 0
} }
const handleMainMouseLeave = async () => { const handleMainMouseLeave = async () => {
setTimeout(() => { setTimeout(() => {
showContextMenu.value -= 1 showContextMenu.value -= 1
}, 20) }, 20)
// nextTick(()=>{ // nextTick(()=>{
// showContextMenu.value -= 1 // showContextMenu.value -= 1
// }) // })
} }
const handleSelect = async (key: string) => { const handleSelect = async (key: string) => {
emit('select', key) emit('select', key)
showContextMenu.value = 0 showContextMenu.value = 0
} }
const handleContextMenu = (e: MouseEvent) => { const handleContextMenu = (e: MouseEvent) => {
e.preventDefault() e.preventDefault()
showContextMenu.value = 0 showContextMenu.value = 0
nextTick().then(() => { nextTick().then(() => {
showContextMenu.value += 1 showContextMenu.value += 1
x.value = e.clientX x.value = e.clientX
y.value = e.clientY y.value = e.clientY
}) })
} }
// const handleTouchStart = (e: TouchEvent) => { // const handleTouchStart = (e: TouchEvent) => {
@@ -76,13 +87,12 @@ const handleContextMenu = (e: MouseEvent) => {
// } // }
// } // }
</script> </script>
<style> <style>
.main { .main {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
} }
</style> </style>
@@ -1,348 +0,0 @@
<template>
<div class="miao-item" draggable="true" ref="itemRef" @dragstart="handleDragStart">
<miaoContextMenu :options="dropDownOptions" :touch-time-out="500" @select="handleDropdownSelect">
<div class="item-main" :class="selected ? 'item-main-selected' : ''" ref="mainRef">
<div class="item-main-front" :style="{ backgroundColor: props.color }"></div>
<Icon class="item-main-icon" v-show="clientWidth > 250"></Icon>
<div class="item-main-info">
<div class="item-main-info-name">
<Icon class="item-main-info-name-icon" v-if="clientWidth <= 250"></Icon>
<n-ellipsis style="max-width: 80%;">{{ name }}</n-ellipsis>
</div>
<div class="item-main-info-date" v-if="time">
{{ time }}
</div>
</div>
<!-- 菜单现在通过右键/长按呼出 -->
<!-- <n-dropdown trigger="click" :options="dropDownOptions" @select="handleDropdownSelect">
<div class="item-main-option" @click="(e) => e.stopPropagation()">
<EllipsisVertical class="item-main-option-icon" />
</div>
</n-dropdown> -->
</div>
</miaoContextMenu>
</div>
</template>
<script setup lang="ts">
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import miaoContextMenu from '../miaoContextMenu.vue'
import {
DocumentText,
FileTray,
Help,
Image,
LogoChrome,
LogoCss3,
LogoJavascript,
LogoVue,
PlayCircle,
Disc
} from '@vicons/ionicons5'
import { computed, onMounted, ref } from 'vue'
import dateFormatter from '@/hooks/dateFormatter'
import { NEllipsis } from 'naive-ui'
import usePluginCenter from '@/hooks/usePluginCenter'
// import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
// const miaoFetchApi = useMiaoFetchApi()
const props = defineProps<{
item: VirtualDirectory | VirtualFile
name?: string
color: string
selectedItem: (VirtualDirectory | VirtualFile)[]
}>()
const emit = defineEmits<{
download: []
delete: []
rename: []
dragStart: [event: DragEvent, item: VirtualDirectory | VirtualFile]
onSelected: []
}>()
const pluginCenter = usePluginCenter()
const itemType = ref<'file' | 'directory'>(props.item.type)
const mainRef = ref<HTMLDivElement>()
// 使用ResizeObserver实现
const clientWidth = ref<number>(666)
const name = props.name ?? props.item.name
const selected = computed(() => {
return props.selectedItem.includes(props.item)
})
const time = computed(() => {
return dateFormatter(
new Date(props.item.stats.mtimeMs),
'yyyy-MM-dd hh:mm:ss'
)
})
const dropDownOptions = computed(() => {
const items = [...props.selectedItem]
if (!selected.value) {
items.push(props.item)
}
const vDirs = items.filter(v => v.type === 'directory')
const vFiles = items.filter(v => v.type === 'file')
const options = [
{
label: '重命名',
key: 'rename'
},
{
label: '删除',
key: 'delete'
},
]
if (itemType.value === 'file') {
options.push(
...[
{
label: '下载',
key: 'download'
}
]
)
}
if (selected.value === false) {
options.push(
...[
{
label: '多选:选择',
key: 'select'
}
]
)
} else {
options.push(
...[
{
label: '多选:取消',
key: 'select'
}
]
)
}
options.push(...pluginCenter.getUsablePlugin(vDirs, vFiles).map(v => ({
label: `插件:${v.name}`,
key: `plugin:${v.key}`
})))
return options
})
// 是不是可以直接在这里面实现?
const handleDropdownSelect = (key: string) => {
if (key === 'rename') {
emit('rename')
} else if (key === 'download') {
emit('download')
} else if (key === 'delete') {
emit('delete')
} else if (key === 'select') {
emit('onSelected')
} else if (key.startsWith('plugin:')) {
const items = [...props.selectedItem]
if (!selected.value) {
items.push(props.item)
}
const vDirs = items.filter(v => v.type === 'directory')
const vFiles = items.filter(v => v.type === 'file')
const plugin = key.split(':')[1]
pluginCenter.usePlugin(plugin, vDirs, vFiles)
}
}
const Icon = computed(() => {
try {
if (props.item.type === 'directory') {
return FileTray
} else if (props.item.type === 'file') {
const aft = props.item.name.split('.').at(-1)?.toLowerCase()
if (!aft) {
throw 1
} else if (
['txt', 'doc', 'docs', 'docx', 'xls', 'xlsx'].includes(aft)
) {
return DocumentText
} else if (
['mp3', 'wav', 'ape', 'acc', 'ogg', 'flac'].includes(aft)
) {
return Disc
} else if (
[
'.xbm',
'.tif',
'pjp',
'.svgz',
'jpg',
'jpeg',
'ico',
'tiff',
'.gif',
'svg',
'.jfif',
'.webp',
'.png',
'.bmp',
'pjpeg',
'.avif'
].includes(aft)
) {
return Image
} else if (['mp4', 'm2v', 'mkv'].includes(aft)) {
return PlayCircle
} else if (['js', 'jsx', 'ts', 'tsx'].includes(aft)) {
return LogoJavascript
} else if (['html'].includes(aft)) {
return LogoChrome
} else if (['css', 'scss', 'less'].includes(aft)) {
return LogoCss3
} else if (['vue'].includes(aft)) {
return LogoVue
}
return DocumentText
}
} catch {
return Help
}
})
const itemRef = ref()
const handleDragStart = (event: DragEvent) => {
emit('dragStart', event, props.item)
// dataBus.set('dragData', [props.item])
}
onMounted(() => {
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
clientWidth.value = entry.contentRect.width;
}
});
if (mainRef.value) {
observer.observe(mainRef.value);
}
})
</script>
<style lang="scss" scoped>
.miao-item {
cursor: pointer;
width: calc(100% - 20px);
min-width: 170px;
// aspect-ratio: 66/9;
height: 50px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
margin: 10px 10px;
.item-main {
position: relative;
width: 100%;
min-width: 200px;
height: 95%;
background-color: rgb(255, 255, 255);
border-radius: 7px;
display: flex;
align-items: center;
transition: background-color 0.3s ease;
.item-main-front {
height: 100%;
min-width: 10px;
border-radius: 7px 0 0 7px;
background-color: rgb(0, 0, 0);
}
.item-main-icon {
min-width: 30px;
margin-left: 10px;
height: 30px;
width: 30px;
color: #00403e;
transition: color 0.15s ease-out;
}
.item-main-info {
margin-left: 10px;
height: 100%;
width: calc( 100% - 10px );
display: flex;
flex-direction: column;
&-name {
position: relative;
user-select: none;
display: flex;
align-items: center;
margin-top: 3px;
&-icon {
color: #00403e;
width: 20px;
margin-right: 5px;
transition: color 0.15s ease-out;
}
}
&-date {
user-select: none;
font-size: small;
}
}
.item-main-option {
height: 100%;
width: 20px;
position: absolute;
right: 0;
border-radius: 0 7px 7px 0;
background-color: rgba(0, 0, 0, 0);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: background-color 0.3s;
.item-main-option-icon {
color: #000;
transition: color 0.15s;
}
&:hover,
&:active {
background-color: #8b8b8b;
.item-main-option-icon {
color: #fff;
}
}
}
&:hover,
&:active {
background-color: #d2d2d2;
}
}
.item-main-selected {
background-color: rgba(0, 128, 207, 0.32);
&:hover {
background-color: rgba(0, 128, 207, 0.64);
;
}
}
}
</style>
+47
View File
@@ -0,0 +1,47 @@
<template>
<div ref="divRef" @dragstart="onDs" :draggable="isDraggable ?? true">
<slot></slot>
</div>
</template>
<script setup lang="ts">
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory';
import useDataBus from '@/hooks/useDataBus';
import { onMounted, ref } from 'vue'
// import Sortable from 'sortablejs'
const props = defineProps<{
isDraggable?: boolean,
bindVirtualFiles?: VirtualFile[],
bindVirtualDirectories?: VirtualDirectory[],
}>()
const emit = defineEmits<{
onDragStart: [e: DragEvent]
}>()
const divRef = ref<HTMLDivElement>()
const onDs = (e: DragEvent) => {
emit('onDragStart', e)
const dataBus = useDataBus()
props.bindVirtualFiles && dataBus.set(
'dragData_vFiles',
props.bindVirtualFiles
)
props.bindVirtualDirectories && dataBus.set(
'dragData_vDirectory',
props.bindVirtualDirectories
)
}
onMounted(() => {
divRef?.value?.addEventListener('dragstart', function (ev) {
var img = new Image()
img.src =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' %3E%3Cpath /%3E%3C/svg%3E"
ev?.dataTransfer?.setDragImage(img, 0, 0)
})
})
</script>
<style scoped></style>
+89 -64
View File
@@ -1,12 +1,12 @@
<template> <template>
<div <div
class="container" class="miao-drop-container"
@dragover="handleDragOver" @dragover="handleDragOver"
@dragleave="handleDragLeave" @dragleave="handleDragLeave"
@drop="handleDrop"> @drop="handleDrop">
<slot></slot> <slot></slot>
<miao-mask v-model:show="showMask"></miao-mask> <miao-mask v-model:show="showMask"></miao-mask>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@@ -16,91 +16,116 @@ import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import config from '@/config' import config from '@/config'
import useDataBus from '@/hooks/useDataBus' import useDataBus from '@/hooks/useDataBus'
const props = defineProps({
disable: {
type: Boolean,
default: false
}
})
const dataBus = useDataBus() const dataBus = useDataBus()
const { uploadSizeLimit } = config const { uploadSizeLimit } = config
const emit = defineEmits<{ const emit = defineEmits<{
onFiles: [files: File[]] onFiles: [files: File[]]
onVirtualFiles: [vFiles: VirtualFile[]] onVirtualFiles: [vFiles: VirtualFile[]]
onVirtualDirectory: [vDirectory: VirtualDirectory[]] onVirtualDirectory: [vDirectory: VirtualDirectory[]]
}>() }>()
const showMask = ref<number>(0) const showMask = ref<number>(0)
// File類型的數據可能爲文件夾,通過FileReader進行判斷 // File類型的數據可能爲文件夾,通過FileReader進行判斷
const isFile = (file: File) => const isFile = (file: File) =>
new Promise((resolve) => { new Promise((resolve) => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = () => { reader.onload = () => {
resolve(true) resolve(true)
reader.abort() reader.abort()
} }
reader.onerror = () => { reader.onerror = () => {
resolve(false) resolve(false)
reader.abort() reader.abort()
} }
reader.readAsText(file) reader.readAsText(file)
}) })
/** /**
* 只保留文件,移去文件夹 * 只保留文件,移去文件夹
* @param {File[]} files * @param {File[]} files
*/ */
const removeDirectory = async (files: File[]) => { const removeDirectory = async (files: File[]) => {
let index = 0 let index = 0
while (index < files.length) { while (index < files.length) {
const currentFile = files[index] const currentFile = files[index]
const _isFile = await isFile(currentFile) const _isFile = await isFile(currentFile)
if (_isFile) { if (_isFile) {
index += 1 index += 1
continue continue
} else { } else {
files.splice(index, 1) files.splice(index, 1)
continue continue
} }
} }
}
const check = (event: DragEvent) => {
return (
[
...(event.dataTransfer?.types?.filter((v) => v === 'Files') ?? []),
...(dataBus.get('dragData_vFiles') ?? []),
...(dataBus.get('dragData_vDirectory') ?? [])
].length !== 0
)
} }
const handleDragOver = async (event: DragEvent) => { const handleDragOver = async (event: DragEvent) => {
showMask.value += 1 if (props.disable || check(event) === false) {
event.preventDefault() return
}
event.preventDefault()
if (showMask.value === 1) {
return
}
showMask.value = 1
} }
const handleDragLeave = async () => { const handleDragLeave = async () => {
showMask.value = 0 showMask.value = 0
} }
const handleDrop = async (event: DragEvent) => { const handleDrop = async (event: DragEvent) => {
showMask.value = 0 if (props.disable || check(event) === false) {
event.preventDefault() return
event.stopPropagation() }
// todo: uploadSize的限制不應該在這裏 showMask.value = 0
const files = [...(event.dataTransfer?.files ?? [])].filter( event.preventDefault()
(file) => file.size < uploadSizeLimit event.stopPropagation()
) // todo: uploadSize的限制不應該在這裏
await removeDirectory(files) const files = [...(event.dataTransfer?.files ?? [])].filter(
const virtualFiles: VirtualFile[] = dataBus.pop('dragData_vFiles') (file) => file.size < uploadSizeLimit
const virtualDirectory: VirtualDirectory[] = dataBus.pop( )
'dragData_vDirectory' await removeDirectory(files)
) const virtualFiles: VirtualFile[] = dataBus.pop('dragData_vFiles')
// console.log({ virtualDirectory, virtualFiles }) const virtualDirectory: VirtualDirectory[] = dataBus.pop(
if (files && files.length) { 'dragData_vDirectory'
emit('onFiles', files) )
} // console.log({ virtualDirectory, virtualFiles })
if (virtualFiles && virtualFiles.length) { if (files && files.length) {
emit('onVirtualFiles', virtualFiles) emit('onFiles', files)
} }
if (virtualDirectory && virtualDirectory.length) { if (virtualFiles && virtualFiles.length) {
emit('onVirtualDirectory', virtualDirectory) emit('onVirtualFiles', virtualFiles)
} }
if (virtualDirectory && virtualDirectory.length) {
emit('onVirtualDirectory', virtualDirectory)
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.container { .miao-drop-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
} }
</style> </style>
+132
View File
@@ -0,0 +1,132 @@
<!-- 只适合用于渲染过程较为复杂的组件,不然可能得不偿失
这么泛用的组件,性能差点也能理解 -->
<template>
<div
class="miao-lazy"
ref="miaoLazyRoot"
:style="
!show
? {
minHeight:
_autoHeight !== undefined ? _autoHeight : props.minHeight,
minWidth,
margin
}
: {}
">
<slot v-if="show" />
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
const props = defineProps({
// disable: {
// type: Boolean,
// default: false,
// },
disableLazy: {
type: Boolean,
default: false
},
// 仅在内容未加载时生效,用于占位
minHeight: {
type: String,
default: 'none'
},
minWidth: {
type: String,
default: 'none'
},
margin: {
type: String,
default: 'none'
},
autoHeight: {
type: Boolean,
default: false
}
})
const miaoLazyRoot = ref<HTMLDivElement>()
const _autoHeight = ref<string>()
const show = ref<boolean>(props.disableLazy)
const isVisible = (dom: HTMLDivElement) => {
const offset = dom.getBoundingClientRect().top
return offset > 0 && offset < 2000
}
const { showSlot, hideSlot, clearTimer } = (() => {
let timer: any
let timerInterval: any
const clearTimer = () => {
timer && clearTimeout(timer)
timerInterval && clearInterval(timerInterval)
timer = undefined
timerInterval = undefined
}
const hideSlot = () => {
clearTimer()
if (props.autoHeight) {
_autoHeight.value = `${miaoLazyRoot.value?.clientHeight}px`
}
show.value = false
}
const showSlot = () => {
if (show.value === true) {
return
}
clearTimer()
// timer = setTimeout(() => {
if (!miaoLazyRoot.value || isVisible(miaoLazyRoot.value) === false) {
return
}
show.value = true
timerInterval = setInterval(() => {
if (show.value === false || miaoLazyRoot.value === undefined) {
return
}
if (isVisible(miaoLazyRoot.value) === false) {
hideSlot()
}
}, 1000)
// if (props.autoHeight) {
// nextTick(() => {
// console.log(miaoLazyRoot.value?.clientHeight)
// _autoHeight.value = `${miaoLazyRoot.value?.clientHeight}px`
// })
// }
}
return { showSlot, hideSlot, clearTimer }
})()
onMounted(() => {
if (show.value === false) {
let options = {
threshold: 0
}
let observer: IntersectionObserver | undefined = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
showSlot()
// miaoLazyRoot.value && observer?.unobserve(miaoLazyRoot.value)
// observer = undefined
} else {
hideSlot()
}
},
options
)
miaoLazyRoot.value && observer.observe(miaoLazyRoot.value)
}
})
onBeforeUnmount(() => {
clearTimer()
})
</script>
+34 -32
View File
@@ -1,55 +1,57 @@
<template> <template>
<transition name="transition"> <transition name="transition">
<div class="mask-body" ref="maskBodyRef" v-show="showMask"> <div class="mask-body" ref="maskBodyRef" v-show="showMask">
<slot></slot> <slot></slot>
</div> </div>
</transition> </transition>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue'
const maskBodyRef = ref<HTMLDivElement>() const maskBodyRef = ref<HTMLDivElement>()
const showMask = defineModel<boolean | number>('show', { default: false, required: true }) const showMask = defineModel<boolean | number>('show', {
default: false,
required: true
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mask-body { .mask-body {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
z-index: 7058214053994319; z-index: 7058214053994319;
// &-container { // &-container {
// } // }
&-show { &-show {
display: flex; display: flex;
transition: opacity 0.3s 0.01s; transition: opacity 0.3s 0.01s;
opacity: 1; opacity: 1;
} }
&-hide { &-hide {
opacity: 0; opacity: 0;
transition: display 0.01s 0.3s; transition: display 0.01s 0.3s;
display: none; display: none;
} }
} }
.transition-enter-active, .transition-enter-active,
.transition-leave-active { .transition-leave-active {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
} }
.transition-enter-from, .transition-enter-from,
.transition-leave-to { .transition-leave-to {
opacity: 0; opacity: 0;
} }
</style> </style>
+79 -60
View File
@@ -1,83 +1,102 @@
<template> <template>
<miao-mask v-model:show="showMask"> <miao-mask v-model:show="showMask">
<n-card class="popup-card" :title="popupConfig?.title" size="medium"> <n-card class="popup-card" :title="popupConfig?.title" size="medium">
{{ popupConfig?.content }} {{ popupConfig?.content }}
<n-input class="popup-input" v-model:value="inputValue" type="text" v-bind="popupConfig?.inputProps" <n-input
v-if="popupConfig?.inputProps" /> class="popup-input"
<div class="popup-button-container"> v-model:value="inputValue"
<n-button class="popup-button-container-button" v-for="option in popupConfig?.options" :key="option.label" type="text"
:type="option.type" @click="_resolve && _resolve(option.key)">{{ option.label }}</n-button> v-bind="popupConfig?.inputProps"
</div> passively-activated
</n-card> autofocus
</miao-mask> v-if="popupConfig?.inputProps" />
<div class="popup-button-container">
<n-button
class="popup-button-container-button"
v-for="option in popupConfig?.options"
:key="option.label"
:type="option.type"
@click="_resolve && _resolve(option.key)">
{{ option.label }}
</n-button>
</div>
</n-card>
</miao-mask>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, nextTick } from "vue"; import { ref, nextTick } from 'vue'
import { NCard, NInput, NButton } from "naive-ui"; import { NCard, NInput, NButton } from 'naive-ui'
import miaoMask from "./miaoMask.vue"; import miaoMask from './miaoMask.vue'
import { InputProps } from "naive-ui"; import { InputProps } from 'naive-ui'
const showMask = ref(false); const showMask = ref(false)
const inputValue = ref<string>(""); const inputValue = ref<string>('')
const _resolve = ref() const _resolve = ref()
type popupConfigType = { type popupConfigType = {
title: string title: string
content?: string content?: string
inputValue?: string, inputValue?: string
inputProps?: InputProps inputProps?: InputProps
options: { options: {
label: string label: string
key: unknown key: unknown
type: 'default' | 'tertiary' | 'primary' | 'success' | 'info' | 'warning' | 'error' type:
}[] | 'default'
| 'tertiary'
| 'primary'
| 'success'
| 'info'
| 'warning'
| 'error'
}[]
} }
const popupConfig = ref<popupConfigType>() const popupConfig = ref<popupConfigType>()
const popup = async (config: popupConfigType) => { const popup = async (config: popupConfigType) => {
popupConfig.value = config popupConfig.value = config
inputValue.value = config.inputValue ?? '' inputValue.value = config.inputValue ?? ''
showMask.value = true showMask.value = true
const key = await new Promise((resolve: (key: unknown) => unknown) => { const key = await new Promise((resolve: (key: unknown) => unknown) => {
_resolve.value = resolve _resolve.value = resolve
}) })
const text = inputValue.value const text = inputValue.value
showMask.value = false; showMask.value = false
nextTick(() => { nextTick(() => {
inputValue.value = '' inputValue.value = ''
_resolve.value = undefined _resolve.value = undefined
}) })
return { return {
text, text,
key key
}; }
}; }
defineExpose({ popup }); defineExpose({ popup })
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.popup-card { .popup-card {
width: 80%; width: 80%;
.popup-input { .popup-input {
margin-bottom: 10px; margin-bottom: 10px;
} }
.popup-button-container { .popup-button-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;
.popup-button-container-button { .popup-button-container-button {
margin-top: 3px; margin-top: 3px;
&:not(:last-child) { &:not(:last-child) {
margin-right: 5px; margin-right: 5px;
}
} }
} }
}
} }
</style> </style>
+19 -13
View File
@@ -1,16 +1,22 @@
const baseUrl = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : ''); const baseUrl =
window.location.protocol +
'//' +
window.location.hostname +
(window.location.port ? ':' + window.location.port : '')
export default { export default {
baseUrl, baseUrl,
uploadSizeLimit: 1024 * 1024 * 100, uploadSizeLimit: 1024 * 1024 * 100,
api: { api: {
get: '/get', get: '/get',
upload: '/upload', upload: '/upload',
delete: '/delete', delete: '/delete',
query: '/query', query: '/query',
rename: '/rename', rename: '/rename',
mkdir: '/mkdir', mkdir: '/mkdir',
cut: '/cut' cut: '/cut',
}, copy: '/copy',
enableUpload: true addresses: '/addresses'
},
enableUpload: true
} }
-20
View File
@@ -1,20 +0,0 @@
export default function dateFormatter(date: Date, fmt: string) {
var o:any = {
"M+": date.getMonth() + 1, //月份
"d+": date.getDate(), //日
"h+": date.getHours(), //小时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (var k in o) {
if (new RegExp("(" + k + ")").test(fmt)) {
fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
}
}
return fmt;
}
-109
View File
@@ -1,109 +0,0 @@
import config from "@/config";
const { uploadSizeLimit } = config;
export type dropHandlerHooks = {
/**
* 仅返回小于config.ts中大小限制的文件,不会返回文件夹
*/
onDropFiles?: (files: File[]) => void;
/**
* 当拖动时
*/
onOver?: () => void;
/**
* 当进入时
*/
onEnter?: () => void;
/**
* 当离开时
*/
onLeave?: () => void;
/**
* 离开或是放下时都会触发
* @returns {any}
*/
onEnd?: () => void;
};
const isFile = (file: File) =>
new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => {
resolve(true);
reader.abort();
};
reader.onerror = () => {
resolve(false);
reader.abort();
};
reader.readAsText(file);
});
/**
* 只保留文件,移去文件夹
* @param {File[]} files
*/
const filesFilter = async (files: File[]) => {
let index = 0;
while (index < files.length) {
const currentFile = files[index];
const _isFile = await isFile(currentFile);
if (_isFile) {
index += 1;
continue;
} else {
files.splice(index, 1);
continue;
}
}
};
export default function dropHandler(dom: HTMLDivElement, hooks?: dropHandlerHooks) {
dom.addEventListener(
"dragenter",
function (e) {
e.preventDefault();
e.stopPropagation();
hooks?.onEnter && hooks?.onEnter();
},
false
);
dom.addEventListener(
"dragover",
function (e) {
e.preventDefault();
e.stopPropagation();
hooks?.onOver && hooks?.onOver();
},
false
);
dom.addEventListener(
"dragleave",
function (e) {
e.preventDefault();
e.stopPropagation();
hooks?.onLeave && hooks?.onLeave();
hooks?.onEnd && hooks.onEnd();
},
false
);
dom.addEventListener(
"drop",
async function (e) {
// console.log(e, e.dataTransfer?.types);
e.preventDefault();
e.stopPropagation();
const files = [...(e.dataTransfer?.files ?? [])].filter(file => file.size < uploadSizeLimit);
await filesFilter(files);
hooks?.onDropFiles && hooks.onDropFiles(files);
hooks?.onEnd && hooks.onEnd();
},
false
);
}
-16
View File
@@ -1,16 +0,0 @@
const idList: number[] = [];
/**
* 生成一个唯一的id
* @param {any} length 生成id的长度
* @returns {number}
*/
export default function generateId(length?: number) {
length || (length = 16);
let id;
while (!id || idList.includes(id)) {
id = Math.ceil(Math.random() * 10 ** 16);
}
idList.push(id)
return id
}
+4 -99
View File
@@ -1,101 +1,6 @@
import MiaoDirectory from '@/views/miaoDirectory.vue' import { alertTipType } from '@/types/type'
import usePluginCenter from './usePluginCenter' import initPlugin from './init/initPlugin'
export default function init() { export default async function init(globalAlertTip: alertTipType) {
const pluginCenter = usePluginCenter() await initPlugin(globalAlertTip)
pluginCenter.registerComponent(
'miaoDirectory',
'文件夹',
async () => MiaoDirectory,
(vDirs, vFiles) => {
if (vFiles.length) {
return false
}
return vDirs.length === 1
}
)
pluginCenter.registerComponent(
'miaoVideoPlayer',
'视频',
async () => (await import('@/views/miaoVideoPlayer.vue')).default,
(vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length === 0) {
return false
}
for (let i of vFiles) {
for (let s of ['mp4', 'm3u8', 'hls']) {
if (i.name.endsWith(s)) {
return true
}
}
}
return false
}
)
pluginCenter.registerComponent(
'miaoIframe',
'框架',
async () => (await import('@/views/miaoIframe.vue')).default,
(vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('html')
}
)
pluginCenter.registerComponent(
'miaoPDF',
'PDF浏览',
async () => (await import('@/views/miaoPDF.vue')).default,
(vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('pdf')
}
)
pluginCenter.registerComponent(
'miaoCodeMirror',
'代码编辑',
async () => (await import('@/views/miaoCodeMirror.vue')).default,
(vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
for (let end of [
'js',
'ts',
'jsx',
'tsx',
'py',
'html',
'css',
'scss',
'go',
'json',
'md',
'yaml',
'txt',
'xml',
'vue',
]) {
if (vFiles[0].name.endsWith(end)) {
return true
}
}
return false
}
)
} }
+32
View File
@@ -0,0 +1,32 @@
import usePluginCenter, { initPluginCenter } from '@/hooks/usePluginCenter'
import { Plugin, registerComponentOption } from '@/class/PluginCenter'
import { alertTipType, PluginExportType } from '@/types/type'
const initPlugin = async (globalAlertTip: alertTipType) => {
initPluginCenter(globalAlertTip)
const modulesFiles = import.meta.glob('@/plugins/**/export*.ts', {
eager: true
})
const pluginCenter = usePluginCenter()
for (let pluginConfig of Object.values(modulesFiles)) {
try {
const _config = pluginConfig as {
default: registerComponentOption | Plugin
type: PluginExportType
key?: string
}
const { type, key = `${Math.random()}` } = _config
if (type === PluginExportType.component) {
pluginCenter.registerComponent(
_config.default as registerComponentOption
)
} else if (type === PluginExportType.default) {
pluginCenter.register(key as string, _config.default as Plugin)
}
} catch (error) {
console.error(`插件 ${pluginConfig} 初始化失败`, error)
}
}
}
export default initPlugin
-47
View File
@@ -1,47 +0,0 @@
export const filePicker = (() => {
let _resolve: ((files: File[]) => void) | undefined;
return (option?: { accept?: "string"; multiple: boolean }) => {
if (_resolve) {
_resolve([]);
_resolve = undefined;
}
return new Promise<File[]>(resolve => {
const inpEle = document.createElement("input");
inpEle.id = `__file_${Math.ceil(Math.random() * 100000000)}`;
inpEle.type = "file";
inpEle.style.display = "none";
_resolve = resolve;
// 文件类型限制
// inpEle.accept="image/*"
option?.accept && (inpEle.accept = option?.accept);
// 多选限制
option?.multiple && (inpEle.multiple = option?.multiple);
// inpEle.addEventListener("change", event => fn.call(inpEle, event, inpEle.files), { once: true });
inpEle.addEventListener(
"change",
() => {
if (!inpEle.files) {
return;
}
const files: File[] = [];
let i = 0;
while (i < inpEle.files?.length) {
const _file = inpEle?.files?.item(i);
if (_file !== null) {
files.push(_file);
}
i += 1;
}
resolve(files);
_resolve = undefined
},
{ once: true }
);
inpEle.click();
});
};
})();
export default {
filePicker
};
+21 -17
View File
@@ -1,22 +1,26 @@
import { reactive } from "vue"; import { reactive } from 'vue'
let dataStorage: { let dataStorage: {
[key: string | number] : any [key: string | number]: any
}; }
export default function useDataBus() { export default function useDataBus() {
if(dataStorage === undefined) { if (dataStorage === undefined) {
dataStorage = reactive({}) dataStorage = reactive({})
} }
return { return {
set(key: string | number, value: any) { set(key: string | number, value: any) {
dataStorage[key] = value; dataStorage[key] = value
return value; return value
}, },
pop(key: string | number) { pop(key: string | number) {
const value = dataStorage[key]; const value = dataStorage[key]
dataStorage[key] = undefined; dataStorage[key] = undefined
return value; return value
} },
}; get(key: string | number) {
const value = dataStorage[key]
return value
}
}
} }
+301 -255
View File
@@ -1,287 +1,333 @@
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory' import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import { import {
getOption, getOption,
getResponse, getResponse,
miaoFetchConfig, miaoFetchConfig,
deleteOption, deleteOption,
uploadOption, uploadOption,
queryOption, queryOption,
renameOption, renameOption,
cutOption, cutOption,
mkdirOption mkdirOption,
getFileOption,
copyOption
} from '@/types/fetch' } from '@/types/fetch'
import Config from '@/config' import Config from '@/config'
import axios, { AxiosRequestConfig } from 'axios' import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
export const useMiaoFetch = (option?: miaoFetchConfig) => { export const useMiaoFetch = (option?: miaoFetchConfig) => {
const retry = option?.retry ?? 0 const retry = option?.retry ?? 0
const controller = new AbortController() const controller = new AbortController()
let flag = true let flag = true
return { return {
miaoFetch: async (config: AxiosRequestConfig<any>) => { miaoFetch: async (
let counter = 0 config: AxiosRequestConfig<any>
let response: any ): Promise<AxiosResponse<any, any>> => {
let errList = [] let counter = 0
while (flag && counter <= retry) { let response: any
try { let errList = []
response = await axios({ while (flag && counter <= retry) {
...config, try {
signal: controller.signal response = await axios({
}) ...config,
return response signal: controller.signal
} catch (_e) { })
errList.push(_e) return response
counter += 1 } catch (_e) {
} errList.push(_e)
counter += 1
} }
throw errList }
}, throw errList
/** },
* 强制终止miaoFetch /**
*/ * 强制终止miaoFetch
abortMiaoFetch: () => { */
flag = false abortMiaoFetch: () => {
controller.abort() flag = false
} controller.abort()
} }
}
} }
const miaoFetchApi = { const miaoFetchApi = {
/** /**
* 获取对应文件夹下的数据 * 获取对应文件夹下的数据
*/ */
async get( async get(
vDirectory: VirtualDirectory, vDirectory: VirtualDirectory,
option?: getOption option?: getOption
): Promise<getResponse> { ): Promise<getResponse> {
const { baseUrl, api } = Config const { baseUrl, api } = Config
const url = `${baseUrl}${api.get}${vDirectory.path}` const url = `${baseUrl}${api.get}${vDirectory.path}`
const retry = option?.retry ?? 0 const retry = option?.retry ?? 0
const { miaoFetch } = useMiaoFetch({ const { miaoFetch } = useMiaoFetch({
retry retry
}) })
const response = await miaoFetch({ const response = await miaoFetch({
url url
}) })
return response.data return response.data
}, },
/** /**
* 获取对应文件夹下的数据 * 获取对应文件下的数据
*/ */
async getFile( getFile(vFile: VirtualFile, option?: getFileOption) {
vFile: VirtualFile, // const { baseUrl, api } = Config
option?: getOption const url = vFile.url
): Promise<getResponse> { const retry = option?.retry ?? 0
// const { baseUrl, api } = Config const axiosOption = option?.axiosOption ?? {}
const url = vFile.url
const retry = option?.retry ?? 0
const axiosOption = option?.axiosOption ?? {}
const { miaoFetch } = useMiaoFetch({ const { miaoFetch, abortMiaoFetch: abort } = useMiaoFetch({
retry retry
}) })
const response = await miaoFetch({ const response = miaoFetch({
url, ...axiosOption url,
}) responseType: 'arraybuffer',
return response.data onDownloadProgress(progressEvent) {
}, option &&
option.onProcess &&
option.onProcess(
progressEvent.total
? progressEvent.loaded / progressEvent.total
: -1
)
},
...axiosOption
})
return {
response: new Promise<Uint8Array>(async (resolve) => {
resolve(new Uint8Array((await response).data))
}),
abort
}
},
/** /**
* 上传单个文件到对应文件夹, 后续要将data类型改成buffer以支持分片上传 * 上传单个文件到对应文件夹, 后续要将data类型改成buffer以支持分片上传
* 正常上传文件请使用uploadQueue * 正常上传文件请使用uploadQueue
*/ */
upload(data: File, targetVDir: VirtualDirectory, option?: uploadOption) { upload(data: File, targetVDir: VirtualDirectory, option?: uploadOption) {
const { baseUrl, api } = Config const { baseUrl, api } = Config
const url = `${baseUrl}${api.upload}${targetVDir.path}` const url = `${baseUrl}${api.upload}${targetVDir.path}`
const retry = option?.retry ?? 0 const retry = option?.retry ?? 0
const { miaoFetch, abortMiaoFetch } = useMiaoFetch({ const { miaoFetch, abortMiaoFetch } = useMiaoFetch({
retry retry
}) })
const sizeTotal = data.size const sizeTotal = data.size
const opType = option?.type ?? 'write' const opType = option?.type ?? 'write'
const formData = new FormData() const formData = new FormData()
formData.append('file', data) formData.append('file', data)
// 必须这样传,不然中文乱码 // 必须这样传,不然中文乱码
formData.append('fileName', data.name) formData.append('fileName', data.name)
formData.append('operateType', opType) formData.append('operateType', opType)
return { return {
abort: () => abortMiaoFetch(), abort: () => abortMiaoFetch(),
response: new Promise<{ response: new Promise<{
message: 'success' | 'failed'
error?: string
}>(async (resolve) => {
const response = await miaoFetch({
url,
method: 'post',
data: formData,
onUploadProgress(progressEvent) {
option &&
option.onProcess &&
option.onProcess(progressEvent.loaded / sizeTotal)
}
})
resolve(response.data)
})
}
},
/**
* 在目标文件夹下创建新的文件夹
*/
mkdir(
targetVDir: VirtualDirectory,
newFolderName: string,
option?: mkdirOption
) {
const { baseUrl, api } = Config
const url = `${baseUrl}${api.mkdir}${targetVDir.path}`
const { miaoFetch } = useMiaoFetch({
retry: option?.retry ?? 0
})
return new Promise<{
message: 'success' | 'failed' message: 'success' | 'failed'
error?: string error?: string
}>(async (resolve) => { }>(async (resolve) => {
const response = await miaoFetch({ const response = await miaoFetch({
url, url,
method: 'post', method: 'post',
data: { data: formData,
folderName: newFolderName onUploadProgress(progressEvent) {
} option &&
option.onProcess &&
option.onProcess(progressEvent.loaded / sizeTotal)
}
}) })
resolve(response.data) resolve(response.data)
}) })
}, }
/** },
* 在目标文件夹下创建新的空文件,实际上是通过upload空文件实现
*/
mkFile(
targetVDir: VirtualDirectory,
newFileName: string,
option?: uploadOption
) {
const emptyBlob = new Blob([])
const emptyFile = new File([emptyBlob], newFileName, {
type: 'text/plain'
})
return this.upload(emptyFile, targetVDir, option)
},
/** /**
* 删除对应的文件或文件夹,若为文件夹会递归删除 * 在目标文件夹下创建新的文件夹
*/ */
delete(target: VirtualFile | VirtualDirectory, option?: deleteOption) { mkdir(
const { baseUrl, api } = Config targetVDir: VirtualDirectory,
const url = `${baseUrl}${api.delete}${target.path}` newFolderName: string,
const retry = option?.retry ?? 0 option?: mkdirOption
const { miaoFetch } = useMiaoFetch({ ) {
retry const { baseUrl, api } = Config
}) const url = `${baseUrl}${api.mkdir}${targetVDir.path}`
const { miaoFetch } = useMiaoFetch({
retry: option?.retry ?? 0
})
return { return new Promise<{
response: new Promise<{ message: 'success' | 'failed'
eventId: number error?: string
}>(async (resolve) => { }>(async (resolve) => {
const response = await miaoFetch({ const response = await miaoFetch({
url, url,
method: 'post' method: 'post',
}) data: {
resolve(response.data) folderName: newFolderName
}) }
} })
}, resolve(response.data)
/** })
* 重命名对应的文件或文件夹 },
*/ /**
rename( * 在目标文件夹下创建新的空文件,实际上是通过upload空文件实现
target: VirtualFile | VirtualDirectory, */
newName: string, mkFile(
option?: renameOption targetVDir: VirtualDirectory,
) { newFileName: string,
const { baseUrl, api } = Config option?: uploadOption
const url = `${baseUrl}${api.rename}${target.path}` ) {
const retry = option?.retry ?? 0 const emptyBlob = new Blob([])
const { miaoFetch } = useMiaoFetch({ const emptyFile = new File([emptyBlob], newFileName, {
retry type: 'text/plain'
}) })
return this.upload(emptyFile, targetVDir, option)
},
return { /**
response: new Promise<{ * 删除对应的文件或文件夹,若为文件夹会递归删除
eventId: number */
}>(async (resolve) => { delete(target: VirtualFile | VirtualDirectory, option?: deleteOption) {
const response = await miaoFetch({ const { baseUrl, api } = Config
url, const url = `${baseUrl}${api.delete}${target.path}`
method: 'post', const retry = option?.retry ?? 0
data: { newName } const { miaoFetch } = useMiaoFetch({
}) retry
resolve(response.data) })
})
}
},
/**
* 剪切文件或文件夹
*/
cut(
target: VirtualFile | VirtualDirectory,
to: VirtualDirectory,
option?: cutOption
) {
const newPath = to.path
const { baseUrl, api } = Config
const url = `${baseUrl}${api.cut}${target.path}`
const retry = option?.retry ?? 0
const { miaoFetch } = useMiaoFetch({
retry
})
return {
response: new Promise<{
eventId: number
}>(async (resolve) => {
const response = await miaoFetch({
url,
method: 'post',
data: { newPath }
})
resolve(response.data)
})
}
},
/** return {
* 查询服务端操作事件的进展 response: new Promise<{
*/ eventId: number
query(eventId: number, option?: queryOption) { }>(async (resolve) => {
const { baseUrl, api } = Config const response = await miaoFetch({
const url = `${baseUrl}${api.query}` url,
const retry = option?.retry ?? 0 method: 'post'
const { miaoFetch } = useMiaoFetch({
retry
})
return {
response: new Promise<{
status: 'process' | 'success' | 'failed'
error?: unknown
}>(async (resolve) => {
const response = await miaoFetch({
url,
method: 'get',
params: {
id: eventId
}
})
resolve(response.data)
}) })
} resolve(response.data)
} })
}
},
/**
* 重命名对应的文件或文件夹
*/
rename(
target: VirtualFile | VirtualDirectory,
newName: string,
option?: renameOption
) {
const { baseUrl, api } = Config
const url = `${baseUrl}${api.rename}${target.path}`
const retry = option?.retry ?? 0
const { miaoFetch } = useMiaoFetch({
retry
})
return {
response: new Promise<{
eventId: number
}>(async (resolve) => {
const response = await miaoFetch({
url,
method: 'post',
data: { newName }
})
resolve(response.data)
})
}
},
/**
* 剪切文件或文件夹
*/
cut(
target: VirtualFile | VirtualDirectory,
to: VirtualDirectory,
option?: cutOption
) {
const newPath = to.path
const { baseUrl, api } = Config
const url = `${baseUrl}${api.cut}${target.path}`
const retry = option?.retry ?? 0
const { miaoFetch } = useMiaoFetch({
retry
})
return {
response: new Promise<{
eventId: number
}>(async (resolve) => {
const response = await miaoFetch({
url,
method: 'post',
data: { newPath }
})
resolve(response.data)
})
}
},
/**
* 复制文件或文件夹
*/
copy(
target: VirtualFile | VirtualDirectory,
to: VirtualDirectory,
option?: copyOption
) {
const newPath = to.path
const { baseUrl, api } = Config
const url = `${baseUrl}${api.copy}${target.path}`
const retry = option?.retry ?? 0
const { miaoFetch } = useMiaoFetch({
retry
})
return {
response: new Promise<{
eventId: number
}>(async (resolve) => {
const response = await miaoFetch({
url,
method: 'post',
data: { newPath }
})
resolve(response.data)
})
}
},
/**
* 查询服务端操作事件的进展
*/
query(eventId: number, option?: queryOption) {
const { baseUrl, api } = Config
const url = `${baseUrl}${api.query}`
const retry = option?.retry ?? 0
const { miaoFetch } = useMiaoFetch({
retry
})
return {
response: new Promise<{
status: 'process' | 'success' | 'failed'
error?: unknown
}>(async (resolve) => {
const response = await miaoFetch({
url,
method: 'get',
params: {
id: eventId
}
})
resolve(response.data)
})
}
}
} }
export default function useMiaoFetchApi() { export default function useMiaoFetchApi() {
return miaoFetchApi return miaoFetchApi
} }
+12 -8
View File
@@ -1,11 +1,15 @@
import PluginCenter from "@/class/PluginCenter"; import PluginCenter from '@/class/PluginCenter'
import { reactive } from "vue"; import { alertTipType } from '@/types/type'
import { reactive } from 'vue'
let pluginCenter: PluginCenter let pluginCenter: PluginCenter
export default function usePluginCenter(){ export const initPluginCenter = (globalAlertTip: alertTipType) => {
if(!pluginCenter) { if (!pluginCenter) {
pluginCenter = reactive(new PluginCenter()) pluginCenter = reactive(new PluginCenter(globalAlertTip))
} }
return pluginCenter }
}
export default function usePluginCenter() {
return pluginCenter
}
+17
View File
@@ -0,0 +1,17 @@
import { VirtualRemoteDirectory } from '@/class/VirtualRemoteDirectory'
import { reactive } from 'vue'
const root = reactive(
new VirtualRemoteDirectory({
name: '根目录',
stats: {
atimeMs: 0,
birthtimeMs: 0,
ctimeMs: 0,
mtimeMs: 0
}
})
)
export default function useRootVDirectory() {
return root
}
+54
View File
@@ -0,0 +1,54 @@
import { ref } from 'vue'
import config from '@/config'
import { useMiaoFetch } from '@/hooks/useMiaoFetchApi'
export interface ServerAddress {
title: string
url: string
}
/**
* 获取服务器所有可用的访问地址
*/
export default function useServerAddresses() {
const addresses = ref<ServerAddress[]>([])
const loading = ref(false)
const error = ref<Error | null>(null)
/**
* 从服务器获取可用地址列表
*/
const fetchAddresses = async () => {
loading.value = true
error.value = null
try {
const { baseUrl, api } = config
const { miaoFetch } = useMiaoFetch({ retry: 1 })
const response = await miaoFetch({
url: `${baseUrl}${api.addresses}`
})
addresses.value = response.data as ServerAddress[]
} catch (err) {
console.error('获取服务器地址失败:', err)
error.value = err as Error
// 如果API失败,提供基本的地址
addresses.value = [
{ title: '当前地址', url: config.baseUrl },
{ title: '本地地址', url: 'http://localhost:17705' }
]
} finally {
loading.value = false
}
}
return {
addresses,
loading,
error,
fetchAddresses
}
}
+7 -7
View File
@@ -1,11 +1,11 @@
import UploadQueue from "@/class/UploadQueue"; import UploadQueue from '@/class/UploadQueue'
import { reactive } from "vue"; import { reactive } from 'vue'
let uploadQueue: UploadQueue let uploadQueue: UploadQueue
export default () => { export default () => {
if(uploadQueue === undefined){ if (uploadQueue === undefined) {
uploadQueue = reactive(new UploadQueue()) uploadQueue = reactive(new UploadQueue())
} }
return uploadQueue return uploadQueue
} }
+9 -9
View File
@@ -1,12 +1,12 @@
import { VirtualPages } from "@/class/VirtualPage" import { VirtualPages } from '@/class/VirtualPage'
import { Reactive, reactive } from "vue" import { Reactive, reactive } from 'vue'
let views: Reactive<VirtualPages> let views: Reactive<VirtualPages>
export default function useViretualPages(){ export default function useVirtualPages() {
if(views === undefined) { if (views === undefined) {
// views = reactive<VirtualPage[]>([]) // views = reactive<VirtualPage[]>([])
views = reactive(new VirtualPages()) views = reactive(new VirtualPages())
} }
return views return views
} }
+2
View File
@@ -1,4 +1,6 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import 'drag-drop-touch'
import '@/styles/tailwind.css'
createApp(App).mount('#app') createApp(App).mount('#app')
+512
View File
@@ -0,0 +1,512 @@
<template>
<div class="chat-container">
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<div class="progress-container">
<div class="progress-bar" :style="{ width: loadingProgress + '%' }"></div>
</div>
<p class="loading-percentage">{{ loadingProgress }}%</p>
<p>{{ loadingState }}</p>
</div>
<div v-else class="chat-main">
<div class="chat-history" ref="chatHistoryRef">
<div v-for="(message, index) in chatHistory" :key="index"
: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 markdown-content" v-else v-html="renderMarkdown(message.content)"></div>
</div>
<!-- 实时生成的内容 -->
<div v-if="generatingMessage" class="message ai-message">
<div class="message-content markdown-content" v-html="renderMarkdown(currentGeneratedText)"></div>
<div class="generating-indicator"><span>.</span><span>.</span><span>.</span></div>
</div>
</div>
<div class="chat-input">
<textarea v-model="userInput" @keydown.enter.prevent="sendMessage" placeholder="输入消息..."
:disabled="processing"></textarea>
<button @click="sendMessage" :disabled="processing || !userInput.trim()">
发送
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory';
import { ref, watch, nextTick, onUnmounted } from 'vue';
import MarkdownIt from 'markdown-it';
import { FilesetResolver, LlmInference } from '@mediapipe/tasks-genai';
import VirtualPage from '@/class/VirtualPage';
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
});
const currentDirectories = defineModel<VirtualDirectory[]>(
'currentDirectories',
{
required: true
}
)
const props = defineProps<{
view: VirtualPage
}>()
// 初始化MarkdownIt
const md = new MarkdownIt({
html: true,
linkify: true,
typographer: true,
breaks: true
});
// 状态管理
const loading = ref(true);
const loadingState = ref('正在初始化模型...');
const loadingProgress = ref(0);
const processing = ref(false);
const userInput = ref('');
const chatHistory = ref<{ role: string, content: string }[]>([]);
const chatHistoryRef = ref<HTMLElement | null>(null);
const llmInstance = ref<any>(null);
const generatingMessage = ref(false);
const currentGeneratedText = ref('');
const updateLoadingProgress = (progress: number, state: string) => {
loadingProgress.value = progress;
loadingState.value = state;
};
const renderMarkdown = (text: string) => {
return md.render(text);
};
const initializeModel = async () => {
if (!currentFiles.value || currentFiles.value.length === 0) {
loadingState.value = '错误:未找到模型文件';
return;
}
try {
const modelFile = currentFiles.value[0];
const modelName = modelFile.name.split('.model.bin')[0] && modelFile.name.split('.model.task')[0];
updateLoadingProgress(5, '正在加载MediaPipe LLM引擎...');
const simulateDownloadProgress = () => {
const totalTime = 2000; // 2秒
const interval = 100; // 每100毫秒更新一次
const steps = totalTime / interval;
const incrementPerStep = 15 / steps; // 从5%到20%
let currentStep = 0;
const timer = setInterval(() => {
if (currentStep >= steps) {
clearInterval(timer);
return;
}
loadingProgress.value = Math.min(20, 5 + currentStep * incrementPerStep);
currentStep++;
}, interval);
};
simulateDownloadProgress();
updateLoadingProgress(20, '正在初始化MediaPipe文件解析器...');
const genai = await FilesetResolver.forGenAiTasks(
currentDirectories.value[0]?.url ?? "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-genai@latest/wasm"
);
updateLoadingProgress(40, '正在加载模型文件...');
const simulateModelLoadingProgress = () => {
const totalTime = 3000; // 3秒
const interval = 100; // 每100毫秒更新一次
const steps = totalTime / interval;
const incrementPerStep = 45 / steps; // 从40%到85%
let currentStep = 0;
const timer = setInterval(() => {
if (currentStep >= steps) {
clearInterval(timer);
return;
}
loadingProgress.value = Math.min(85, 40 + currentStep * incrementPerStep);
currentStep++;
}, interval);
};
simulateModelLoadingProgress();
llmInstance.value = await LlmInference.createFromOptions(genai, {
baseOptions: {
modelAssetPath: modelFile.url
},
maxTokens: 2048,
temperature: 0.7
});
updateLoadingProgress(90, '初始化对话...');
await new Promise(resolve => setTimeout(resolve, 500));
updateLoadingProgress(100, '加载完成!');
await new Promise(resolve => setTimeout(resolve, 300));
loading.value = false;
chatHistory.value.push({
role: 'assistant',
content: `您好!我是${modelName},有什么可以帮助您的?`
});
} catch (error) {
console.error('模型加载失败:', error);
loadingState.value = `模型加载失败: ${error instanceof Error ? error.message : String(error)}`;
loadingProgress.value = 0;
}
};
// 发送消息
const sendMessage = async () => {
if (!userInput.value.trim() || processing.value) return;
const userMessage = userInput.value.trim();
userInput.value = '';
// 添加用户消息到历史
chatHistory.value.push({
role: 'user',
content: userMessage
});
processing.value = true;
generatingMessage.value = true;
currentGeneratedText.value = '';
try {
// 滚动到底部
await nextTick();
scrollToBottom();
// 构建提示词(包含历史对话)
// const history = chatHistory.value
// .map(msg => `${msg.role === 'user' ? '用户' : 'AI'}:${msg.content}`)
// .join('\n\n');
// const prompt = history + '\n\n用户:' + userMessage + '\n\nAI:';
// 使用MediaPipe生成回复
llmInstance.value.generateResponse(
userMessage,
null, // 不使用LoRA模型
(partialResult: string, done: boolean) => {
// 累加部分结果并更新UI
currentGeneratedText.value += partialResult;
const banStart = [',', ',']
for (const ban of banStart) {
if (currentGeneratedText.value.startsWith(ban)) {
currentGeneratedText.value = currentGeneratedText.value.slice(1)
}
}
// 滚动到底部以显示最新内容
nextTick().then(scrollToBottom);
if (done) {
// 添加完整回复到历史
chatHistory.value.push({
role: 'assistant',
content: currentGeneratedText.value
});
// 重置生成状态
generatingMessage.value = false;
processing.value = false;
currentGeneratedText.value = '';
nextTick().then(scrollToBottom);
}
}
);
} catch (error) {
console.error('生成回复出错:', error);
// 添加错误信息
chatHistory.value.push({
role: 'assistant',
content: `抱歉,生成回复时出现错误: ${error instanceof Error ? error.message : String(error)}`
});
// 重置状态
generatingMessage.value = false;
processing.value = false;
currentGeneratedText.value = '';
nextTick().then(scrollToBottom);
}
};
// 滚动到聊天历史底部
const scrollToBottom = () => {
if (chatHistoryRef.value) {
chatHistoryRef.value.scrollTop = chatHistoryRef.value.scrollHeight;
}
};
// 清理资源函数
const cleanupResources = () => {
// 如果正在进行推理,停止推理过程
if (processing.value || generatingMessage.value) {
// 标记处理完成,停止状态更新
processing.value = false;
generatingMessage.value = false;
console.log('组件卸载:已停止正在进行的文本生成');
}
// 释放LLM实例资源
if (llmInstance.value) {
const releaseResources = async () => {
try {
// 调用模型实例的清理方法(如果有)
if (typeof llmInstance.value.close === 'function') {
llmInstance.value.close();
}
// 置空实例引用
llmInstance.value = null;
console.log('组件卸载:已释放模型资源');
} catch (error) {
console.error('释放模型资源时出错:', error);
setTimeout(() => {
releaseResources();
}, 1000);
}
}
releaseResources();
}
};
onMounted(() => {
props.view.setTitle('LLM Demo')
})
// 在组件卸载时清理资源
onUnmounted(() => {
cleanupResources();
});
// 监听模型文件变化
watch(() => currentFiles.value, () => {
if (currentFiles.value && currentFiles.value.length > 0) {
initializeModel();
}
}, { immediate: true });
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background-color: #f5f5f5;
overflow: hidden;
}
.loading-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
padding: 20px;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #3498db;
animation: spin 1s ease-in-out infinite;
margin-bottom: 20px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.progress-container {
width: 80%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #1e88e5, #64b5f6);
transition: width 0.3s ease;
border-radius: 10px;
}
.loading-percentage {
font-size: 18px;
font-weight: bold;
color: #1e88e5;
margin-bottom: 10px;
}
.chat-main {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.chat-history {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
max-width: 85%;
padding: 10px 16px;
border-radius: 18px;
word-break: break-word;
}
.user-message {
align-self: flex-end;
background-color: #1e88e5;
color: white;
border-bottom-right-radius: 4px;
}
.ai-message {
align-self: flex-start;
background-color: #e0e0e0;
color: #333;
border-bottom-left-radius: 4px;
}
.markdown-content :deep(p) {
margin: 0.5em 0;
}
.markdown-content :deep(pre) {
background-color: #f0f0f0;
padding: 8px;
border-radius: 4px;
overflow-x: auto;
}
.markdown-content :deep(code) {
background-color: #f0f0f0;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
.markdown-content :deep(ul),
.markdown-content :deep(ol) {
margin: 0.5em 0;
padding-left: 1.5em;
}
.markdown-content :deep(table) {
border-collapse: collapse;
margin: 1em 0;
}
.markdown-content :deep(th),
.markdown-content :deep(td) {
border: 1px solid #ddd;
padding: 4px 8px;
}
.markdown-content :deep(blockquote) {
margin: 0.5em 0;
padding-left: 1em;
border-left: 4px solid #ddd;
color: #555;
}
.generating-indicator {
display: inline-block;
margin-left: 5px;
}
.generating-indicator span {
display: inline-block;
animation: blink 1.4s infinite both;
}
.generating-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.generating-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
.chat-input {
display: flex;
padding: 10px;
background-color: white;
border-top: 1px solid #e0e0e0;
}
.chat-input textarea {
flex: 1;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 10px;
resize: none;
height: 60px;
font-family: inherit;
font-size: 14px;
}
.chat-input button {
margin-left: 10px;
background-color: #1e88e5;
color: white;
border: none;
border-radius: 4px;
padding: 0 20px;
cursor: pointer;
font-weight: bold;
}
.chat-input button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>
+35
View File
@@ -0,0 +1,35 @@
import { registerComponentOption } from '@/class/PluginCenter'
import PlayCircle from '@vicons/ionicons5/es/PlayCircle'
const pluginConfig: registerComponentOption = {
key: 'browser_llm',
name: 'Browser_LLM',
icon: PlayCircle,
getComponent: async () =>
(await import('@/plugins/browser_llm/browser_llm.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length > 1) {
return false
}
if (vDirs.length === 1 && vDirs[0].name !== 'tasks_genai_wasm') {
return false
}
if (vFiles.length === 0) {
return false
}
for (let i of vFiles) {
for (let s of ['model.bin', 'model.task']) {
if (i.name.endsWith(s)) {
return true
}
}
}
return false
},
priority: 6
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
+16
View File
@@ -0,0 +1,16 @@
import { VirtualFile } from '@/class/VirtualDirectory'
import config from '@/config'
import { usePluginHooksType } from '@/types/type'
import copy2Clipboard from 'copy-to-clipboard'
const copy = (__: any, vFiles: VirtualFile[], hooks: usePluginHooksType) => {
const { globalAlertTip } = hooks
const vFile = vFiles[0]
console.log(vFile.url)
copy2Clipboard(`${config.baseUrl}${vFile.url}`)
globalAlertTip(`${vFile.name}的链接已复制至剪切板`, {
type: 'info',
timeout: 2000
})
}
export default copy
@@ -0,0 +1,27 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import Copy from './copy'
import { CopyOutline } from '@vicons/ionicons5'
import { PluginExportType } from '@/types/type'
import { shallowRef } from 'vue'
const pluginConfig: Plugin = {
name: '复制链接',
icon: shallowRef(CopyOutline),
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return true
},
disable: false,
group: [PluginGroup.default],
func: Copy,
priority: -100
}
export default pluginConfig
export const type = PluginExportType.default
@@ -0,0 +1,174 @@
import { VirtualDirectory, VirtualFile } from '@/class/VirtualDirectory'
import { usePluginHooksType } from '@/types/type'
// 用于存储复制的项目
let copiedItems: {
files: VirtualFile[],
directories: VirtualDirectory[],
isCut: boolean
} = {
files: [],
directories: [],
isCut: false
}
// 复制或剪切虚拟项目
const copyItems = (vDirs: VirtualDirectory[], vFiles: VirtualFile[], hooks: usePluginHooksType, isCut = false) => {
const { globalAlertTip } = hooks
// 保存复制的项目
copiedItems = {
files: [...vFiles],
directories: [...vDirs],
isCut
}
// 生成消息文本
const fileCount = vFiles.length
const dirCount = vDirs.length
let messageText = ''
if (fileCount > 0 && dirCount > 0) {
messageText = `已${isCut ? '剪切' : '复制'} ${fileCount}个文件和${dirCount}个文件夹`
} else if (fileCount > 0) {
messageText = `已${isCut ? '剪切' : '复制'} ${fileCount}个文件`
} else if (dirCount > 0) {
messageText = `已${isCut ? '剪切' : '复制'} ${dirCount}个文件夹`
}
// 显示提示
globalAlertTip(messageText, {
type: 'info',
timeout: 2000
})
}
// 粘贴虚拟项目
const pasteItems = async (vDirs: VirtualDirectory[], _vFiles: VirtualFile[], hooks: usePluginHooksType) => {
const { globalAlertTip, getRootVDirectory } = hooks
// 检查目标目录
if (vDirs.length !== 1) {
globalAlertTip('请选择一个目标文件夹进行粘贴', {
type: 'error',
timeout: 2000
})
return
}
// 检查是否有复制的项目
if (copiedItems.files.length === 0 && copiedItems.directories.length === 0) {
globalAlertTip('没有可粘贴的项目', {
type: 'info',
timeout: 2000
})
return
}
const targetDir = vDirs[0]
let successCount = 0
let failCount = 0
// 处理粘贴文件
for (const file of copiedItems.files) {
try {
if (copiedItems.isCut) {
// 剪切操作
const success = await file.moveTo(targetDir)
if (success) {
successCount++
} else {
failCount++
}
} else {
// 复制操作
const success = await file.copyTo(targetDir)
if (success) {
successCount++
} else {
failCount++
}
}
} catch (err) {
console.error('粘贴文件失败:', err)
failCount++
}
}
// 处理粘贴目录
for (const dir of copiedItems.directories) {
try {
if (copiedItems.isCut) {
// 目录移动 - 使用mv方法而不是moveTo
const success = await dir.mv(targetDir)
if (success) {
successCount++
} else {
failCount++
}
} else {
// 目录复制
const success = await dir.copy(targetDir)
if (success) {
successCount++
} else {
failCount++
}
}
} catch (err) {
console.error('粘贴目录失败:', err)
failCount++
}
}
// 如果是剪切操作,粘贴后清空复制的项目
if (copiedItems.isCut && successCount > 0) {
copiedItems = {
files: [],
directories: [],
isCut: false
}
}
// 显示操作结果
if (successCount > 0 && failCount === 0) {
globalAlertTip(`成功${copiedItems.isCut ? '移动' : '复制'}了${successCount}个项目`, {
type: 'success',
timeout: 2000
})
} else if (successCount > 0 && failCount > 0) {
globalAlertTip(`${successCount}个项目成功,${failCount}个项目失败`, {
type: 'info',
timeout: 2000
})
} else {
globalAlertTip('操作失败', {
type: 'error',
timeout: 2000
})
}
// 更新目录
try {
// 使用update方法更新根目录
const rootDir = getRootVDirectory();
await rootDir.update();
} catch (err) {
console.error('更新目录失败:', err);
}
}
// 复制功能
export const copy = (vDirs: VirtualDirectory[], vFiles: VirtualFile[], hooks: usePluginHooksType) => {
copyItems(vDirs, vFiles, hooks, false)
}
// 剪切功能
export const cut = (vDirs: VirtualDirectory[], vFiles: VirtualFile[], hooks: usePluginHooksType) => {
copyItems(vDirs, vFiles, hooks, true)
}
// 粘贴功能
export const paste = (vDirs: VirtualDirectory[], vFiles: VirtualFile[], hooks: usePluginHooksType) => {
pasteItems(vDirs, vFiles, hooks)
}
@@ -0,0 +1,60 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import { copy, cut, paste } from './copyPaste'
import {
CopyOutline,
CutOutline,
DocumentAttachOutline
} from '@vicons/ionicons5'
import { PluginExportType } from '@/types/type'
import { shallowRef } from 'vue'
// 复制插件配置
export const copyPluginConfig: Plugin = {
name: '复制',
icon: shallowRef(CopyOutline),
filter: (vDirs, vFiles) => {
// 至少选择了一个文件或文件夹
return vDirs.length > 0 || vFiles.length > 0
},
disable: false,
group: [PluginGroup.default],
func: copy,
priority: -10
}
// 剪切插件配置
export const cutPluginConfig: Plugin = {
name: '剪切',
icon: shallowRef(CutOutline),
filter: (vDirs, vFiles) => {
// 至少选择了一个文件或文件夹
return vDirs.length > 0 || vFiles.length > 0
},
disable: false,
group: [PluginGroup.default],
func: cut,
priority: -10
}
// 粘贴插件配置
export const pastePluginConfig: Plugin = {
name: '粘贴',
icon: shallowRef(DocumentAttachOutline),
filter: (vDirs, vFiles) => {
// 只选择了一个目录作为粘贴目标
return vDirs.length === 1 && vFiles.length === 0
},
disable: false,
group: [PluginGroup.default],
func: paste,
priority: -10
}
// 导出为默认插件类型
export const type = PluginExportType.default
// 插件键值
export const key = 'miaoCopy'
// 导出默认的复制插件
export default copyPluginConfig
@@ -0,0 +1,11 @@
import { cutPluginConfig } from './export'
import { PluginExportType } from '@/types/type'
// 导出为默认插件类型
export const type = PluginExportType.default
// 插件键值
export const key = 'miaoCut'
// 导出剪切插件
export default cutPluginConfig
@@ -0,0 +1,11 @@
import { pastePluginConfig } from './export'
import { PluginExportType } from '@/types/type'
// 导出为默认插件类型
export const type = PluginExportType.default
// 插件键值
export const key = 'miaoPaste'
// 导出粘贴插件
export default pastePluginConfig
+25
View File
@@ -0,0 +1,25 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import hideFps from './hideFps'
import { EyeOffOutline } from '@vicons/ionicons5'
import { PluginExportType } from '@/types/type'
import { shallowRef } from 'vue'
import { fpsElement } from './shared'
const pluginConfig: Plugin = {
name: '隐藏FPS',
icon: shallowRef(EyeOffOutline),
filter: () => {
// 只有当FPS显示元素存在时才显示此按钮
return fpsElement.value !== null
},
disable: false,
group: [PluginGroup.mainMenu],
func: hideFps,
priority: 0
}
export default pluginConfig
export const type = PluginExportType.default
export const key = 'hideFps'
+24
View File
@@ -0,0 +1,24 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import showFps from './showFps'
import { SpeedometerOutline } from '@vicons/ionicons5'
import { PluginExportType } from '@/types/type'
import { shallowRef } from 'vue'
import { fpsElement } from './shared'
const pluginConfig: Plugin = {
name: '显示FPS',
icon: shallowRef(SpeedometerOutline),
filter: () => {
return fpsElement.value === null
},
disable: false,
group: [PluginGroup.mainMenu],
func: showFps,
priority: 0
}
export default pluginConfig
export const type = PluginExportType.default
export const key = 'showFps'
+18
View File
@@ -0,0 +1,18 @@
// 引入共享变量
import { fpsElement, animationFrameId, setFpsElement, setAnimationFrameId } from '../fps/shared';
const hideFps = () => {
// 停止帧率计算
if (animationFrameId.value !== null) {
cancelAnimationFrame(animationFrameId.value);
setAnimationFrameId(null);
}
// 移除FPS显示元素
if (fpsElement.value) {
document.body.removeChild(fpsElement.value);
setFpsElement(null);
}
};
export default hideFps;
+25
View File
@@ -0,0 +1,25 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import hideFps from './hideFps'
import { EyeOffOutline } from '@vicons/ionicons5'
import { PluginExportType } from '@/types/type'
import { shallowRef } from 'vue'
import { fpsElement } from '../fps/shared'
const pluginConfig: Plugin = {
name: '隐藏FPS',
icon: shallowRef(EyeOffOutline),
filter: () => {
// 只有当FPS显示元素存在时才显示此按钮
return fpsElement.value !== null
},
disable: false,
group: [PluginGroup.mainMenu],
func: hideFps,
priority: 0
}
export default pluginConfig
export const type = PluginExportType.default
export const key = 'hideFps'
+5
View File
@@ -0,0 +1,5 @@
export * from './shared';
export { default as showFps } from './showFps';
export { default as hideFps } from './hideFps';
export { default as showFpsPlugin } from './export_show';
export { default as hideFpsPlugin } from './export_hide';
+14
View File
@@ -0,0 +1,14 @@
import { shallowRef } from 'vue';
// 使用shallowRef包装状态
export const fpsElement = shallowRef<HTMLDivElement | null>(null);
export const animationFrameId = shallowRef<number | null>(null);
// 更新设置函数
export const setFpsElement = (el: HTMLDivElement | null) => {
fpsElement.value = el;
};
export const setAnimationFrameId = (id: number | null) => {
animationFrameId.value = id;
};
+57
View File
@@ -0,0 +1,57 @@
import { fpsElement, setFpsElement, setAnimationFrameId } from '../fps/shared';
// const frameArr = new Array<number>(0)
let lastTime: number = 0
const createFpsElement = () => {
const el = document.createElement('div');
el.style.position = 'fixed';
el.style.top = '10px';
el.style.left = '10px';
el.style.backgroundColor = 'rgba(255, 255, 255, 0.2)';
el.style.pointerEvents = 'none';
el.style.userSelect = 'none';
el.style.color = 'green';
el.style.padding = '5px 10px';
el.style.borderRadius = '4px';
el.style.fontFamily = 'monospace';
el.style.fontSize = '14px';
el.style.zIndex = '9999';
document.body.appendChild(el);
return el;
};
const calculateFps = () => {
// const range = 1000
// const curr = new Date()
// frameArr.push(curr.getTime())
// while (frameArr[0] < curr.getTime() - range) {
// frameArr.shift()
// }
// const fps = Math.floor(frameArr.length / range * 1000)
// if (fpsElement.value) {
// fpsElement.value.textContent = `FPS: ${fps}`;
// }
// const id = requestAnimationFrame(calculateFps);
// setAnimationFrameId(id);
const curr = new Date()
const fps = Math.floor(1000 / (curr.getTime() - lastTime))
if (fpsElement.value && lastTime !== 0) {
fpsElement.value.textContent = `FPS: ${fps}`;
}
lastTime = curr.getTime()
const id = requestAnimationFrame(calculateFps);
setAnimationFrameId(id);
};
const showFps = () => {
if (fpsElement.value) return;
const el = createFpsElement();
setFpsElement(el);
const id = requestAnimationFrame(calculateFps);
setAnimationFrameId(id);
};
export default showFps;
+23
View File
@@ -0,0 +1,23 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import fullScreen from './fullScreen'
import { ExpandOutline } from '@vicons/ionicons5'
import { PluginExportType } from '@/types/type'
import { shallowRef } from 'vue'
const pluginConfig: Plugin = {
name: '全屏',
icon: shallowRef(ExpandOutline),
filter: () => {
return true
},
disable: false,
group: [PluginGroup.mainMenu],
func: fullScreen,
priority: -1
}
export default pluginConfig
export const type = PluginExportType.default
export const key = 'fullScreen'
+4
View File
@@ -0,0 +1,4 @@
const fullScreen = () => {
document.body.requestFullscreen()
}
export default fullScreen
+49
View File
@@ -0,0 +1,49 @@
import { registerComponentOption } from '@/class/PluginCenter'
import { CodeOutline } from '@vicons/ionicons5'
import { PluginExportType } from '@/types/type'
const pluginConfig: registerComponentOption = {
key: 'miaoCodeMirror',
name: '代码编辑',
icon: CodeOutline,
getComponent: async () =>
(await import('@/plugins/miaoCodeMirror/miaoCodeMirror.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
for (let end of [
'js',
'ts',
'jsx',
'tsx',
'py',
'html',
'css',
'scss',
'go',
'json',
'md',
'yaml',
'txt',
'xml',
'vue',
'srt',
'vtt',
'ass',
'ssa'
]) {
if (vFiles[0].name.endsWith(end)) {
return true
}
}
return false
}
}
export default pluginConfig
export const type = PluginExportType.component
@@ -0,0 +1,216 @@
<template>
<miao-message-provider ref="miaoMessage">
<div
class="codemirror-container"
ref="rootRef"
:class="`codemirror-container-${theme}`">
<div class="codemirror-container-top">
<div
class="codemirror-container-btn codemirror-container-btn-save"
@click="handleSave">
保存
</div>
<div
class="codemirror-container-btn codemirror-container-btn-save"
@click="handleReset">
恢复到上次保存
</div>
</div>
<div class="codemirror-container-main">
<NScrollbar style="max-height: calc(100%)">
<codemirror v-model="codeData" :extensions="extensions" />
</NScrollbar>
</div>
</div>
</miao-message-provider>
</template>
<script setup lang="ts">
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
import { VirtualFile } from '@/class/VirtualDirectory'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import { onMounted, ref } from 'vue'
import { Codemirror } from 'vue-codemirror'
import { NScrollbar } from 'naive-ui'
const miaoFetchApi = useMiaoFetchApi()
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const decoder = new TextDecoder('utf-8')
const miaoMessage = ref<InstanceType<typeof miaoMessageProvider>>()
const vFile = ref<VirtualFile>()
const rootRef = ref<HTMLDivElement>()
// const _key = ref(Math.random())
const codeData = ref<string>()
// 第一次点击保存时自动备份data,暂时懒得做
const bakData = ref<string>()
const extensions = ref<any[]>([])
const theme = ref<'light' | 'dark'>('light')
const handleSave = async () => {
if (bakData.value === codeData.value || !vFile.value) {
miaoMessage.value?.alertTip('内容未改动', {
timeout: 3000,
type: 'info'
})
return
}
const parentDir = vFile.value?.parent
const file_bak = new File(
[new Blob([bakData.value ?? ''])],
`${vFile.value?.name}.bak`
)
const file = new File([new Blob([codeData.value ?? ''])], vFile.value.name)
bakData.value = codeData.value
const { response } = miaoFetchApi.upload(file, parentDir)
false && miaoFetchApi.upload(file_bak, parentDir)
await response
miaoMessage.value?.alertTip('保存成功', {
timeout: 5000,
type: 'success'
})
}
const handleReset = async () => {
codeData.value = bakData.value
}
// const switchTheme = async () => {
// if (!vFile.value) {
// return
// }
// theme.value = theme.value === 'dark' ? 'light' : 'dark'
// extensions.value = [
// ...(await getLangExtensions(vFile.value?.name)),
// // theme.value === 'dark' ?
// ]
// _key.value = Math.random()
// }
const getLangExtensions = async (fileName: string) => {
const ext = fileName.split('.').pop()?.toLocaleLowerCase()
switch (ext) {
case 'js':
case 'jsx':
case 'ts':
case 'tsx':
const { javascript } = await import('@codemirror/lang-javascript')
return [
javascript({
jsx: ext.substring(2, 3) === 'x',
typescript: ext.substring(0, 2) === 'js'
})
]
break
case 'css':
case 'scss':
const { sass } = await import('@codemirror/lang-sass')
return [sass()]
break
case 'html':
const { html } = await import('@codemirror/lang-html')
return [html()]
break
case 'py':
const { python } = await import('@codemirror/lang-python')
return [python()]
break
case 'go':
const { go } = await import('@codemirror/lang-go')
return [go()]
break
case 'json':
const { json } = await import('@codemirror/lang-json')
return [json()]
break
case 'md':
const { markdown } = await import('@codemirror/lang-markdown')
return [markdown()]
break
case 'yaml':
const { yaml } = await import('@codemirror/lang-yaml')
return [yaml()]
break
case 'vue':
const { vue } = await import('@codemirror/lang-vue')
return [vue()]
break
case 'xml':
const { xml } = await import('@codemirror/lang-xml')
return [xml()]
break
default:
return []
}
}
onMounted(async () => {
vFile.value = currentFiles.value[0]
const fileName = currentFiles.value[0].name
codeData.value = decoder.decode(
await miaoFetchApi.getFile(currentFiles.value[0], {
axiosOption: {
headers: {
'Cache-Control': 'no-cache'
}
}
}).response
)
bakData.value = codeData.value
extensions.value = [...(await getLangExtensions(fileName))]
rootRef.value?.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 's') {
event.preventDefault()
handleSave()
}
})
})
</script>
<style scoped lang="scss">
.codemirror-container {
width: 100%;
height: 100%;
overflow-y: hidden;
.codemirror-container-top {
height: 25px;
display: flex;
border-bottom: 1px solid #000;
}
.codemirror-container-btn {
height: 25px;
padding: 0 10px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
user-select: none;
background-color: none;
transition: background-color 0.25s;
&:hover {
background-color: #e8e8e8;
}
}
.codemirror-container-main {
height: calc(100% - 25px);
position: relative;
}
}
.codemirror-container-dark {
background-color: rgb(30, 30, 30);
}
.codemirror-container-light {
background-color: rgb(255, 255, 255);
}
</style>
+24
View File
@@ -0,0 +1,24 @@
// @ts-nocheck
import { registerComponentOption } from '@/class/PluginCenter'
import { PluginExportType } from '@/types/type'
const pluginConfig: registerComponentOption = {
key: 'miaoCrypto',
name: '文件加密',
getComponent: async () => (await import('./miaoCrypto.vue')).default,
filter: (vDirs, vFiles) => {
return true
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('txt')
},
disable: true
}
export default pluginConfig
export const type = PluginExportType.component
+19
View File
@@ -0,0 +1,19 @@
import aesJs from 'aes-js'
import aes_worker from '../workers/aes-worker?worker'
/**
* 使用worker进行aes加密
* @param {any} key:string 须保证长度正确
* @param {any} data:Uint8Array
* @returns {Uint8Array}
*/
export default (key: string, data: Uint8Array) => {
const key_u8i = aesJs.utils.utf8.toBytes(key)
return new Promise<Uint8Array>((resolve) => {
const aesWorker = new aes_worker()
aesWorker.onmessage = (event) => {
resolve(event.data)
}
aesWorker.postMessage({ data, key: key_u8i })
})
}
+208
View File
@@ -0,0 +1,208 @@
<template>
<miao-drop-handler @on-virtual-files="handleDropVirtualFiles">
<div class="miaoCrypto-container">
<div class="miaoCrypto-container-encrypt miaoCrypto-container-item">
<n-scrollbar style="max-height: 100%">
<div
v-for="item of workItemList"
:key="item.id"
class="miaoCrypto-container-item-item">
<div
class="miaoCrypto-container-item-percentBar"
:style="{
zIndex: 0,
width:
item.getBufferProgress !== -1
? `${item.getBufferProgress * 100}%`
: `0%`,
backgroundColor: 'blue'
}"></div>
<div
class="miaoCrypto-container-item-percentBar"
:style="{
zIndex: 1,
width: item.status === 'finish' ? `100%` : `0%`,
backgroundColor: 'green'
}"></div>
<span class="miaoCrypto-container-item-content">
{{ item.name }}{{ item.status }}
</span>
</div>
</n-scrollbar>
</div>
<!-- <div class="miaoCrypto-container-decrypt miaoCrypto-container-item"></div> -->
</div>
</miao-drop-handler>
</template>
<script setup lang="ts">
import { NScrollbar } from 'naive-ui'
import { onUnmounted, ref, shallowReactive } from 'vue'
import aes_ctr_worker from './hooks/AES'
import { VirtualFile } from '@/class/VirtualDirectory'
import miaoDropHandler from '@/components/miaoDropHandler.vue'
import { reactive } from 'vue'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import { onMounted } from 'vue'
import { computed } from 'vue'
const miaoFetchApi = useMiaoFetchApi()
interface workItem {
id: number
status: 'wait' | 'process' | 'finish' | 'error'
buffer?: Uint8Array
getBuffer: () => Promise<Uint8Array>
getBufferProgress: number
getBufferAbort?: () => void
isGettingBuffer: boolean
name: string
}
class Work {
constructor(func: (key: string, data: Uint8Array) => Promise<Uint8Array>) {
this.queue = []
this.func = func
this.abortLoop = false
}
queue: workItem[]
func: (key: string, data: Uint8Array) => Promise<Uint8Array>
abortLoop: boolean
get isBusy() {
return this.queue.filter((v) => v.status === 'process').length > 0
}
async funcLoop() {
if (this.isBusy || this.abortLoop) {
return
}
const currentItem = this.queue.filter((v) => v.status === 'wait')[0]
if (currentItem === undefined || currentItem.buffer === undefined) {
return
}
currentItem.status = 'process'
try {
currentItem.buffer = await this.func(key.value, currentItem.buffer)
currentItem.status = 'finish'
} catch (e) {
currentItem.status = 'error'
}
this.funcLoop()
}
async getBufferLoop() {
const _list = this.queue.filter(
(v) => v.isGettingBuffer === false && v.buffer === undefined
)
if (_list.length === 0 || this.abortLoop) {
return
}
const item = _list[0]
item.isGettingBuffer = true
try {
item.buffer = await item.getBuffer()
} catch (e) {
item.status = 'error'
}
item.isGettingBuffer = false
this.funcLoop()
this.getBufferLoop()
}
push(itemList: workItem[]) {
this.queue.push(...itemList)
this.getBufferLoop()
}
pushVFiles(itemList: VirtualFile[]) {
this.push(
itemList.map((v) => {
const item: workItem = shallowReactive({
id: Math.random(),
status: 'wait',
name: v.name,
getBuffer: () => {
const { response, abort } = miaoFetchApi.getFile(v, {
onProcess(percent) {
item.getBufferProgress = percent
}
})
item.getBufferAbort = abort
return response
},
isGettingBuffer: false,
getBufferProgress: 0
})
return item
})
)
}
}
const encryptWork = reactive(new Work(aes_ctr_worker))
// const decryptWork = reactive(new Work(aes_ctr_worker))
const workItemList = computed(() => [
...encryptWork.queue
// ...decryptWork.queue
])
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const key = ref('ABlockIs16Bytes!')
const handleDropVirtualFiles = (vFiles: VirtualFile[]) => {
encryptWork.pushVFiles(vFiles)
}
onMounted(() => {
encryptWork.pushVFiles(currentFiles.value)
})
onUnmounted(() => {
encryptWork.abortLoop = true
encryptWork.queue.forEach((item) => {
item.getBufferAbort && item.getBufferAbort()
})
})
</script>
<style scoped lang="scss">
.miaoCrypto-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
&-item {
max-height: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
&-item {
margin: 10px auto;
width: 95%;
height: 40px;
background-color: #fff;
box-shadow:
0 0 15px -7px rgba(0, 0, 0, 0.1725490196),
0 0 3px 1px #43434359;
border-radius: 5px;
position: relative;
.miaoCrypto-container-item-percentBar {
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 5px;
}
.miaoCrypto-container-item-content {
position: relative;
z-index: 3;
}
}
}
}
</style>
@@ -0,0 +1,9 @@
import aesJs from 'aes-js'
self.onmessage = async function (event) {
const data = event.data.data as Uint8Array
const key = event.data.key as Uint8Array
var aesCtr = new aesJs.ModeOfOperation.ctr(key, new aesJs.Counter(7058))
const encryptData = aesCtr.encrypt(data)
self.postMessage(encryptData)
}
@@ -0,0 +1,76 @@
<template>
<div
class="miao-directory-container-bottom"
:style="{
backgroundColor: index % 2 === 1 ? 'rgb(61 61 61)' : 'rgb(162 162 162)'
}">
<div class="container-bottom-item">
<div class="container-bottom-item-icon">
<add-outline
:style="{
color: index % 2 === 0 ? 'rgb(0 0 0)' : 'rgb(255 255 255)'
}"
@click="emit('add-new-item')" />
</div>
</div>
<div class="container-bottom-item">
<div class="container-bottom-item-icon">
<cloud-upload-outline
:style="{
color: index % 2 === 0 ? 'rgb(0 0 0)' : 'rgb(255 255 255)'
}"
@click="emit('pick-files-upload')" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { AddOutline, CloudUploadOutline } from '@vicons/ionicons5'
defineProps<{
index: number
}>()
const emit = defineEmits<{
'add-new-item': []
'pick-files-upload': []
}>()
</script>
<style scoped lang="scss">
.miao-directory-container-bottom {
height: 60px;
background-color: bisque;
display: flex;
justify-content: space-around;
align-items: center;
user-select: none;
box-shadow: 0px 1px 10px #888888;
z-index: 2;
.container-bottom-item {
height: 40px;
width: 40px;
display: flex;
justify-content: center;
align-items: center;
.container-bottom-item-icon {
svg {
width: 30px;
height: 30px;
transition: width 0.3s ease;
transition: height 0.3s ease;
cursor: pointer;
}
&:hover {
svg {
width: 40px;
height: 40px;
}
}
}
}
}
</style>
@@ -0,0 +1,304 @@
<template>
<miao-draggable-div class="miao-item" :isDraggable="!draggable" ref="itemRef">
<miaoContextMenu :options="dropDownOptions ?? []" :touch-time-out="500" @select="handleDropdownSelect">
<div class="item-main" :class="selected ? 'item-main-selected' : ''" ref="mainRef">
<div class="item-main-front" :style="{ backgroundColor: props.color }"></div>
<Icon class="item-main-icon" v-show="clientWidth > 250"></Icon>
<div class="item-main-info">
<div class="item-main-info-name">
<Icon class="item-main-info-name-icon" v-if="clientWidth <= 250"></Icon>
<n-ellipsis :style="{ maxWidth: clientWidth > 250 ? '80%' : '75%' }">
{{ name }}
</n-ellipsis>
</div>
<div class="item-main-info-date" v-if="time">
{{ time }}
</div>
</div>
<miao-draggable-div class="item-main-dragBtn" :isDraggable="draggable" @on-drag-start="handleDragStart">
<ReorderFourOutline></ReorderFourOutline>
</miao-draggable-div>
</div>
</miaoContextMenu>
</miao-draggable-div>
</template>
<script setup lang="ts">
import miaoContextMenu from '@/components/miaoContextMenu.vue'
import {
DocumentText,
FileTray,
Help,
Image,
LogoChrome,
LogoCss3,
LogoJavascript,
LogoVue,
PlayCircle,
Disc,
ReorderFourOutline
} from '@vicons/ionicons5'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import dateFormatter from '@/utils/dateFormatter'
import { NEllipsis } from 'naive-ui'
import { DropdownOption } from 'naive-ui/es/dropdown'
import miaoDraggableDiv from '@/components/miaoDraggableDiv.vue'
// import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
// const miaoFetchApi = useMiaoFetchApi()
const props = defineProps<{
item: {
name: string
type: 'directory' | 'file'
stats: {
mtimeMs: number
}
}
name?: string
color: string
dropDownOptions?: DropdownOption[]
selected?: boolean
}>()
const emit = defineEmits<{
download: []
delete: []
rename: []
dragStart: [e: DragEvent]
onSelected: []
plugin: [plugin: string]
}>()
const mainRef = ref<HTMLDivElement>()
const draggable = ref(true)
// 使用ResizeObserver实现
// 什么?性能问题?这玩意性能问题多着呢,轮不到它。
const clientWidth = ref<number>(666)
const name = props.name ?? props.item.name
const observer = new ResizeObserver((entries) => {
for (let entry of entries) {
clientWidth.value = entry.contentRect.width
}
})
const time = computed(() => {
return dateFormatter(
new Date(props.item.stats.mtimeMs),
'yyyy-MM-dd hh:mm:ss'
)
})
// 应该将所有事件emit出去才对
const handleDropdownSelect = (key: string) => {
if (key === 'rename') {
emit('rename')
} else if (key === 'download') {
emit('download')
} else if (key === 'delete') {
emit('delete')
} else if (key === 'select') {
emit('onSelected')
} else if (key.startsWith('plugin:')) {
const plugin = key.split(':')[1]
emit('plugin', plugin)
}
}
const Icon = computed(() => {
try {
if (props.item.type === 'directory') {
return FileTray
} else if (props.item.type === 'file') {
const aft = props.item.name.split('.').at(-1)?.toLowerCase()
if (!aft) {
throw 1
} else if (
['txt', 'doc', 'docs', 'docx', 'xls', 'xlsx'].includes(aft)
) {
return DocumentText
} else if (['mp3', 'wav', 'ape', 'acc', 'ogg', 'flac'].includes(aft)) {
return Disc
} else if (
[
'xbm',
'tif',
'pjp',
'svgz',
'jpg',
'jpeg',
'ico',
'tiff',
'gif',
'svg',
'jfif',
'webp',
'png',
'bmp',
'pjpeg',
'avif'
].includes(aft)
) {
return Image
} else if (['mp4', 'm2v', 'mkv'].includes(aft)) {
return PlayCircle
} else if (['js', 'jsx', 'ts', 'tsx'].includes(aft)) {
return LogoJavascript
} else if (['html'].includes(aft)) {
return LogoChrome
} else if (['css', 'scss', 'less'].includes(aft)) {
return LogoCss3
} else if (['vue'].includes(aft)) {
return LogoVue
}
return DocumentText
}
} catch {
return Help
}
})
const itemRef = ref()
const handleDragStart = (e: DragEvent) => {
emit('dragStart', e)
}
onMounted(() => {
if (mainRef.value) {
observer.observe(mainRef.value)
}
})
onBeforeUnmount(() => {
if (mainRef.value) {
observer.unobserve(mainRef.value)
}
})
</script>
<style lang="scss" scoped>
.miao-item {
cursor: pointer;
// width: calc(100% - 20px);
width: 100%;
// min-width: 170px;
// aspect-ratio: 66/9;
height: 50px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
// margin: 10px 10px;
.item-main {
position: relative;
width: 100%;
height: 100%;
background-color: rgb(255, 255, 255);
border-radius: 7px;
display: flex;
align-items: center;
transition: background-color 0.3s ease;
overflow-x: hidden;
.item-main-front {
height: 100%;
min-width: 10px;
border-radius: 7px 0 0 7px;
background-color: rgb(0, 0, 0);
}
.item-main-icon {
min-width: 30px;
margin-left: 10px;
height: 30px;
width: 30px;
color: #00403e;
transition: color 0.15s ease-out;
}
.item-main-info {
margin-left: 10px;
height: 100%;
width: calc(100% - 10px);
display: flex;
flex-direction: column;
&-name {
position: relative;
user-select: none;
display: flex;
align-items: center;
margin-top: 3px;
&-icon {
color: #00403e;
width: 20px;
margin-right: 5px;
transition: color 0.15s ease-out;
}
}
&-date {
user-select: none;
font-size: small;
text-wrap: nowrap;
}
}
.item-main-option {
height: 100%;
width: 20px;
position: absolute;
right: 0;
border-radius: 0 7px 7px 0;
background-color: rgba(0, 0, 0, 0);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transition: background-color 0.3s;
.item-main-option-icon {
color: #000;
transition: color 0.15s;
}
&:hover,
&:active {
background-color: #8b8b8b;
.item-main-option-icon {
color: #fff;
}
}
}
&:hover,
&:active {
background-color: #d2d2d2;
}
}
.item-main-selected {
background-color: rgba(0, 128, 207, 0.32);
&:hover {
background-color: rgba(0, 128, 207, 0.64);
}
}
}
.item-main-dragBtn {
width: 20px;
height: 20px;
position: absolute;
right: 5px;
}
</style>
@@ -0,0 +1,188 @@
<template>
<div
class="miao-directory-container-items"
:style="{
backgroundColor: index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)'
}">
<n-scrollbar ref="scrollbarRef" style="max-height: 100%">
<miao-lazy-div
class="miao-directory-container-items-container"
v-for="dir in showData_directory"
:key="dir.id"
min-height="50px"
:disable-lazy="props.index < 20"
margin="10px">
<miaoDirectoryItem
:item="dir"
:color="props.color"
:selected="selectedItem.includes(dir)"
:drop-down-options="itemDropDownOptions(dir).value"
@delete="handleItemDelete(dir)"
@plugin="(p) => handleItemUsePlugin(dir)(p)"
@click="handleItemClick(dir)"
@drag-start="(e) => handleItemDragStart(dir)(e)"
@rename="handleItemRename(dir)"
@on-selected="handleItemSelect(dir)" />
</miao-lazy-div>
<miao-lazy-div
class="miao-directory-container-items-container"
v-for="file in showData_files"
:key="file.id"
min-height="50px">
<miaoDirectoryItem
:item="file"
:color="props.color"
:selected="selectedItem.includes(file)"
:drop-down-options="itemDropDownOptions(file).value"
@click="handleItemClick(file)"
@download="handleItemDownload(file)"
@delete="handleItemDelete(file)"
@drag-start="(e) => handleItemDragStart(file)(e)"
@rename="handleItemRename(file)"
@plugin="(p) => handleItemUsePlugin(file)(p)"
@on-selected="handleItemSelect(file)" />
</miao-lazy-div>
</n-scrollbar>
</div>
</template>
<script setup lang="ts">
import { NScrollbar } from 'naive-ui'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import miaoDirectoryItem from './miaoDirectoryItem.vue'
import miaoLazyDiv from '@/components/miaoLazyDiv.vue'
import { computed, ref } from 'vue'
import usePluginCenter from '@/hooks/usePluginCenter'
import { renderIcon } from '@/utils/miaoTools'
const pluginCenter = usePluginCenter()
const props = defineProps<{
color: string
index: number
showData_directory: VirtualDirectory[]
showData_files: VirtualFile[]
selectedItem: (VirtualFile | VirtualDirectory)[]
}>()
const emit = defineEmits<{
'item-click': [item: VirtualDirectory | VirtualFile]
'item-download': [item: VirtualFile]
'item-delete': [item: VirtualDirectory | VirtualFile]
'item-drag-start': [_event: DragEvent, item: VirtualDirectory | VirtualFile]
'item-rename': [item: VirtualDirectory | VirtualFile]
'item-select': [item: VirtualDirectory | VirtualFile]
}>()
const scrollbarRef = ref()
const itemDropDownOptions = (item: VirtualDirectory | VirtualFile) =>
computed(() => {
const items = [...props.selectedItem]
const selected = props.selectedItem.includes(item)
if (!selected) {
items.push(item)
}
const vDirs = items.filter((v) => v.type === 'directory')
const vFiles = items.filter((v) => v.type === 'file')
const options = [
{
label: '重命名',
key: 'rename'
},
{
label: '删除',
key: 'delete'
}
]
if (item.type === 'file') {
options.push(
...[
{
label: '下载',
key: 'download'
}
]
)
}
if (selected === false) {
options.push(
...[
{
label: '多选:选择',
key: 'select'
}
]
)
} else {
options.push(
...[
{
label: '多选:取消',
key: 'select'
}
]
)
}
options.push(
...pluginCenter.getUsablePlugin(vDirs, vFiles).map((v) => ({
label: `插件:${v.name}`,
key: `plugin:${v.key}`,
icon: v.icon ? renderIcon(v.icon) : undefined
}))
)
return options
})
const handleItemClick = (item: VirtualDirectory | VirtualFile) => {
emit('item-click', item)
}
const handleItemDownload = (item: VirtualFile) => {
emit('item-download', item)
}
const handleItemDelete = (item: VirtualDirectory | VirtualFile) => {
emit('item-delete', item)
}
const handleItemDragStart =
(item: VirtualDirectory | VirtualFile) => (_event: DragEvent) => {
emit('item-drag-start', _event, item)
}
const handleItemRename = (item: VirtualDirectory | VirtualFile) => {
emit('item-rename', item)
}
const handleItemSelect = (item: VirtualDirectory | VirtualFile) => {
emit('item-select', item)
}
const handleItemUsePlugin =
(item: VirtualDirectory | VirtualFile) => (plugin: string) => {
const items = [...props.selectedItem]
const selected = props.selectedItem.includes(item)
if (!selected) {
items.push(item)
}
const vDirs = items.filter((v) => v.type === 'directory')
const vFiles = items.filter((v) => v.type === 'file')
pluginCenter.usePlugin(plugin, vDirs, vFiles)
}
defineExpose({
scrollTo: (...args: any) => scrollbarRef.value.scrollTo(...args)
})
</script>
<style scoped lang="scss">
.miao-directory-container-items {
width: 100%;
flex: 1;
min-height: 0;
.miao-directory-container-items-container {
margin: 10px;
}
}
</style>
@@ -0,0 +1,204 @@
<template>
<div
class="miao-directory-container-top"
:style="{
backgroundColor: index % 2 === 0 ? '#f8f8f8' : 'rgb(240 240 240)'
}">
<div
class="container-top-colorfulBar"
:style="{ backgroundColor: props.color }"></div>
<div class="container-top-breadcrumb">
<n-scrollbar
ref="scrollbarRef"
:x-scrollable="true"
class="container-top-breadcrumb-scroll"
@wheel="handleWheel">
<div class="container-top-breadcrumb-container">
<n-breadcrumb
separator=">"
class="sb-nn"
style="margin-left: 5px">
<n-breadcrumb-item :clickable="false">
<n-icon size="18">
<CloudOutline />
</n-icon>
</n-breadcrumb-item>
<n-breadcrumb-item
v-for="dir in currentDirectory?.getParents"
@click="handleClickBreadcrumbItem(dir)"
:clickable="true">
<n-ellipsis class="!max-w-[100px]">
{{ dir.name }}
</n-ellipsis>
</n-breadcrumb-item>
</n-breadcrumb>
</div>
</n-scrollbar>
</div>
<div class="container-top-tools">
<n-icon
class="container-top-tools-item"
size="15"
@click="$emit('back')">
<ChevronBackOutline />
</n-icon>
<n-icon
class="container-top-tools-item"
size="14"
@click="$emit('reload')">
<ReloadOutline class="container-top-tools-item-reload" />
</n-icon>
<n-dropdown
trigger="click"
:options="openMenuOption"
@select="(key: string) => emit('menuSelect', key)">
<n-icon class="container-top-tools-item" size="14">
<EllipsisVertical />
</n-icon>
</n-dropdown>
</div>
</div>
</template>
<script setup lang="ts">
import {
CloudOutline,
ChevronBackOutline,
ReloadOutline,
EllipsisVertical
} from '@vicons/ionicons5'
import VirtualDirectory from '@/class/VirtualDirectory'
import { ref } from 'vue'
import { NScrollbar } from 'naive-ui/es/scrollbar';
const props = defineProps<{
color: string
index: number
currentDirectory: VirtualDirectory | undefined
openMenuOption: {
key: string
label: string
[key: string]: any
}[]
}>()
const emit = defineEmits<{
back: []
reload: []
menuSelect: [key: string]
'set-current-directory': [virtualDirectory: VirtualDirectory]
}>()
const scrollbarRef = ref<InstanceType<typeof NScrollbar>>()
const handleClickBreadcrumbItem = (virtualDirectory: VirtualDirectory) => {
emit('set-current-directory', virtualDirectory)
}
const handleWheel = (event: WheelEvent) => {
event.preventDefault()
event.stopPropagation()
if (scrollbarRef.value) {
scrollbarRef.value.scrollBy({
left: event.deltaY,
behavior: "smooth"
})
}
}
defineExpose({
// @ts-ignore
scrollTo: (...args: any[]) => scrollbarRef.value?.scrollTo(...args)
// getRef: () => scrollbarRef.value
})
</script>
<style scoped lang="scss">
.miao-directory-container-top {
height: 30px;
width: 100%;
user-select: none;
position: relative;
display: flex;
align-items: center;
overflow: hidden;
background-color: #f1f1f1;
.container-top-colorfulBar {
height: 70%;
width: 5px;
margin-left: 10px;
border-radius: 3px 0 0 3px;
}
.container-top-breadcrumb {
position: relative;
height: 70%;
width: calc(100% - 95px);
display: flex;
justify-content: flex-start;
align-items: center;
// overflow: hidden;
overflow-y: hidden;
overflow-x: hidden;
background-color: #ffffff;
padding: 0 5px;
border-radius: 0 3px 3px 0;
.container-top-breadcrumb-container {
// position: absolute;
height: 21px;
// right: 0;
// top: 0;
min-width: 100%;
display: flex;
justify-content: flex-start;
align-items: center;
overflow: hidden;
}
}
.container-top-tools {
position: relative;
height: 100%;
// width: 70px;
padding: 0 5px;
display: flex;
justify-content: space-around;
align-items: center;
.container-top-tools-item {
width: 20px;
height: 20px;
margin: 0 1px;
display: flex;
justify-content: center;
align-items: center;
background-color: #ffffff00;
border-radius: 5px;
transition: background-color 0.3s;
cursor: pointer;
.container-top-tools-item-reload {
transition: transform 0.6s ease-in-out;
}
&:hover {
background-color: rgba(128, 128, 128, 0.4);
}
&:active .container-top-tools-item-reload {
transform: rotate(360deg);
}
}
}
}
</style>
<style lang="scss">
.container-top-breadcrumb-scroll {
.n-scrollbar-rail {
transform: translateY(5px);
}
}
</style>
+23
View File
@@ -0,0 +1,23 @@
import { registerComponentOption } from '@/class/PluginCenter'
import FileTray from '@vicons/ionicons5/es/FileTray'
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 () => MiaoDirectory,
filter: (vDirs, vFiles) => {
if (vFiles.length) {
return false
}
return vDirs.length === 1
}
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
@@ -0,0 +1,40 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import { PluginExportType } from '@/types/type'
import { watch } from 'vue'
const pluginConfig: Plugin = {
name: '初始化完成之后打开miaoDirectory',
filter: () => {
return false
},
disable: false,
group: [PluginGroup.immediately],
func: (_, __, hooks) => {
const { getPluginCenter, getRootVDirectory, getViews } = hooks
const views = getViews()
const pc = getPluginCenter()
const root = getRootVDirectory()
setTimeout(() => {
if (views.length === 0) {
pc.usePlugin('miaoDirectory', [root])
}
watch(
() => views.length,
() => {
if (views.length === 0) {
pc.usePlugin('miaoDirectory', [root])
}
}
)
pc.deregister('miaoDirectory_onInit')
})
},
priority: -1
}
export default pluginConfig
export const type = PluginExportType.default
export const key = 'miaoDirectory_onInit'
+660
View File
@@ -0,0 +1,660 @@
<template>
<miao-drop-handler
@on-virtual-directory="handleDropVDirectory"
@on-files="handleDropFiles"
@on-virtual-files="handleDropVirtualFiles">
<miaoAlertTipProvider ref="miaoAlertTip">
<div class="miao-directory-item-container" ref="rootDomRef">
<!-- 顶部面包屑导航和工具栏 -->
<miao-directory-top
ref="miaoDirectoryTopRef"
:color="props.color"
:current-directory="currentDirectory"
@back="handleBack"
:open-menu-option="topBarMenuOption"
@reload="handleReload"
@exit="emit('exit')"
:index="index"
@set-current-directory="setCurrentDirectory"
@search="handleSearch"
@menu-select="handleTopBarMenuSelect" />
<!-- 文件和文件夹列表 -->
<miao-directory-items
ref="miaoDirectoryItemRef"
:color="props.color"
:showData_directory="showData_directory"
:showData_files="showData_files"
:selected-item="selectedItem.value"
:index="index"
@item-click="handleItemClick"
@item-download="handleItemDownload"
@item-delete="handleItemDelete"
@item-drag-start="handleItemDragStart"
@item-rename="handleItemRename"
@item-select="handleItemSelect" />
<!-- 底部工具栏 -->
<miao-directory-bottom
:index="index"
@add-new-item="handleAddNewItem"
v-if="false"
@pick-files-upload="handlePickFilesUpload" />
<miao-mask v-model:show="showModel"></miao-mask>
<miao-popup-input ref="popupInput"></miao-popup-input>
</div>
</miaoAlertTipProvider>
</miao-drop-handler>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import Config from '@/config'
import useMiaoFetchApi from '@/hooks/useMiaoFetchApi'
import useDataBus from '@/hooks/useDataBus'
import useUploadQueue from '@/hooks/useUploadQueue'
import useVirtualPages from '@/hooks/useVirtualPages'
import { filePicker, renderIcon } from '@/utils/miaoTools'
import uniq from 'lodash/uniq'
import miaoDirectoryTop from './components/miaoDirectoryTop.vue'
import MiaoDirectoryItems from './components/miaoDirectoryItemsContainer.vue'
import MiaoDirectoryBottom from './components/miaoDirectoryBottom.vue'
import MiaoMask from '@/components/miaoMask.vue'
import MiaoPopupInput from '@/components/miaoPopupInput.vue'
import MiaoDropHandler from '@/components/miaoDropHandler.vue'
import miaoAlertTipProvider from '@/components/miaoAlertTipProvider.vue'
import {
CloseOutline,
SearchOutline,
FilterOutline,
CheckmarkDoneOutline,
DocumentText,
ExtensionPuzzleOutline,
FileTray,
AddOutline,
CloudUploadOutline
} from '@vicons/ionicons5'
import usePluginCenter from '@/hooks/usePluginCenter'
const { baseUrl, api } = Config
const dataBus = useDataBus()
const views = useVirtualPages()
const uploadQueue = useUploadQueue()
const miaoFetchApi = useMiaoFetchApi()
const pluginCenter = usePluginCenter()
const props = defineProps<{
color: string
id: number
}>()
const emit = defineEmits<{
exit: []
}>()
const popupInput = ref<InstanceType<typeof MiaoPopupInput>>()
// 是否显示模态框
const showModel = ref(0)
const searchText = ref<string>('')
const showDirs = ref<boolean>(true)
const showFiles = ref<boolean>(true)
const miaoDirectoryItemRef = ref()
const miaoDirectoryTopRef = ref<InstanceType<typeof miaoDirectoryTop>>()
const miaoAlertTip = ref<InstanceType<typeof miaoAlertTipProvider>>()
class SelectedItem {
constructor() {
this._selectedItem = []
}
_selectedItem: (VirtualDirectory | VirtualFile)[]
push(item: VirtualDirectory | VirtualFile) {
this._selectedItem = uniq([item, ...this._selectedItem])
}
pushArray(items: (VirtualDirectory | VirtualFile)[]) {
this._selectedItem = uniq([...items, ...this._selectedItem])
}
remove(item: VirtualDirectory | VirtualFile) {
this._selectedItem = this._selectedItem.filter((v) => v !== item)
}
/**
* 仅保留filter为true的元素, filter默认为()=>false
*/
clear(filter?: (item: VirtualDirectory | VirtualFile) => boolean) {
filter || (filter = (__) => false)
this._selectedItem = this.value.filter(filter)
}
// 保证内容仍然存在于dir中且各个元素不重复
get value() {
const _filterList = [
...(currentDirectory.value?.directories ?? []),
...(currentDirectory.value?.files ?? [])
]
return uniq(this._selectedItem).filter((v) => {
if (currentDirectory.value === undefined) {
return false
}
// @ts-ignore
return _filterList.includes(v)
})
}
}
// 已选择的item,用于多选功能
const selectedItem = reactive(new SelectedItem())
const currentDirectories = defineModel<VirtualDirectory[]>(
'currentDirectories',
{
required: true
}
)
// 目前展示的Dir
const currentDirectory = computed(() => {
return currentDirectories.value[0]
})
// 当前组件在views中的序号
const index = computed(() => {
return views.getIndex(props.id)
})
// 用于展示的数据,后续会增加排序等操作
const showData_directory = computed<VirtualDirectory[]>(() => {
if (showDirs.value === false) {
return []
}
let data = currentDirectory.value?.directories
? [...currentDirectory.value?.directories]
: []
if (searchText.value !== '') {
data = data.filter((vd) => vd.name.includes(searchText.value))
}
return data.sort((a, b) => a.name.localeCompare(b.name))
})
const showData_files = computed<VirtualFile[]>(() => {
if (showFiles.value === false) {
return []
}
let data = currentDirectory.value?.files
? [...currentDirectory.value?.files]
: []
if (searchText.value !== '') {
data = data.filter((vd) => vd.name.includes(searchText.value))
}
return data.sort((a, b) => a.name.localeCompare(b.name))
})
const topBarMenuOption = computed(() => {
return [
{
label: '新建',
key: 'create',
icon: renderIcon(AddOutline)
},
{
label: '上传',
key: 'upload',
icon: renderIcon(CloudUploadOutline)
},
{
label: '搜索',
key: 'search',
icon: renderIcon(SearchOutline)
},
searchText.value !== ''
? {
label: '重置搜索',
key: 'searchReset',
icon: renderIcon(SearchOutline)
}
: undefined,
// selectedItem.value.length !== 0
// ? {
// label: '重置多选',
// key: 'selectedItemReset',
// icon: renderIcon(CheckmarkDoneOutline)
// }
// : undefined,
{
label: '筛选',
key: 'filter',
children: [
{
label: `${showDirs.value ? '隐藏' : '显示'}文件夹`,
key: 'showDirs',
icon: renderIcon(FileTray)
},
{
label: `${showFiles.value ? '隐藏' : '显示'}文件`,
key: 'showFiles',
icon: renderIcon(DocumentText)
}
],
icon: renderIcon(FilterOutline)
},
{
label: '多选',
key: 'multiSelect',
children: [
{
label: `全选`,
key: 'multiSelect:all'
},
{
label: `全不选`,
key: 'multiSelect:reset'
},
{
label: `反选`,
key: 'multiSelect:inversion'
},
selectedItem.value.length === 2
? {
label: `区间内全选`,
key: 'multiSelect:range'
}
: undefined
].filter((v) => v !== undefined),
icon: renderIcon(CheckmarkDoneOutline)
},
{
label: '插件',
key: 'plugin',
children: [
...pluginCenter
.getUsablePlugin([currentDirectories.value[0]], [])
.map((v) => ({
label: `${v.name}`,
key: `plugin:${v.key}`,
icon: v.icon ? renderIcon(v.icon) : undefined
}))
],
icon: renderIcon(ExtensionPuzzleOutline)
},
{
label: '关闭',
key: 'close',
icon: renderIcon(CloseOutline)
}
].filter((v) => v !== undefined)
})
// 设置显示的dir
const setCurrentDirectory = async (virtualDirectory: VirtualDirectory) => {
try {
// 若没有缓存数据则同步刷新,有缓存数据则异步刷新
if (!virtualDirectory.isUpdated) {
await virtualDirectory.update()
} else {
virtualDirectory.update()
}
if (currentDirectories.value[0] !== virtualDirectory) {
selectedItem.clear()
handleSearchReset()
nextTick(() => {
miaoDirectoryItemRef.value.scrollTo({
top: 0,
behavior: 'instant'
})
// miaoDirectoryTopRef.value &&
// miaoDirectoryTopRef.value.scrollTo({
// top: 0,
// behavior: 'instant'
// })
miaoDirectoryTopRef.value?.scrollTo({
top: 0,
left: 999,
behavior: 'instant'
})
})
}
currentDirectories.value[0] = virtualDirectory
} catch (e) {
miaoAlertTip.value?.alertTip(`获取文件夹内容出错`, {
type: 'error',
timeout: 3000
})
}
}
const reload = () => {
setCurrentDirectory(currentDirectory.value)
}
const openUrl = (
href: string,
config?: {
download?: string
target?: string
}
) => {
let { target, download } = config ?? {}
const a = document.createElement('a')
a.setAttribute('href', href)
a.setAttribute('target', target ?? '_blank')
download && a.setAttribute('download', download)
a.click()
}
const handleTopBarMenuSelect = (key: string) => {
if (key === 'close') {
emit('exit')
} else if (key === 'search') {
handleSearch()
} else if (key === 'upload') {
handlePickFilesUpload()
} else if (key === 'create') {
handleAddNewItem()
} else if (key === 'searchReset') {
handleSearchReset()
// } else if (key === 'selectedItemReset') {
// selectedItem.clear()
} else if (key === 'showDirs') {
showDirs.value = !showDirs.value
showDirs.value === false &&
selectedItem.clear((v) => v.type !== 'directory')
} else if (key === 'showFiles') {
showFiles.value = !showFiles.value
showFiles.value === false && selectedItem.clear((v) => v.type !== 'file')
} else if (key.startsWith('plugin:')) {
const pluginName = key.split(':')[1]
pluginCenter.usePlugin(pluginName, [currentDirectory.value], [])
} else if (key.startsWith('multiSelect:')) {
const opName = key.split(':')[1]
switch (opName) {
case 'all':
selectedItem.pushArray([
...showData_directory.value,
...showData_files.value
])
break
case 'reset':
selectedItem.clear()
break
case 'inversion':
const sArr = selectedItem.value
const arr = [
...showData_directory.value,
...showData_files.value
].filter((i) => !sArr.includes(i))
selectedItem.clear()
selectedItem.pushArray(arr)
break
case 'range':
const _sArr = selectedItem.value
const _arr = [...showData_directory.value, ...showData_files.value]
const _nArr = []
let _counter = 0
for (const i of _arr) {
if (_sArr.includes(i)) {
_counter++
}
if (_counter === 1) {
_nArr.push(i)
} else if (_counter === 2) {
_nArr.push(i)
break
}
}
selectedItem.clear()
selectedItem.pushArray(_nArr)
break
}
}
}
const handleBack = () => {
if (currentDirectory.value?.parent) {
setCurrentDirectory(currentDirectory.value.parent)
}
}
const handleReload = () => {
reload()
}
const handleItemClick = (item: VirtualDirectory | VirtualFile) => {
if (selectedItem.value.length > 0) {
handleItemSelect(item)
} else {
if (item.type === 'file') {
const useablePlugins = pluginCenter.getUsablePlugin([], [item])
if (useablePlugins.length > 0) {
pluginCenter.usePlugin(useablePlugins[0].key, [], [item])
} else {
openUrl(`${baseUrl}${api.get}${item.path}`)
}
} else if (item.type === 'directory') {
setCurrentDirectory(item)
}
}
}
const handleItemDownload = (item: VirtualFile) => {
openUrl(`${baseUrl}${api.get}${item.path}`, {
download: item.name
})
}
const handleItemDelete = async (_item: VirtualDirectory | VirtualFile) => {
for (let item of uniq([...selectedItem.value, _item])) {
(async () => {
const success = await item.delete();
if (success) {
reload();
}
})();
}
}
const handleItemRename = async (item: VirtualDirectory | VirtualFile) => {
const popupRes = await popupInput.value?.popup({
title: '重命名',
inputValue: item.name,
inputProps: {
placeholder: item.name
},
options: [
{
label: '取消',
key: false,
type: 'error'
},
{
label: '确认',
key: true,
type: 'success'
}
]
})
if (popupRes?.key === false) {
return
}
const newName = popupRes?.text
if (!newName) {
return
}
const success = await item.rename(newName);
if (success) {
reload();
}
}
const handleItemDragStart = async (
_event: DragEvent,
item: VirtualDirectory | VirtualFile
) => {
const items = uniq([item, ...selectedItem.value])
dataBus.set(
'dragData_vFiles',
items.filter((v) => v.type === 'file')
)
dataBus.set(
'dragData_vDirectory',
items.filter((v) => v.type === 'directory')
)
// selectedItem.clear()
}
const handleDropFiles = async (files: File[]) => {
currentDirectory.value &&
uploadQueue.push(files, currentDirectory.value, {
onFinish() {
reload()
}
})
}
const handleDropVDirectory = async (vDirs: VirtualDirectory[]) => {
console.log(vDirs, currentDirectory.value)
for (let dir of vDirs) {
(async () => {
const success = await dir.mv(currentDirectory.value);
if (success) {
reload();
}
})();
}
}
const handleDropVirtualFiles = (files: VirtualFile[]) => {
for (let file of files) {
(async () => {
const success = await file.moveTo(currentDirectory.value);
if (success) {
reload();
}
})();
}
}
const handlePickFilesUpload = async () => {
const files = await filePicker({
multiple: true
})
const _currDir = currentDirectory.value
uploadQueue.push(files, _currDir, {
onFinish() {
_currDir.update()
}
})
}
const handleAddNewItem = async () => {
const popupRes = await popupInput.value?.popup({
title: '新建文件/文件夹',
inputProps: {
placeholder: ''
},
options: [
{
label: '取消',
key: 'cancel',
type: 'error'
},
{
label: '文件',
key: 'file',
type: 'success'
},
{
label: '文件夹',
key: 'dir',
type: 'success'
}
]
})
// todo: 上传前如重名文件夹等等的错误提前检查
if (popupRes?.key === 'cancel' || popupRes?.text === '') {
return
}
const _currDir = currentDirectory.value
const name = popupRes?.text
if (!name) {
return
}
// @ts-ignore
const opType: 'file' | 'dir' = popupRes?.key
if (opType === 'dir') {
const res = await miaoFetchApi.mkdir(currentDirectory.value, name)
if (res.message === 'success') {
_currDir.update()
}
} else {
const { response } = miaoFetchApi.mkFile(currentDirectory.value, name)
const res = await response
if (res.message === 'success') {
_currDir.update()
}
}
}
const handleItemSelect = (item: VirtualDirectory | VirtualFile) => {
if (!selectedItem.value.includes(item)) {
selectedItem.push(item)
} else {
selectedItem.remove(item)
}
}
const handleSearch = async () => {
const popupRes = await popupInput.value?.popup({
title: '搜索当前文件夹',
inputValue: searchText.value,
inputProps: {
placeholder: ''
},
options: [
{
label: '取消',
key: 'cancel',
type: 'default'
},
{
label: '重置',
key: 'reset',
type: 'default'
},
{
label: '搜索',
key: 'search',
type: 'success'
}
]
})
if (!popupRes) {
return
}
if (popupRes.key === 'search') {
searchText.value = popupRes.text
} else if (popupRes.key === 'reset') {
handleSearchReset()
}
}
const handleSearchReset = () => {
searchText.value = ''
}
onMounted(async () => {
await setCurrentDirectory(currentDirectory.value)
})
</script>
<style scoped lang="scss">
$top-height: 30px;
$bottom-bar-height: 60px;
.miao-directory-item-container {
position: relative;
height: 100%;
width: inherit;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 170px;
}
</style>
+25
View File
@@ -0,0 +1,25 @@
import { registerComponentOption } from '@/class/PluginCenter'
import LogoChrome from '@vicons/ionicons5/es/LogoChrome'
const pluginConfig: registerComponentOption = {
key: 'miaoIframe',
name: '框架',
icon: LogoChrome,
getComponent: async () =>
(await import('@/plugins/miaoIframe/miaoIframe.vue')).default,
filter: (vDirs, vFiles) => {
if (vDirs.length !== 0) {
return false
}
if (vFiles.length !== 1) {
return false
}
return vFiles[0].name.endsWith('html')
},
priority: 2
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
+53
View File
@@ -0,0 +1,53 @@
<template>
<div class="miao-markdown-container-miaomiaomiao">
<iframe
ref="iframeRef"
:src="iframeSrc"
frameborder="0"
class="iframe-container-iframe"></iframe>
</div>
</template>
<script setup lang="ts">
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import cloneDeep from 'lodash/cloneDeep'
import { inject, onMounted, ref } from 'vue'
import buildFromTree from '../../utils/buildFromTree'
const root = inject('rootDirectory') as VirtualDirectory
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const iframeSrc = ref<string>()
const iframeRef = ref<HTMLIFrameElement>()
const sendRootTree = () => {
iframeRef.value?.contentWindow &&
iframeRef.value.contentWindow.postMessage(cloneDeep(root.tree), '*')
}
const handleIframeMessage = (e: MessageEvent<any>) => {
e
// const data = e.data;
sendRootTree()
}
onMounted(() => {
iframeSrc.value = currentFiles.value[0].url
if (typeof window.addEventListener != 'undefined') {
window.addEventListener('message', handleIframeMessage, false)
}
console.log(buildFromTree(cloneDeep(root.tree)))
})
</script>
<style scoped lang="scss">
.miao-markdown-container-miaomiaomiao {
width: 100%;
height: 100%;
.iframe-container-iframe {
width: inherit;
height: inherit;
}
}
</style>
+48
View File
@@ -0,0 +1,48 @@
import { registerComponentOption } from '@/class/PluginCenter'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import { Image } from '@vicons/ionicons5'
const pluginConfig: registerComponentOption = {
key: 'miaoImages',
name: '图片浏览',
icon: Image,
getComponent: async () =>
(await import('@/plugins/miaoImages/miaoImages.vue')).default,
filter: (vDirs: VirtualDirectory[], vFiles: VirtualFile[]) => {
if (vDirs.length === 0 && vFiles.length === 0) {
return false
}
if (vDirs.length) {
return true
}
for (let vf of vFiles)
for (let end of [
'xbm',
'tif',
'pjp',
'svgz',
'jpg',
'jpeg',
'ico',
'tiff',
'gif',
'svg',
'jfif',
'webp',
'png',
'.bmp',
'pjpeg',
'avif'
]) {
if (vf.name.endsWith(end)) {
return true
}
}
return false
}
}
export default pluginConfig
import { PluginExportType } from '@/types/type'
export const type = PluginExportType.component
+342
View File
@@ -0,0 +1,342 @@
<template>
<miao-drop-handler @on-virtual-directory="handleDrop" @on-virtual-files="handleDrop">
<miao-message-provider ref="miaoMessageRef">
<div class="miaoImage" ref="rootRef">
<n-scrollbar ref="NScrollbarRef" style="max-height: 100%">
<div class="miaoImage-container" v-for="imageGroup in imageGroupList">
<div class="miaoImage-container-day">
<!-- <div class="miaoImage-container-divide miaoImage-container-divide-before">
<div class="miaoImage-container-divide-line"></div>
</div> -->
<span class="miaoImage-container-day-text">
{{ imageGroup.day }}
</span>
<!-- <div class="miaoImage-container-divide">
<div class="miaoImage-container-divide-line"></div>
</div> -->
</div>
<div class="miaoImage-container-row" v-for="vFile in imageGroup.items" :key="vFile.id">
<MiaoDraggableDiv :bind-virtual-files="[vFile]" class="size-full flex justify-center items-center">
<img :src="vFile.url" class="miaoImage-container-row-image" loading="lazy" :class="activeImage === vFile
? 'miaoImage-container-row-image-active'
: ''
" @click="activeImage = vFile" />
</MiaoDraggableDiv >
</div>
</div>
</n-scrollbar>
<miao-mask :show="activeImage !== undefined" @click="activeImage = undefined" class="miaoImage-mask"
@wheel="handleWheel">
<ChevronBack class="miaoImage-mask-btn miaoImage-mask-btn-back" @click="handleActiveImageBack" />
<img :src="activeImage?.url" class="miaoImage-mask-active" @click="(e) => e.stopPropagation()"
draggable="false" :style="{ transform: `scale(${imgViewScale})` }" />
<ChevronForward class="miaoImage-mask-btn miaoImage-mask-btn-forward"
@click="handleActiveImageForward" />
</miao-mask>
</div>
</miao-message-provider>
</miao-drop-handler>
</template>
<script setup lang="ts">
import miaoMask from '@/components/miaoMask.vue'
import miaoDropHandler from '@/components/miaoDropHandler.vue'
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
import uniq from 'lodash/uniq'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { NScrollbar } from 'naive-ui'
import { ChevronBack, ChevronForward } from '@vicons/ionicons5'
const imageSuffixList = [
'xbm',
'tif',
'pjp',
'svgz',
'jpg',
'jpeg',
'ico',
'tiff',
'gif',
'svg',
'jfif',
'webp',
'png',
'bmp',
'pjpeg',
'avif'
]
const currentDirectories = defineModel<VirtualDirectory[]>(
'currentDirectories',
{
required: true
}
)
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
required: true
})
const NScrollbarRef = ref()
const rootRef = ref<any>()
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
const imgViewScale = ref(1)
const imageFileList = ref<VirtualFile[]>([])
const activeImage = ref<VirtualFile>()
const imageGroupList = computed(() => {
return groupByDay(imageFileList.value)
})
watch(activeImage, () => {
nextTick(scrollToActiveImage)
imgViewScale.value = 1
})
const groupByDay = (array: VirtualFile[]) => {
const result = []
let currentDay = null
let currentGroup: VirtualFile[] = []
for (const item of array) {
const date = new Date(item.stats.mtimeMs)
const day = `${date.getFullYear()}年 ${String(
date.getMonth() + 1
).padStart(2, '0')}月 ${String(date.getDate()).padStart(2, '0')}日`
if (day !== currentDay) {
if (currentGroup.length > 0) {
result.push({
day: currentDay,
items: currentGroup
})
}
currentDay = day
currentGroup = [item]
} else {
currentGroup.push(item)
}
}
if (currentGroup.length > 0) {
result.push({
day: currentDay,
items: currentGroup
})
}
return result
}
const scrollToActiveImage = () => {
const dom = rootRef.value?.querySelector(
'.miaoImage-container-row-image-active'
)
dom &&
NScrollbarRef.value.scrollTo({
top: dom.offsetTop - 30,
behavior: 'smooth'
})
}
const handleActiveImageBack = (e: any) => {
e.stopPropagation()
if (!activeImage.value) {
return
}
const index = imageFileList.value.indexOf(activeImage.value)
activeImage.value =
imageFileList.value[
(index - 1 + imageFileList.value.length) % imageFileList.value.length
]
}
const handleActiveImageForward = (e: any) => {
e.stopPropagation()
if (!activeImage.value) {
return
}
const index = imageFileList.value.indexOf(activeImage.value)
activeImage.value =
imageFileList.value[
(index + 1 + imageFileList.value.length) % imageFileList.value.length
]
}
const handleDrop = async (vItems: (VirtualFile | VirtualDirectory)[]) => {
const length_before = imageFileList.value.length
if (vItems[0].type === 'directory') {
// @ts-ignore
currentDirectories.value = [...currentDirectories.value, ...vItems]
for (const i of vItems) {
// @ts-ignore
await updateImageSrcList(i)
}
} else {
// @ts-ignore
currentFiles.value = [...currentFiles, ...vItems]
// @ts-ignore
await updateImageSrcList(vItems)
}
miaoMessageRef.value?.alertTip(
length_before === imageFileList.value.length
? '未发现任何可添加图片'
: `导入${imageFileList.value.length - length_before}张图片`,
{
type:
length_before === imageFileList.value.length ? 'info' : 'success',
timeout: 5000
}
)
}
const updateImageSrcList = async (source: VirtualFile[] | VirtualDirectory) => {
let newFileList: VirtualFile[]
if ('type' in source) {
await source.update()
newFileList = [
...(source.files?.filter((v) =>
imageSuffixList.includes(v.name.split('.').pop() ?? '')
) ?? [])
]
} else {
newFileList = [
...(source.filter((v) =>
imageSuffixList.includes(v.name.split('.').pop() ?? '')
) ?? [])
]
}
imageFileList.value = uniq([...imageFileList.value, ...newFileList]).sort(
(a, b) => -a.stats.mtimeMs + b.stats.mtimeMs
)
}
const handleWheel = (e: WheelEvent) => {
if (e.deltaY < 0) {
imgViewScale.value *= 1.42857
} else {
imgViewScale.value /= 1.42857
}
}
onMounted(async () => {
const length_before = imageFileList.value.length
for (const i of [currentFiles.value, ...currentDirectories.value]) {
await updateImageSrcList(i)
}
miaoMessageRef.value?.alertTip(
length_before === imageFileList.value.length
? '未发现任何新增图片'
: `导入${imageFileList.value.length - length_before}张图片`,
{
type:
length_before === imageFileList.value.length ? 'info' : 'success',
timeout: 5000
}
)
})
</script>
<style scoped lang="scss">
.miaoImage {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-wrap: wrap;
gap: 10px;
.miaoImage-container {
width: 100%;
display: flex;
flex-wrap: wrap;
margin-bottom: 20px;
margin-top: 20px;
.miaoImage-container-day {
width: 100%;
display: flex;
align-items: center;
position: relative;
margin: 0 20px;
.miaoImage-container-divide {
flex: 1;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.miaoImage-container-divide-line {
height: 3px;
width: 100%;
border-radius: 5px;
background-color: rgb(244 244 244);
}
.miaoImage-container-divide-before {
flex: 0.1;
}
.miaoImage-container-day-text {
margin: 0 0 0 10px;
font-size: 15px;
user-select: none;
}
}
.miaoImage-container-row {
display: flex;
justify-content: center;
align-items: center;
width: 25%;
aspect-ratio: 1;
.miaoImage-container-row-image {
width: 80%;
aspect-ratio: 1;
object-fit: cover;
transition: width 0.25s;
box-shadow: 0 0 10px -5px;
cursor: pointer;
user-select: none;
&:hover {
width: 90%;
}
&-active {
width: 90%;
}
}
}
}
.miaoImage-mask {
background-color: rgba(0, 0, 0, 0.444);
overflow: hidden;
}
.miaoImage-mask-active {
max-width: 80%;
max-height: 80%;
user-select: none;
}
.miaoImage-mask-btn {
color: #ffffff99;
width: 7%;
position: absolute;
cursor: pointer;
&-back {
left: 1%;
}
&-forward {
right: 1%;
}
}
}
</style>
+211
View File
@@ -0,0 +1,211 @@
<template>
<miao-alert-tip-provider ref="miaoMessageRef">
<div class="clipboard-container">
<n-card class="clipboard-card" title="剪贴板" size="medium">
<div class="key-section">
<n-input v-model:value="key" type="text" placeholder="请输入Key值" class="key-input" />
<div class="button-group">
<n-button @click="getData" type="primary" size="small">查询</n-button>
<n-button @click="setData" type="info" size="small">设置</n-button>
<n-button @click="deleteData" type="error" size="small">删除</n-button>
</div>
</div>
<div class="data-section">
<n-input v-model:value="content" type="textarea" placeholder="数据内容"
:autosize="{ minRows: 5, maxRows: 15 }" class="data-input" />
</div>
<div class="flex justify-end items-center gap-3">
<n-button @click="clearInput" type="error" size="small">清空输入</n-button>
<div class="flex justify-center items-center">
<n-checkbox v-model:checked="useDisk">保存到磁盘</n-checkbox>
</div>
</div>
<div class="status-section" v-if="statusMessage">
<n-alert :type="statusType" class="status-alert">
{{ statusMessage }}
</n-alert>
</div>
</n-card>
</div>
</miao-alert-tip-provider>
</template>
<script setup lang="ts">
import { ref, computed, inject } from 'vue'
import {
NCard,
NInput,
NButton,
NCheckbox,
NAlert
} from 'naive-ui'
import miaoAlertTipProvider from '@/components/miaoAlertTipProvider.vue'
import { alertTipType } from '@/types/type'
const key = ref('')
const content = ref('')
const useDisk = ref(false)
const statusMessage = ref('')
const statusType = ref<'success' | 'error' | 'info' | 'warning'>('info')
const miaoMessageRef = ref<InstanceType<typeof miaoAlertTipProvider>>()
const globalAlertTip = inject<alertTipType>('globalAlertTip')
// 计算磁盘参数
const diskParam = computed(() => useDisk.value ? '1' : '0')
// 显示状态消息
const showStatus = (msg: string, type: 'success' | 'error' | 'info' | 'warning' = 'info') => {
statusMessage.value = msg
statusType.value = type
setTimeout(() => {
statusMessage.value = ''
}, 3000)
}
// 显示消息提示
const showMessage = (content: string, type: 'success' | 'error' | 'info' = 'info') => {
if (miaoMessageRef.value?.alertTip) {
miaoMessageRef.value.alertTip(content, { type, timeout: 3000 })
} else if (globalAlertTip) {
globalAlertTip(content, { type, timeout: 3000 })
}
}
// 获取数据
const getData = async () => {
if (!key.value.trim()) {
showMessage('请输入Key值', 'error')
return
}
try {
const response = await fetch(`/map/get/${key.value}?disk=${diskParam.value}`)
if (!response.ok) {
if (response.status === 404) {
showStatus('未找到对应的数据', 'warning')
showMessage('未找到对应的数据', 'error')
} else {
showStatus(`获取失败:${response.status}`, 'error')
showMessage(`获取失败:${response.status}`, 'error')
}
return
}
const data = await response.text()
content.value = data
showStatus('获取成功', 'success')
showMessage('获取成功', 'success')
} catch (error) {
showStatus(`获取失败:${error}`, 'error')
showMessage(`获取失败:${error}`, 'error')
}
}
// 设置数据
const setData = async () => {
if (!key.value.trim()) {
showMessage('请输入Key值', 'error')
return
}
try {
const response = await fetch(`/map/set/${key.value}?disk=${diskParam.value}`, {
method: 'POST',
body: content.value
})
if (!response.ok) {
showStatus(`设置失败:${response.status}`, 'error')
showMessage(`设置失败:${response.status}`, 'error')
return
}
showStatus('设置成功', 'success')
showMessage('设置成功', 'success')
} catch (error) {
showStatus(`设置失败:${error}`, 'error')
showMessage(`设置失败:${error}`, 'error')
}
}
// 删除数据
const deleteData = async () => {
if (!key.value.trim()) {
showMessage('请输入Key值', 'error')
return
}
try {
const response = await fetch(`/map/delete/${key.value}?disk=${diskParam.value}`, {
method: 'POST'
})
if (!response.ok) {
showStatus(`删除失败:${response.status}`, 'error')
showMessage(`删除失败:${response.status}`, 'error')
return
}
content.value = ''
showStatus('删除成功', 'success')
showMessage('删除成功', 'success')
} catch (error) {
showStatus(`删除失败:${error}`, 'error')
showMessage(`删除失败:${error}`, 'error')
}
}
// 清空输入
const clearInput = () => {
key.value = ''
content.value = ''
}
</script>
<style scoped lang="scss">
.clipboard-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
}
.clipboard-card {
width: 100%;
max-width: 600px;
}
.key-section {
display: flex;
margin-bottom: 16px;
gap: 10px;
}
.key-input {
flex: 1;
}
.button-group {
display: flex;
gap: 8px;
}
.data-section {
margin-bottom: 16px;
}
// .disk-option {
// // margin-bottom: 16px;
// }
.status-section {
margin-top: 16px;
}
.status-alert {
width: 100%;
}
</style>
+18
View File
@@ -0,0 +1,18 @@
import { registerComponentOption } from '@/class/PluginCenter'
import { PluginExportType } from '@/types/type'
import { CopyOutline } from '@vicons/ionicons5'
const pluginConfig: registerComponentOption = {
key: 'miaoKVMap',
name: '键值存储',
icon: CopyOutline,
getComponent: async () => (await import('./KVMapPlugin.vue')).default,
filter: (_, __) => _.length === 0 && __.length === 0,
disable: false,
single: true,
priority: 0
}
export default pluginConfig
export const type = PluginExportType.component
+25
View File
@@ -0,0 +1,25 @@
import { Plugin, PluginGroup } from '@/class/PluginCenter'
import { PluginExportType } from '@/types/type'
import { shallowRef } from 'vue'
import { CopyOutline } from '@vicons/ionicons5'
const pluginConfig: Plugin = {
name: '键值存储',
icon: shallowRef(CopyOutline),
filter: (_, __, views) => {
return views.find('miaoKVMap').length === 0
},
disable: false,
group: [PluginGroup.mainMenu],
func(_, __, hooks) {
const { getPluginCenter } = hooks
const PluginCenter = getPluginCenter()
PluginCenter.usePlugin('miaoKVMap', [], [])
},
priority: 0
}
export default pluginConfig
export const type = PluginExportType.default
// export const key = 'miaoClipboard_mainMenu'
Loaded 100 of 154 files, more files were not shown because too many files have changed in this diff. Show more