Compare commits
5
Commits
b8bea8e3bd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1373de573 | ||
|
|
79221d37d6 | ||
|
|
9ee472e5de | ||
|
|
f341159f1a | ||
|
|
97276224ee |
No files matched your search
@@ -53,15 +53,15 @@ jobs:
|
|||||||
|
|
||||||
- name: 显示构建产物
|
- name: 显示构建产物
|
||||||
run: |
|
run: |
|
||||||
ls -la
|
ls -la output || true
|
||||||
find . -name "miao-directory*" -type f
|
find output -type f -name "miao-directory*" || true
|
||||||
|
|
||||||
- name: 上传构建产物
|
- name: 上传构建产物
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: miao-directory-all
|
name: miao-directory-all
|
||||||
path: |
|
path: |
|
||||||
**/miao-directory*
|
output/**
|
||||||
|
|
||||||
- name: 生成版本号
|
- name: 生成版本号
|
||||||
id: version
|
id: version
|
||||||
@@ -71,13 +71,13 @@ jobs:
|
|||||||
|
|
||||||
- name: 创建自动发布
|
- name: 创建自动发布
|
||||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
|
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
name: Auto-Build-${{ steps.version.outputs.date }}-${{ steps.version.outputs.sha_short }}
|
name: Auto-Build-${{ steps.version.outputs.date }}-${{ steps.version.outputs.sha_short }}
|
||||||
tag_name: auto-${{ steps.version.outputs.date }}-${{ steps.version.outputs.sha_short }}
|
tag_name: auto-${{ steps.version.outputs.date }}-${{ steps.version.outputs.sha_short }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
files: |
|
files: |
|
||||||
**/miao-directory*
|
output/**
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -9,6 +9,7 @@ lerna-debug.log*
|
|||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
output/
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
|||||||
+70
-54
@@ -1,4 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
OUT_DIR="$SCRIPT_DIR/output"
|
||||||
|
|
||||||
# 解析命令行参数
|
# 解析命令行参数
|
||||||
FULL_BUILD=false
|
FULL_BUILD=false
|
||||||
@@ -25,9 +29,16 @@ check_upx() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
require_cmd() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "$1 未找到,请安装后再试"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# 清理已有构建产物
|
# 清理已有构建产物
|
||||||
echo "清理已有构建产物..."
|
echo "清理已有构建产物..."
|
||||||
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
# 基础前缀
|
# 基础前缀
|
||||||
PREFIX="miao-directory"
|
PREFIX="miao-directory"
|
||||||
@@ -44,7 +55,7 @@ BASE_ARCHOS=(
|
|||||||
|
|
||||||
# 清理基础构建产物
|
# 清理基础构建产物
|
||||||
for suffix in "${BASE_ARCHOS[@]}"; do
|
for suffix in "${BASE_ARCHOS[@]}"; do
|
||||||
[ -f "./$PREFIX-$suffix" ] && rm "./$PREFIX-$suffix"
|
[ -f "$OUT_DIR/$PREFIX-$suffix" ] && rm "$OUT_DIR/$PREFIX-$suffix"
|
||||||
done
|
done
|
||||||
|
|
||||||
# 如果是完整构建,清理额外架构产物
|
# 如果是完整构建,清理额外架构产物
|
||||||
@@ -64,7 +75,7 @@ if [ "$FULL_BUILD" = true ]; then
|
|||||||
|
|
||||||
# 清理特殊版本产物
|
# 清理特殊版本产物
|
||||||
for suffix in "${SPECIAL_ARCHOS[@]}"; do
|
for suffix in "${SPECIAL_ARCHOS[@]}"; do
|
||||||
[ -f "./$PREFIX-$suffix" ] && rm "./$PREFIX-$suffix"
|
[ -f "$OUT_DIR/$PREFIX-$suffix" ] && rm "$OUT_DIR/$PREFIX-$suffix"
|
||||||
done
|
done
|
||||||
|
|
||||||
# 处理带x-前缀的不同系统和架构
|
# 处理带x-前缀的不同系统和架构
|
||||||
@@ -74,7 +85,7 @@ if [ "$FULL_BUILD" = true ]; then
|
|||||||
# OpenBSD系列
|
# OpenBSD系列
|
||||||
"openbsd:amd64,arm64,386"
|
"openbsd:amd64,arm64,386"
|
||||||
# Windows额外架构
|
# Windows额外架构
|
||||||
"win.exe:386"
|
"windows:arm64"
|
||||||
# Linux额外架构
|
# Linux额外架构
|
||||||
"linux:386,mips,mips64,mips64le,ppc64,ppc64le,riscv64,s390x"
|
"linux:386,mips,mips64,mips64le,ppc64,ppc64le,riscv64,s390x"
|
||||||
)
|
)
|
||||||
@@ -88,20 +99,24 @@ if [ "$FULL_BUILD" = true ]; then
|
|||||||
IFS="," read -ra arch_array <<< "$archs"
|
IFS="," read -ra arch_array <<< "$archs"
|
||||||
for arch in "${arch_array[@]}"; do
|
for arch in "${arch_array[@]}"; do
|
||||||
file="$PREFIX-x-$arch-$os"
|
file="$PREFIX-x-$arch-$os"
|
||||||
[ -f "./$file" ] && rm "./$file"
|
[ -f "$OUT_DIR/$file" ] && rm "$OUT_DIR/$file"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "构建前端..."
|
echo "构建前端..."
|
||||||
|
require_cmd pnpm
|
||||||
|
require_cmd go
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
echo "复制前端文件到服务端..."
|
echo "复制前端文件到服务端..."
|
||||||
rm -rf ./src-server/web/*
|
rm -rf ./src-server/web/*
|
||||||
mkdir -p ./src-server/web/
|
mkdir -p ./src-server/web/
|
||||||
cp -r ./dist/* ./src-server/web/
|
if compgen -G "./dist/*" > /dev/null; then
|
||||||
|
cp -r ./dist/* ./src-server/web/
|
||||||
|
fi
|
||||||
|
|
||||||
cd ./src-server
|
cd "$SCRIPT_DIR/src-server"
|
||||||
|
|
||||||
# ===== 构建阶段 =====
|
# ===== 构建阶段 =====
|
||||||
echo "===== 开始构建阶段 ====="
|
echo "===== 开始构建阶段 ====="
|
||||||
@@ -110,21 +125,17 @@ echo "===== 开始构建阶段 ====="
|
|||||||
echo "构建基础版本..."
|
echo "构建基础版本..."
|
||||||
|
|
||||||
# Windows AMD64 无GUI
|
# Windows AMD64 无GUI
|
||||||
echo "构建 Windows AMD64 无GUI 版本"
|
echo "构建 Win AMD64 无GUI 版本"
|
||||||
go env -w GOOS=windows GOARCH=amd64
|
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o "$OUT_DIR/miao-directory-amd64-win.exe"
|
||||||
go build -ldflags="-s -w" -o ../miao-directory-amd64-win.exe
|
|
||||||
|
|
||||||
# Linux AMD64 无GUI
|
# Linux AMD64 无GUI
|
||||||
echo "构建 Linux AMD64 无GUI 版本"
|
echo "构建 Linux AMD64 无GUI 版本"
|
||||||
go env -w GOOS=linux GOARCH=amd64
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o "$OUT_DIR/miao-directory-amd64-linux"
|
||||||
export CGO_ENABLED=0
|
chmod +x "$OUT_DIR/miao-directory-amd64-linux"
|
||||||
go build -ldflags="-s -w" -o ../miao-directory-amd64-linux
|
|
||||||
export CGO_ENABLED=1
|
|
||||||
|
|
||||||
# Windows AMD64 GUI
|
# Windows AMD64 GUI
|
||||||
echo "构建 Windows AMD64 GUI 版本"
|
echo "构建 Win AMD64 GUI 版本"
|
||||||
go env -w GOOS=windows GOARCH=amd64
|
GOOS=windows GOARCH=amd64 go build -tags webview -ldflags="-s -w -H=windowsgui" -o "$OUT_DIR/miao-directory-amd64-win.gui.exe"
|
||||||
go build -tags webview -ldflags="-s -w -H=windowsgui" -o ../miao-directory-amd64-win.gui.exe
|
|
||||||
|
|
||||||
# 完整构建 - 额外的系统和架构
|
# 完整构建 - 额外的系统和架构
|
||||||
if [ "$FULL_BUILD" = true ]; then
|
if [ "$FULL_BUILD" = true ]; then
|
||||||
@@ -136,44 +147,56 @@ if [ "$FULL_BUILD" = true ]; then
|
|||||||
mv resource.syso resource.syso.bak
|
mv resource.syso resource.syso.bak
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 无需GUI和syso的构建批次
|
: # CGO 在每次构建调用中禁用,无需全局 export
|
||||||
export CGO_ENABLED=0
|
|
||||||
|
|
||||||
# 定义操作系统和架构映射
|
# 定义操作系统和架构映射
|
||||||
declare -A OS_ARCHS
|
# 在工具函数处增加大小写格式化,避免使用 Bash 4 的 ${var^}/${var^^}
|
||||||
OS_ARCHS["linux"]="arm64 386 mips mips64 mips64le ppc64 ppc64le riscv64 s390x"
|
uc_first() {
|
||||||
OS_ARCHS["darwin"]="amd64 arm64"
|
local s="$1"
|
||||||
OS_ARCHS["windows"]="arm64 386"
|
local first="${s:0:1}"
|
||||||
OS_ARCHS["freebsd"]="amd64 arm64 386"
|
local rest="${s:1}"
|
||||||
OS_ARCHS["openbsd"]="amd64 arm64 386"
|
printf "%s%s" "$(printf "%s" "$first" | tr '[:lower:]' '[:upper:]')" "$rest"
|
||||||
# OS_ARCHS["netbsd"]="amd64 arm64 386"
|
}
|
||||||
|
|
||||||
# 遍历操作系统和架构进行构建
|
to_upper() {
|
||||||
for os in "${!OS_ARCHS[@]}"; do
|
printf "%s" "$1" | tr '[:lower:]' '[:upper:]'
|
||||||
echo "构建 ${os^} 系列版本..."
|
}
|
||||||
go env -w GOOS=$os
|
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
|
||||||
archs=${OS_ARCHS[$os]}
|
|
||||||
|
|
||||||
for arch in $archs; do
|
|
||||||
# 设置输出文件名后缀
|
# 设置输出文件名后缀
|
||||||
suffix=""
|
suffix=""
|
||||||
if [ "$os" = "windows" ]; then
|
if [ "$os" = "win" ]; then
|
||||||
suffix=".exe"
|
suffix=".exe"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "构建 ${os^} ${arch^^} 版本"
|
echo "构建 $(uc_first "$os") $(to_upper "$arch") 版本"
|
||||||
go env -w GOARCH=$arch
|
|
||||||
|
|
||||||
# 根据条件设置不同的输出文件名格式
|
# 根据条件设置不同的输出文件名格式
|
||||||
if [[ "$os" == "darwin" || ("$os" == "linux" && "$arch" == "arm64") || ("$os" == "windows" && "$arch" == "arm64") ]]; then
|
if [[ "$os" == "darwin" || ("$os" == "linux" && "$arch" == "arm64") || ("$os" == "win" && "$arch" == "arm64") ]]; then
|
||||||
output_file="../miao-directory-${arch}-${os}${suffix}"
|
output_file="$OUT_DIR/miao-directory-${arch}-${os}${suffix}"
|
||||||
else
|
else
|
||||||
output_file="../miao-directory-x-${arch}-${os}${suffix}"
|
output_file="$OUT_DIR/miao-directory-x-${arch}-${os}${suffix}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
go build -ldflags="-s -w" -o "$output_file"
|
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
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -183,7 +206,7 @@ if [ "$FULL_BUILD" = true ]; then
|
|||||||
mv resource.syso.bak resource.syso
|
mv resource.syso.bak resource.syso
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export CGO_ENABLED=1
|
: # 无需恢复 CGO,全程使用内联变量
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ===== 压缩阶段 =====
|
# ===== 压缩阶段 =====
|
||||||
@@ -206,16 +229,16 @@ if check_upx; then
|
|||||||
echo "压缩基础版本..."
|
echo "压缩基础版本..."
|
||||||
|
|
||||||
# 压缩Windows无GUI版本
|
# 压缩Windows无GUI版本
|
||||||
echo "正在压缩Windows AMD64无GUI版本"
|
echo "正在压缩Win AMD64无GUI版本"
|
||||||
upx $COMPRESSION_LEVEL -o ../miao-directory-amd64-win.upx.exe ../miao-directory-amd64-win.exe
|
upx $COMPRESSION_LEVEL -o "$OUT_DIR/miao-directory-amd64-win.upx.exe" "$OUT_DIR/miao-directory-amd64-win.exe"
|
||||||
|
|
||||||
# 压缩Linux AMD64版本
|
# 压缩Linux AMD64版本
|
||||||
echo "正在压缩Linux AMD64版本"
|
echo "正在压缩Linux AMD64版本"
|
||||||
upx $COMPRESSION_LEVEL -o ../miao-directory-amd64-linux.upx ../miao-directory-amd64-linux
|
upx $COMPRESSION_LEVEL -o "$OUT_DIR/miao-directory-amd64-linux.upx" "$OUT_DIR/miao-directory-amd64-linux"
|
||||||
|
|
||||||
# 压缩Windows GUI版本
|
# 压缩Windows GUI版本
|
||||||
echo "正在压缩Windows AMD64 GUI版本"
|
echo "正在压缩Win AMD64 GUI版本"
|
||||||
upx $COMPRESSION_LEVEL -o ../miao-directory-amd64-win.gui.upx.exe ../miao-directory-amd64-win.gui.exe
|
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
|
if [ "$FULL_BUILD" = true ]; then
|
||||||
@@ -223,15 +246,8 @@ if check_upx; then
|
|||||||
|
|
||||||
# 压缩Linux ARM64版本
|
# 压缩Linux ARM64版本
|
||||||
echo "正在压缩Linux ARM64版本"
|
echo "正在压缩Linux ARM64版本"
|
||||||
upx $COMPRESSION_LEVEL -o ../miao-directory-arm64-linux.upx ../miao-directory-arm64-linux
|
upx $COMPRESSION_LEVEL -o "$OUT_DIR/miao-directory-arm64-linux.upx" "$OUT_DIR/miao-directory-arm64-linux"
|
||||||
|
|
||||||
# # 压缩macOS AMD64版本
|
|
||||||
# echo "正在压缩macOS AMD64版本"
|
|
||||||
# upx $COMPRESSION_LEVEL -o ../miao-directory-amd64-darwin.upx ../miao-directory-amd64-darwin
|
|
||||||
|
|
||||||
# # 压缩macOS ARM64版本
|
|
||||||
# echo "正在压缩macOS ARM64版本"
|
|
||||||
# upx $COMPRESSION_LEVEL -o ../miao-directory-arm64-darwin.upx ../miao-directory-arm64-darwin
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "所有压缩完成"
|
echo "所有压缩完成"
|
||||||
|
|||||||
Vendored
-3
@@ -15,19 +15,16 @@ declare module 'vue' {
|
|||||||
MiaoLazyDiv: typeof import('./src/components/miaoLazyDiv.vue')['default']
|
MiaoLazyDiv: typeof import('./src/components/miaoLazyDiv.vue')['default']
|
||||||
MiaoMask: typeof import('./src/components/miaoMask.vue')['default']
|
MiaoMask: typeof import('./src/components/miaoMask.vue')['default']
|
||||||
MiaoPopupInput: typeof import('./src/components/miaoPopupInput.vue')['default']
|
MiaoPopupInput: typeof import('./src/components/miaoPopupInput.vue')['default']
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
|
||||||
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
|
||||||
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NGrid: typeof import('naive-ui')['NGrid']
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
NGridItem: typeof import('naive-ui')['NGridItem']
|
NGridItem: typeof import('naive-ui')['NGridItem']
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
|
||||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
"build:auto": "pnpm run build && go env -w GOOS=windows GOARCH=amd64 && rm -rf ./src-server/web/* && cp -r ./dist/* ./src-server/web/ && cd ./src-server && go env -w GOOS=windows && go build -o ../miao-directory.exe",
|
"build:auto": "pnpm run build && go env -w GOOS=windows GOARCH=amd64 && rm -rf ./src-server/web/* && cp -r ./dist/* ./src-server/web/ && cd ./src-server && go env -w GOOS=windows && go build -o ../miao-directory.exe",
|
||||||
"build:fast": "bash build.auto.release.sh -fast",
|
"build:fast": "bash build.auto.release.sh -fast",
|
||||||
|
"build:normal": "bash build.auto.release.sh",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
|
|||||||
File renamed without changes.
@@ -1,13 +1,14 @@
|
|||||||
import { registerComponentOption } from '@/class/PluginCenter'
|
import { registerComponentOption } from '@/class/PluginCenter'
|
||||||
import FileTray from '@vicons/ionicons5/es/FileTray'
|
import FileTray from '@vicons/ionicons5/es/FileTray'
|
||||||
// import MiaoDirectory from '@/plugins/miaoDirectory/miaoDirectory.vue'
|
import MiaoDirectory from '@/plugins/miaoDirectory/miaoDirectory.vue'
|
||||||
|
|
||||||
const pluginConfig: registerComponentOption = {
|
const pluginConfig: registerComponentOption = {
|
||||||
key: 'miaoDirectory',
|
key: 'miaoDirectory',
|
||||||
name: '文件夹',
|
name: '文件夹',
|
||||||
icon: FileTray,
|
icon: FileTray,
|
||||||
getComponent: async () =>
|
// getComponent: async () =>
|
||||||
(await import('@/plugins/miaoDirectory/miaoDirectory.vue')).default,
|
// (await import('@/plugins/miaoDirectory/miaoDirectory.vue')).default,
|
||||||
|
getComponent: async () => MiaoDirectory,
|
||||||
filter: (vDirs, vFiles) => {
|
filter: (vDirs, vFiles) => {
|
||||||
if (vFiles.length) {
|
if (vFiles.length) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<miao-mask :show="show" @click="$emit('close')" class="miaoImage-mask" @wheel="handleWheel">
|
||||||
|
<ChevronBack class="miaoImage-mask-btn miaoImage-mask-btn-back" @click.stop="$emit('back')" />
|
||||||
|
<MiaoDraggableDiv
|
||||||
|
v-if="file"
|
||||||
|
:is-draggable="isDraggable"
|
||||||
|
:bind-virtual-files="[file]"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
|
@pointerdown="handlePressStart"
|
||||||
|
@pointerup="handlePressEnd"
|
||||||
|
@pointercancel="handlePressEnd"
|
||||||
|
@pointerleave="handlePressEnd"
|
||||||
|
@touchstart.passive="handleTouchStart"
|
||||||
|
@touchend="handleTouchEnd"
|
||||||
|
>
|
||||||
|
<img :src="file.url" class="miaoImage-mask-active" @click="(e) => e.stopPropagation()" draggable="false" :style="{ transform: `scale(${imgViewScale})` }" />
|
||||||
|
</MiaoDraggableDiv>
|
||||||
|
<ChevronForward class="miaoImage-mask-btn miaoImage-mask-btn-forward" @click.stop="$emit('forward')" />
|
||||||
|
</miao-mask>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import miaoMask from '@/components/miaoMask.vue'
|
||||||
|
import MiaoDraggableDiv from '@/components/miaoDraggableDiv.vue'
|
||||||
|
import { ChevronBack, ChevronForward } from '@vicons/ionicons5'
|
||||||
|
import type { VirtualFile } from '@/class/VirtualDirectory'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
file?: VirtualFile
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
back: []
|
||||||
|
forward: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const imgViewScale = ref(1)
|
||||||
|
const isDraggable = ref(false)
|
||||||
|
let pressTimer: number | undefined
|
||||||
|
|
||||||
|
const clearPress = () => {
|
||||||
|
if (pressTimer !== undefined) {
|
||||||
|
clearTimeout(pressTimer)
|
||||||
|
pressTimer = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePressStart = () => {
|
||||||
|
clearPress()
|
||||||
|
pressTimer = window.setTimeout(() => {
|
||||||
|
isDraggable.value = true
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePressEnd = () => {
|
||||||
|
clearPress()
|
||||||
|
// 延迟复位,避免影响 drop
|
||||||
|
setTimeout(() => {
|
||||||
|
isDraggable.value = false
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTouchStart = () => handlePressStart()
|
||||||
|
const handleTouchEnd = () => handlePressEnd()
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
isDraggable.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
if (e.deltaY < 0) {
|
||||||
|
imgViewScale.value *= 1.42857
|
||||||
|
} else {
|
||||||
|
imgViewScale.value /= 1.42857
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.file,
|
||||||
|
() => {
|
||||||
|
imgViewScale.value = 1
|
||||||
|
isDraggable.value = false
|
||||||
|
clearPress()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
(v) => {
|
||||||
|
if (!v) {
|
||||||
|
imgViewScale.value = 1
|
||||||
|
isDraggable.value = false
|
||||||
|
clearPress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.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;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
&-back {
|
||||||
|
left: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-forward {
|
||||||
|
right: 1%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -25,28 +25,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
<miao-mask :show="activeImage !== undefined" @click="activeImage = undefined" class="miaoImage-mask"
|
<MiaoImageViewer
|
||||||
@wheel="handleWheel">
|
:show="activeImage !== undefined"
|
||||||
<ChevronBack class="miaoImage-mask-btn miaoImage-mask-btn-back" @click="handleActiveImageBack" />
|
:file="activeImage"
|
||||||
<img :src="activeImage?.url" class="miaoImage-mask-active" @click="(e) => e.stopPropagation()"
|
@close="activeImage = undefined"
|
||||||
draggable="false" :style="{ transform: `scale(${imgViewScale})` }" />
|
@back="handleActiveImageBack"
|
||||||
<ChevronForward class="miaoImage-mask-btn miaoImage-mask-btn-forward"
|
@forward="handleActiveImageForward"
|
||||||
@click="handleActiveImageForward" />
|
/>
|
||||||
</miao-mask>
|
|
||||||
</div>
|
</div>
|
||||||
</miao-message-provider>
|
</miao-message-provider>
|
||||||
</miao-drop-handler>
|
</miao-drop-handler>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import miaoMask from '@/components/miaoMask.vue'
|
|
||||||
import miaoDropHandler from '@/components/miaoDropHandler.vue'
|
import miaoDropHandler from '@/components/miaoDropHandler.vue'
|
||||||
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
|
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
|
||||||
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
|
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
|
||||||
import uniq from 'lodash/uniq'
|
import uniq from 'lodash/uniq'
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { NScrollbar } from 'naive-ui'
|
import { NScrollbar } from 'naive-ui'
|
||||||
import { ChevronBack, ChevronForward } from '@vicons/ionicons5'
|
import MiaoImageViewer from './miaoImageViewer.vue'
|
||||||
|
|
||||||
const imageSuffixList = [
|
const imageSuffixList = [
|
||||||
'xbm',
|
'xbm',
|
||||||
@@ -80,7 +78,6 @@ const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
|
|||||||
const NScrollbarRef = ref()
|
const NScrollbarRef = ref()
|
||||||
const rootRef = ref<any>()
|
const rootRef = ref<any>()
|
||||||
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
|
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
|
||||||
const imgViewScale = ref(1)
|
|
||||||
const imageFileList = ref<VirtualFile[]>([])
|
const imageFileList = ref<VirtualFile[]>([])
|
||||||
const activeImage = ref<VirtualFile>()
|
const activeImage = ref<VirtualFile>()
|
||||||
|
|
||||||
@@ -90,7 +87,6 @@ const imageGroupList = computed(() => {
|
|||||||
|
|
||||||
watch(activeImage, () => {
|
watch(activeImage, () => {
|
||||||
nextTick(scrollToActiveImage)
|
nextTick(scrollToActiveImage)
|
||||||
imgViewScale.value = 1
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const groupByDay = (array: VirtualFile[]) => {
|
const groupByDay = (array: VirtualFile[]) => {
|
||||||
@@ -210,14 +206,6 @@ const updateImageSrcList = async (source: VirtualFile[] | VirtualDirectory) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleWheel = (e: WheelEvent) => {
|
|
||||||
if (e.deltaY < 0) {
|
|
||||||
imgViewScale.value *= 1.42857
|
|
||||||
} else {
|
|
||||||
imgViewScale.value /= 1.42857
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const length_before = imageFileList.value.length
|
const length_before = imageFileList.value.length
|
||||||
for (const i of [currentFiles.value, ...currentDirectories.value]) {
|
for (const i of [currentFiles.value, ...currentDirectories.value]) {
|
||||||
@@ -329,6 +317,7 @@ onMounted(async () => {
|
|||||||
width: 7%;
|
width: 7%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
&-back {
|
&-back {
|
||||||
left: 1%;
|
left: 1%;
|
||||||
|
|||||||
+214
-63
@@ -2,48 +2,30 @@
|
|||||||
<miao-message-provider ref="miaoMessageRef">
|
<miao-message-provider ref="miaoMessageRef">
|
||||||
<div class="miao-container">
|
<div class="miao-container">
|
||||||
<div class="miao-container-topBar">
|
<div class="miao-container-topBar">
|
||||||
<div
|
<div class="topBar-main">
|
||||||
class="miao-container-topBar-btn"
|
<div class="top-btn" @click="() => handleZoom(10)" title="快速缩小">--</div>
|
||||||
@click="() => handleZoom(10)"
|
<div class="top-btn" @click="() => handleZoom()" title="缩小">-</div>
|
||||||
title="快速缩小">
|
<div class="top-btn" @click="() => handleResetMargin()" title="重置">重置</div>
|
||||||
--
|
<div class="top-btn" @click="() => handleShrink()" title="放大">+</div>
|
||||||
</div>
|
<div class="top-btn" @click="() => handleShrink(10)" title="快速放大">++</div>
|
||||||
<div
|
<div class="scale-indicator">{{ Math.round(scale * 100) }}%</div>
|
||||||
class="miao-container-topBar-btn"
|
|
||||||
@click="() => handleZoom()"
|
|
||||||
title="缩小">
|
|
||||||
-
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="miao-container-topBar-btn"
|
|
||||||
@click="() => handleResetMargin()"
|
|
||||||
title="还原">
|
|
||||||
还原缩放
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="miao-container-topBar-btn"
|
|
||||||
@click="() => handleShrink()"
|
|
||||||
title="放大">
|
|
||||||
+
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="miao-container-topBar-btn"
|
|
||||||
@click="() => handleShrink(10)"
|
|
||||||
title="快速放大">
|
|
||||||
++
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NScrollbar>
|
<NScrollbar ref="scrollRef">
|
||||||
<div
|
<div
|
||||||
class="miao-container-pdf"
|
class="miao-container-pdf"
|
||||||
:style="{
|
@pointerdown="onPointerDown"
|
||||||
alignItems: pdfMargin < 0 ? 'baseline' : 'center'
|
@pointermove="onPointerMove"
|
||||||
}">
|
@pointerup="onPointerUp"
|
||||||
|
@pointercancel="onPointerCancel"
|
||||||
|
:class="{ 'is-dragging': isDragging}"
|
||||||
|
:style="{'align-items': scale > 1 ? 'unset' : 'center' }">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref="pdfEl"
|
ref="pdfEl"
|
||||||
class="miao-container-pdf-content"
|
class="miao-container-pdf-content"
|
||||||
:style="{
|
:style="{
|
||||||
width: `calc( 100% - ${pdfMargin}px)`
|
width: `${(scale * 100).toFixed(2)}%`,
|
||||||
}"></div>
|
}"></div>
|
||||||
</div>
|
</div>
|
||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
@@ -53,7 +35,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
|
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
|
||||||
import { onMounted, ref, shallowReactive } from 'vue'
|
import { onMounted, ref, shallowReactive, computed, onBeforeUnmount } from 'vue'
|
||||||
import PdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?worker'
|
import PdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?worker'
|
||||||
import { VirtualFile } from '@/class/VirtualDirectory'
|
import { VirtualFile } from '@/class/VirtualDirectory'
|
||||||
import { NScrollbar } from 'naive-ui'
|
import { NScrollbar } from 'naive-ui'
|
||||||
@@ -64,31 +46,148 @@ const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
|
|||||||
|
|
||||||
const pdfEl = ref<HTMLDivElement>()
|
const pdfEl = ref<HTMLDivElement>()
|
||||||
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
|
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
|
||||||
// 通过margin来实现缩小功能
|
// 新缩放方案:使用等比scale
|
||||||
const pdfMargin = ref<number>(0)
|
const scale = ref<number>(1)
|
||||||
|
const minScale = 0.5
|
||||||
|
const maxScale = 4
|
||||||
|
|
||||||
const pdfPageCount = ref<number>(-1)
|
const scrollRef = ref<InstanceType<typeof NScrollbar> | null>(null)
|
||||||
|
const isDragging = ref(false)
|
||||||
|
let pressTimeoutId: number | null = null
|
||||||
|
let dragging = false
|
||||||
|
let startX = 0
|
||||||
|
let startY = 0
|
||||||
|
let startLeft = 0
|
||||||
|
let startTop = 0
|
||||||
|
const longPressDelay = 100
|
||||||
|
|
||||||
const canvasList = shallowReactive<HTMLCanvasElement[]>([])
|
// 更稳健的滚动容器解析逻辑,避免依赖 $el.querySelector
|
||||||
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
|
const findScrollableParent = (el: HTMLElement | null): HTMLElement | null => {
|
||||||
|
let cur: HTMLElement | null = el?.parentElement ?? null
|
||||||
|
while (cur) {
|
||||||
|
const style = window.getComputedStyle(cur)
|
||||||
|
const overflowY = style.overflowY
|
||||||
|
const overflowX = style.overflowX
|
||||||
|
const canScroll =
|
||||||
|
(overflowY === 'auto' || overflowY === 'scroll' || overflowX === 'auto' || overflowX === 'scroll') &&
|
||||||
|
(cur.scrollHeight > cur.clientHeight || cur.scrollWidth > cur.clientWidth)
|
||||||
|
if (canScroll) return cur
|
||||||
|
cur = cur.parentElement
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const resolveScrollContainer = () => {
|
||||||
|
const fromScrollbar = pdfEl.value?.closest('.n-scrollbar')?.querySelector('.n-scrollbar-container') as HTMLElement | null
|
||||||
|
scrollContainer.value = fromScrollbar ?? findScrollableParent(pdfEl.value ?? null)
|
||||||
|
}
|
||||||
|
const getScrollEl = (): HTMLElement | null => {
|
||||||
|
if (!scrollContainer.value) resolveScrollContainer()
|
||||||
|
return scrollContainer.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerDown = (e: PointerEvent) => {
|
||||||
|
resolveScrollContainer()
|
||||||
|
const el = getScrollEl()
|
||||||
|
if (!el) return
|
||||||
|
startX = e.clientX
|
||||||
|
startY = e.clientY
|
||||||
|
startLeft = el.scrollLeft
|
||||||
|
startTop = el.scrollTop
|
||||||
|
dragging = false
|
||||||
|
;(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId)
|
||||||
|
if (pressTimeoutId) window.clearTimeout(pressTimeoutId)
|
||||||
|
pressTimeoutId = window.setTimeout(() => {
|
||||||
|
dragging = true
|
||||||
|
isDragging.value = true
|
||||||
|
document.body.style.cursor = 'grabbing'
|
||||||
|
}, longPressDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPointerMove = (e: PointerEvent) => {
|
||||||
|
const el = getScrollEl()
|
||||||
|
if (!el || !dragging) return
|
||||||
|
const dx = e.clientX - startX
|
||||||
|
const dy = e.clientY - startY
|
||||||
|
let newLeft = startLeft - dx
|
||||||
|
let newTop = startTop - dy
|
||||||
|
const maxLeft = Math.max(0, el.scrollWidth - el.clientWidth)
|
||||||
|
const maxTop = Math.max(0, el.scrollHeight - el.clientHeight)
|
||||||
|
newLeft = Math.max(0, Math.min(maxLeft, newLeft))
|
||||||
|
newTop = Math.max(0, Math.min(maxTop, newTop))
|
||||||
|
el.scrollLeft = newLeft
|
||||||
|
el.scrollTop = newTop
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const endDrag = (e: PointerEvent) => {
|
||||||
|
if (pressTimeoutId) {
|
||||||
|
window.clearTimeout(pressTimeoutId)
|
||||||
|
pressTimeoutId = null
|
||||||
|
}
|
||||||
|
if (dragging) {
|
||||||
|
isDragging.value = false
|
||||||
|
document.body.style.cursor = ''
|
||||||
|
}
|
||||||
|
dragging = false
|
||||||
|
try {
|
||||||
|
(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId)
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
const onPointerUp = endDrag
|
||||||
|
const onPointerCancel = endDrag
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (pressTimeoutId) window.clearTimeout(pressTimeoutId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 等比缩放并保持视窗中心不抖动
|
||||||
|
const applyZoom = (multiplier: number) => {
|
||||||
|
// const el = getScrollEl()
|
||||||
|
const oldScale = scale.value
|
||||||
|
let newScale = oldScale * multiplier
|
||||||
|
newScale = Math.max(minScale, Math.min(maxScale, newScale))
|
||||||
|
// const ratio = newScale / oldScale
|
||||||
|
// if (el) {
|
||||||
|
// const cx = el.clientWidth / 2
|
||||||
|
// const cy = el.clientHeight / 2
|
||||||
|
// // const targetLeft = (el.scrollLeft + cx) * ratio - cx
|
||||||
|
// // const targetTop = (el.scrollTop + cy) * ratio - cy
|
||||||
|
// scale.value = +newScale.toFixed(4)
|
||||||
|
// // requestAnimationFrame(() => {
|
||||||
|
// // const maxLeft = Math.max(0, el.scrollWidth - el.clientWidth)
|
||||||
|
// // const maxTop = Math.max(0, el.scrollHeight - el.clientHeight)
|
||||||
|
// // el.scrollLeft = Math.max(0, Math.min(maxLeft, targetLeft))
|
||||||
|
// // el.scrollTop = Math.max(0, Math.min(maxTop, targetTop))
|
||||||
|
// // })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
scale.value = +newScale.toFixed(4)
|
||||||
|
}
|
||||||
|
|
||||||
const handleZoom = (index?: number) => {
|
const handleZoom = (index?: number) => {
|
||||||
const _index = index ?? 1
|
const _index = index ?? 1
|
||||||
pdfMargin.value += 10 * _index
|
const factor = 1 + 0.1 * _index
|
||||||
|
applyZoom(1 / factor)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleShrink = (index?: number) => {
|
const handleShrink = (index?: number) => {
|
||||||
const _index = index ?? 1
|
const _index = index ?? 1
|
||||||
// if (pdfMargin.value - 10 * _index <= 0) {
|
const factor = 1 + 0.1 * _index
|
||||||
// return pdfMargin.value = 0
|
applyZoom(factor)
|
||||||
// }
|
|
||||||
pdfMargin.value -= 10 * _index
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResetMargin = () => {
|
const handleResetMargin = () => {
|
||||||
pdfMargin.value = 0
|
const el = getScrollEl()
|
||||||
|
scale.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pdfPageCount = ref<number>(-1)
|
||||||
|
const canvasList = shallowReactive<HTMLCanvasElement[]>([])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// 初始解析一次滚动容器(等待一帧确保DOM就绪)
|
||||||
|
requestAnimationFrame(() => resolveScrollContainer())
|
||||||
const setLoadingMessage = miaoMessageRef.value?.alertTip('加载PDF插件中', {
|
const setLoadingMessage = miaoMessageRef.value?.alertTip('加载PDF插件中', {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
timeout: -1
|
timeout: -1
|
||||||
@@ -134,6 +233,15 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.miao-container {
|
.miao-container {
|
||||||
|
--bar-height: 44px;
|
||||||
|
--bar-bg: linear-gradient(90deg, #eef1f5 0%, #e3e7ec 100%);
|
||||||
|
--btn-bg: #ffffff;
|
||||||
|
--btn-hover: #f2f4f7;
|
||||||
|
--btn-active: #e6e9ef;
|
||||||
|
--btn-border: #d7dbe2;
|
||||||
|
--text: #1f2328;
|
||||||
|
--subtext: #6b7280;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgb(201, 201, 201);
|
background-color: rgb(201, 201, 201);
|
||||||
@@ -144,31 +252,57 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.miao-container-topBar {
|
.miao-container-topBar {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0 30px;
|
padding: 0 12px;
|
||||||
height: 30px;
|
height: var(--bar-height);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: rgb(170, 176, 182);
|
background: var(--bar-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||||
|
backdrop-filter: saturate(180%) blur(8px);
|
||||||
|
|
||||||
&-btn {
|
.topBar-main {
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
height: 80%;
|
|
||||||
padding: 0 5px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 8px;
|
||||||
font-size: larger;
|
position: relative;
|
||||||
letter-spacing: 1px;
|
user-select: none;
|
||||||
color: black;
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: #eaeaea42;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
height: 30px;
|
||||||
|
min-width: 34px;
|
||||||
|
padding: 0 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: var(--text);
|
||||||
|
background-color: var(--btn-bg);
|
||||||
|
border: 1px solid var(--btn-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 0 rgba(0,0,0,0.02);
|
||||||
|
transition: background-color .15s ease, transform .08s ease, box-shadow .15s ease;
|
||||||
|
|
||||||
|
&:hover { background-color: var(--btn-hover); }
|
||||||
|
&:active { background-color: var(--btn-active); transform: scale(0.98); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: -60px;
|
||||||
|
min-width: 54px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--subtext);
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint { color: var(--subtext); font-size: 12px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.miao-container-pdf {
|
.miao-container-pdf {
|
||||||
@@ -176,9 +310,26 @@ onMounted(async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: center; // 居中以便缩小时居中显示
|
||||||
|
touch-action: none; // 以便自定义拖动
|
||||||
|
&.is-dragging { cursor: grabbing; }
|
||||||
|
|
||||||
.miao-container-pdf-content {
|
.miao-container-pdf-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
transition: width .12s ease, padding-top .12s ease;
|
||||||
|
will-change: width, padding-top;
|
||||||
|
|
||||||
|
// 让Canvas随容器宽度等比缩放,保证显示完整
|
||||||
|
canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 12px auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { registerComponentOption } from '@/class/PluginCenter'
|
||||||
|
import VirtualDirectory, { VirtualFile } from '@/class/VirtualDirectory'
|
||||||
|
import { Analytics } from '@vicons/ionicons5'
|
||||||
|
|
||||||
|
const pluginConfig: registerComponentOption = {
|
||||||
|
key: '45632456',
|
||||||
|
name: 'ONNX DEMO BIRD',
|
||||||
|
icon: Analytics,
|
||||||
|
getComponent: async () =>
|
||||||
|
(await import('./onnx_demo.vue')).default,
|
||||||
|
filter: (_vDirs: VirtualDirectory[], vFiles: VirtualFile[]) => {
|
||||||
|
if (vFiles.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否存在.onnx文件
|
||||||
|
let hasOnnxFile = false
|
||||||
|
// 检查是否存在图片文件
|
||||||
|
let hasImageFile = false
|
||||||
|
|
||||||
|
const imageSuffixList = [
|
||||||
|
'xbm', 'tif', 'pjp', 'svgz', 'jpg', 'jpeg', 'ico', 'tiff', 'gif',
|
||||||
|
'svg', 'jfif', 'webp', 'png', 'bmp', 'pjpeg', 'avif'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (let vf of vFiles) {
|
||||||
|
if (vf.name.endsWith('.onnx')) {
|
||||||
|
hasOnnxFile = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let end of imageSuffixList) {
|
||||||
|
if (vf.name.endsWith(end)) {
|
||||||
|
hasImageFile = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasOnnxFile && hasImageFile) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default pluginConfig
|
||||||
|
|
||||||
|
import { PluginExportType } from '@/types/type'
|
||||||
|
export const type = PluginExportType.component
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
<template>
|
||||||
|
<miao-drop-handler @on-virtual-files="handleDrop">
|
||||||
|
<miao-message-provider ref="miaoMessageRef">
|
||||||
|
<div class="onnx-demo" ref="rootRef">
|
||||||
|
<n-card title="ONNX模型演示" class="onnx-demo-card">
|
||||||
|
<n-grid :cols="2" :x-gap="12">
|
||||||
|
<n-grid-item>
|
||||||
|
<div class="onnx-demo-section">
|
||||||
|
<h3>输入图像</h3>
|
||||||
|
<div v-if="inputImage" class="onnx-demo-image-container">
|
||||||
|
<img :src="inputImage.url" class="onnx-demo-image" />
|
||||||
|
<p>{{ inputImage.name }}</p>
|
||||||
|
</div>
|
||||||
|
<n-empty v-else description="无图像" />
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item>
|
||||||
|
<div class="onnx-demo-section">
|
||||||
|
<h3>ONNX模型</h3>
|
||||||
|
<div v-if="onnxModel" class="onnx-demo-model-info">
|
||||||
|
<p>模型: {{ onnxModel.name }}</p>
|
||||||
|
<p>大小: {{ formatFileSize(onnxModel.size) }}</p>
|
||||||
|
</div>
|
||||||
|
<n-empty v-else description="无模型" />
|
||||||
|
</div>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
|
<!-- <div class="onnx-demo-control">
|
||||||
|
<n-button type="primary" @click="processImage" :disabled="!inputImage || !onnxModel || isProcessing"
|
||||||
|
:loading="isProcessing">
|
||||||
|
处理图像
|
||||||
|
</n-button>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="onnx-demo-result" v-if="modelOutput">
|
||||||
|
<h3>模型输出:</h3>
|
||||||
|
<n-card embedded>
|
||||||
|
<pre>{{ modelOutput }}</pre>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</miao-message-provider>
|
||||||
|
</miao-drop-handler>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import miaoDropHandler from '@/components/miaoDropHandler.vue'
|
||||||
|
import miaoMessageProvider from '@/components/miaoAlertTipProvider.vue'
|
||||||
|
import { VirtualFile } from '@/class/VirtualDirectory'
|
||||||
|
import VirtualDirectory from '@/class/VirtualDirectory'
|
||||||
|
import { onMounted, ref, computed, watch } from 'vue'
|
||||||
|
// @ts-ignore
|
||||||
|
let ort: any;
|
||||||
|
|
||||||
|
const currentFiles = defineModel<VirtualFile[]>('currentFiles', {
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentDirectories = defineModel<VirtualDirectory[]>(
|
||||||
|
'currentDirectories',
|
||||||
|
{
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const miaoMessageRef = ref<InstanceType<typeof miaoMessageProvider>>()
|
||||||
|
const rootRef = ref()
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
const modelOutput = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 获取第一个图像文件
|
||||||
|
const inputImage = computed(() => {
|
||||||
|
const imageSuffixList = [
|
||||||
|
'xbm', 'tif', 'pjp', 'svgz', 'jpg', 'jpeg', 'ico', 'tiff', 'gif',
|
||||||
|
'svg', 'jfif', 'webp', 'png', 'bmp', 'pjpeg', 'avif'
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const file of currentFiles.value) {
|
||||||
|
for (const suffix of imageSuffixList) {
|
||||||
|
if (file.name.toLowerCase().endsWith(suffix)) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取ONNX模型文件
|
||||||
|
const onnxModel = computed(() => {
|
||||||
|
for (const file of currentFiles.value) {
|
||||||
|
if (file.name.toLowerCase().endsWith('.onnx')) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleDrop = (vFiles: VirtualFile[]) => {
|
||||||
|
const imageSuffixList = [
|
||||||
|
'xbm', 'tif', 'pjp', 'svgz', 'jpg', 'jpeg', 'ico', 'tiff', 'gif',
|
||||||
|
'svg', 'jfif', 'webp', 'png', 'bmp', 'pjpeg', 'avif'
|
||||||
|
]
|
||||||
|
let imgFile: VirtualFile | null = null
|
||||||
|
out: for (const file of vFiles) {
|
||||||
|
for (const suffix of imageSuffixList) {
|
||||||
|
if (file.name.toLowerCase().endsWith(suffix)) {
|
||||||
|
imgFile = file
|
||||||
|
break out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imgFile) {
|
||||||
|
currentFiles.value.unshift(imgFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatFileSize = (size: number): string => {
|
||||||
|
if (size < 1024) {
|
||||||
|
return `${size} B`
|
||||||
|
} else if (size < 1024 * 1024) {
|
||||||
|
return `${(size / 1024).toFixed(2)} KB`
|
||||||
|
} else {
|
||||||
|
return `${(size / (1024 * 1024)).toFixed(2)} MB`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeImage = async (img: ImageBitmap, width: number, height: number): Promise<ImageBitmap> => {
|
||||||
|
const canvas = new OffscreenCanvas(width, height)
|
||||||
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx?.drawImage(img, 0, 0, width, height)
|
||||||
|
return await createImageBitmap(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图像
|
||||||
|
const processImage = async () => {
|
||||||
|
if (!inputImage.value || !onnxModel.value) {
|
||||||
|
miaoMessageRef.value?.alertTip('请选择图像和ONNX模型文件', { type: 'error', timeout: 2000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isProcessing.value = true
|
||||||
|
modelOutput.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const alertUpdate = miaoMessageRef.value!.alertTip('正在加载ONNX模型...', { type: 'info', timeout: 2000 })
|
||||||
|
|
||||||
|
const modelResponse = await fetch(onnxModel.value.url)
|
||||||
|
const modelBuffer = await modelResponse.arrayBuffer()
|
||||||
|
alertUpdate('正在创建推理会话...')
|
||||||
|
|
||||||
|
const session = await ort.InferenceSession.create(modelBuffer)
|
||||||
|
|
||||||
|
alertUpdate('正在加载图像...')
|
||||||
|
const imgResponse = await fetch(inputImage.value.url)
|
||||||
|
const imgBlob = await imgResponse.blob()
|
||||||
|
|
||||||
|
alertUpdate('正在预处理图像...')
|
||||||
|
const size = 448
|
||||||
|
const img = await createImageBitmap(imgBlob)
|
||||||
|
const data = await resizeImage(img, size, size)
|
||||||
|
|
||||||
|
const canvas = new OffscreenCanvas(size, size)
|
||||||
|
const ctx = canvas.getContext('2d') as unknown as CanvasRenderingContext2D
|
||||||
|
ctx.drawImage(data, 0, 0, size, size)
|
||||||
|
const imageData = ctx.getImageData(0, 0, size, size).data
|
||||||
|
const newImageData = new Uint8ClampedArray(size * size * 3)
|
||||||
|
for (let i = 0; i < size * size; i++) {
|
||||||
|
newImageData[i * 3] = imageData[i * 4]
|
||||||
|
newImageData[i * 3 + 1] = imageData[i * 4 + 1]
|
||||||
|
newImageData[i * 3 + 2] = imageData[i * 4 + 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// const greyScale = [];
|
||||||
|
// for (let i = 0; i < imageData.length; i += 4) {
|
||||||
|
// greyScale.push(
|
||||||
|
// (imageData[i] * 0.299 + imageData[i + 1] * 0.587 + imageData[i + 2] * 0.114 - 127.5) /
|
||||||
|
// 127.5
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// debugger
|
||||||
|
const inputTensor = new ort.Tensor('float32', new Float32Array(newImageData), [1, 3, size, size])
|
||||||
|
|
||||||
|
alertUpdate('执行模型推理...')
|
||||||
|
const outputMap = await session.run({
|
||||||
|
input: inputTensor
|
||||||
|
})
|
||||||
|
|
||||||
|
alertUpdate('处理模型输出结果...')
|
||||||
|
|
||||||
|
const outputTensor = Object.values(outputMap)[0]
|
||||||
|
const formattedOutput = JSON.stringify(
|
||||||
|
{
|
||||||
|
data: [...[...(outputTensor as { data: Float32Array }).data].entries()].sort((a, b) => b[1] - a[1]).map(([index, value]) => ({ index, value }))
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
modelOutput.value = formattedOutput
|
||||||
|
|
||||||
|
alertUpdate('处理完成!', { type: 'success' })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ONNX处理错误:', error)
|
||||||
|
miaoMessageRef.value?.alertTip(`处理出错`, { type: 'error', timeout: 2000 })
|
||||||
|
modelOutput.value = `错误: ${error}`
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log('currentDirectories.value[0].url', currentDirectories.value[0]?.url)
|
||||||
|
if (currentDirectories.value && currentDirectories.value.length > 0 && currentDirectories.value[0].url) {
|
||||||
|
const ortModule = await import(/* @vite-ignore */ `${currentDirectories.value[0].url}dist/ort.all.min.mjs`);
|
||||||
|
ort = ortModule;
|
||||||
|
console.log('ort', ort)
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
const ortModule = await import(/* @vite-ignore */ 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.all.min.js');
|
||||||
|
ort = ortModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
await initModel();
|
||||||
|
})
|
||||||
|
|
||||||
|
const initModel = async () => {
|
||||||
|
if (inputImage.value && onnxModel.value) {
|
||||||
|
miaoMessageRef.value?.alertTip('已检测到图像和ONNX模型,可以进行处理', { type: 'info', timeout: 2000 })
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => inputImage.value, () => {
|
||||||
|
processImage()
|
||||||
|
}, {
|
||||||
|
immediate: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.onnx-demo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onnx-demo-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onnx-demo-section {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onnx-demo-image-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onnx-demo-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 180px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onnx-demo-model-info {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onnx-demo-control {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onnx-demo-result {
|
||||||
|
margin-top: 16px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onnx-demo-result pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File renamed without changes.
@@ -3,6 +3,7 @@ import VirtualDirectory, { VirtualFile } from "@/class/VirtualDirectory"
|
|||||||
import VirtualPage from "@/class/VirtualPage"
|
import VirtualPage from "@/class/VirtualPage"
|
||||||
import usePluginCenter from "@/hooks/usePluginCenter"
|
import usePluginCenter from "@/hooks/usePluginCenter"
|
||||||
import useVirtualPages from "@/hooks/useVirtualPages"
|
import useVirtualPages from "@/hooks/useVirtualPages"
|
||||||
|
import { Component } from "vue"
|
||||||
|
|
||||||
export const componentOption2pluginOption = (option: registerComponentOption) => {
|
export const componentOption2pluginOption = (option: registerComponentOption) => {
|
||||||
const {
|
const {
|
||||||
@@ -17,7 +18,8 @@ export const componentOption2pluginOption = (option: registerComponentOption) =>
|
|||||||
single = false,
|
single = false,
|
||||||
priority = 0
|
priority = 0
|
||||||
} = option
|
} = option
|
||||||
let _component: any
|
let _component: Component
|
||||||
|
let _componentPromise: Promise<Component>
|
||||||
const views = useVirtualPages()
|
const views = useVirtualPages()
|
||||||
const pluginCenter = usePluginCenter()
|
const pluginCenter = usePluginCenter()
|
||||||
return {
|
return {
|
||||||
@@ -36,13 +38,19 @@ export const componentOption2pluginOption = (option: registerComponentOption) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_component === undefined) {
|
if (_component === undefined) {
|
||||||
const setAlertTip = pluginCenter.globalAlertTip(`加载 ${name} 插件中`)
|
if (_componentPromise !== undefined) {
|
||||||
_component = await getComponent()
|
await _componentPromise
|
||||||
setAlertTip &&
|
}
|
||||||
setAlertTip(`加载 ${name} 插件完成`, {
|
else {
|
||||||
type: 'success',
|
const setAlertTip = pluginCenter.globalAlertTip(`加载 ${name} 插件中`)
|
||||||
timeout: 2000
|
_componentPromise = getComponent()
|
||||||
})
|
_component = await _componentPromise
|
||||||
|
setAlertTip &&
|
||||||
|
setAlertTip(`加载 ${name} 插件完成`, {
|
||||||
|
type: 'success',
|
||||||
|
timeout: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const _findRes = views.find(_component) as VirtualPage[]
|
const _findRes = views.find(_component) as VirtualPage[]
|
||||||
if (single && _findRes.length !== 0) {
|
if (single && _findRes.length !== 0) {
|
||||||
|
|||||||
Reference in new issue
Block a user