diff --git a/.gitignore b/.gitignore index ecd4f8f..e8b108b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,10 +23,20 @@ dist-ssr *.sln *.sw? .vscode + 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 +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~ + test stats.html diff --git a/build.auto.release.sh b/build.auto.release.sh index ca2e7ba..6b1a8e9 100644 --- a/build.auto.release.sh +++ b/build.auto.release.sh @@ -1,5 +1,16 @@ #!/bin/bash +# 检查UPX是否存在 +check_upx() { + if command -v upx &> /dev/null; then + echo "UPX已找到,将执行可执行文件压缩" + return 0 + else + echo "UPX未找到,跳过压缩步骤" + return 1 + fi +} + pnpm run build rm -rf ./src-server/web/* @@ -8,16 +19,42 @@ cp -r ./dist/* ./src-server/web/ cd ./src-server -echo "building amd64 win" +# 安装webview2依赖(如果尚未安装) +go get github.com/jchv/go-webview2 +# 构建不带GUI版本(Windows) +echo "building amd64 win (no GUI)" go env -w GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ../miao-directory-amd64-win.exe -echo "building amd64 linux" +# 尝试UPX压缩Windows无GUI版本 +if check_upx; then + echo "正在压缩Windows无GUI版本" + upx -9 -o ../miao-directory-amd64-win.upx.exe ../miao-directory-amd64-win.exe +fi +# 构建不带GUI版本(Linux) +echo "building amd64 linux (no GUI)" go env -w GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ../miao-directory-amd64-linux +# 构建带GUI版本(仅Windows,go-webview2主要针对Windows平台) +echo "building amd64 win with GUI" +go env -w GOOS=windows GOARCH=amd64 +go build -tags webview -ldflags="-s -w -H=windowsgui" -o ../miao-directory-amd64-win.gui.exe + +# 尝试UPX压缩Windows GUI版本 +if check_upx; then + echo "正在压缩Windows GUI版本" + upx -9 -o ../miao-directory-amd64-win.gui.upx.exe ../miao-directory-amd64-win.gui.exe +fi + +# 如果需要为Linux构建带GUI版本,需要系统安装相应的依赖 +# echo "building amd64 linux with GUI" +# go env -w GOOS=linux GOARCH=amd64 +# go build -tags webview -ldflags="-s -w" -o ../miao-directory-gui-amd64-linux + +# 可选:ARM64 Linux版本 # go env -w GOOS=linux GOARCH=arm64 # go build -ldflags="-s -w" -o ../miao-directory-arm64-linux diff --git a/src-server/api/addressesHandler.go b/src-server/api/addressesHandler.go new file mode 100644 index 0000000..0bfebe2 --- /dev/null +++ b/src-server/api/addressesHandler.go @@ -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 +} diff --git a/src-server/go.mod b/src-server/go.mod index 9302537..ad65d8c 100644 --- a/src-server/go.mod +++ b/src-server/go.mod @@ -16,6 +16,8 @@ require ( 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 @@ -26,10 +28,11 @@ require ( 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.24.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 diff --git a/src-server/go.sum b/src-server/go.sum index 89b1376..b45bef0 100644 --- a/src-server/go.sum +++ b/src-server/go.sum @@ -28,6 +28,11 @@ github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaC 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= @@ -64,16 +69,22 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS 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= diff --git a/src-server/main.go b/src-server/main.go index 3d3d2c6..4d54de0 100644 --- a/src-server/main.go +++ b/src-server/main.go @@ -39,6 +39,9 @@ func main() { router.POST("/rename/*path", api.RenameHandler(fsOperateEventCenter)) router.POST("/delete/*path", api.DeleteHandler(fsOperateEventCenter)) + // 添加获取所有可用地址的API + router.GET("/addresses", api.AddressesHandler(port)) + // Redirect from '/' to '/web/' router.GET("/", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/web/") @@ -54,7 +57,22 @@ func main() { address := "[::]" + port printListeningAddresses(port) - router.Run(address) + + // 在非阻塞的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 { diff --git a/src-server/webview.go b/src-server/webview.go new file mode 100644 index 0000000..9a4e431 --- /dev/null +++ b/src-server/webview.go @@ -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 diff --git a/src-server/webview_no.go b/src-server/webview_no.go new file mode 100644 index 0000000..aa8d811 --- /dev/null +++ b/src-server/webview_no.go @@ -0,0 +1,12 @@ +//go:build !webview +// +build !webview + +package main + +// 定义是否使用WebView的变量 +var useWebView = false + +// 空函数,在非webview版本中不执行任何操作 +func startWebView(port string) { + // 在非webview版本中不做任何事情 +} diff --git a/src/App.vue b/src/App.vue index 7e1772d..860c4dc 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,7 +12,7 @@ import type VirtualDirectory from './class/VirtualDirectory' import type { VirtualFile } from './class/VirtualDirectory' import MiaoMask from './components/miaoMask.vue' import MiaoMessageProvider from './components/miaoAlertTipProvider.vue' -import config from './config' +// import config from './config' import useVirtualPages from './hooks/useVirtualPages' import init from './hooks/init' import usePluginCenter from './hooks/usePluginCenter' @@ -22,8 +22,9 @@ 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(false) const viewRef = ref() @@ -36,6 +37,7 @@ const views = useVirtualPages() let pluginCenter = ref() const rootDirectory = useRootVDirectory() + // component就是一个vue组件,类似于miaoDirectory const createView = ( component: any, @@ -77,35 +79,34 @@ const openMenuOption = computed( }, pluginCenter.value ? { - label: '插件', - key: 'plugin', - children: [ - ...pluginCenter.value - .getUsablePlugin([], [], { - group: [PluginGroup.mainMenu] - }) - .map((v) => ({ - label: `${v.name}`, - key: `plugin:${v.key}`, - icon: v.icon ? renderIcon(v.icon) : undefined - })) - ], - icon: renderIcon(ExtensionPuzzleOutline) - } + label: '插件', + key: 'plugin', + children: [ + ...pluginCenter.value + .getUsablePlugin([], [], { + group: [PluginGroup.mainMenu] + }) + .map((v) => ({ + label: `${v.name}`, + key: `plugin:${v.key}`, + icon: v.icon ? renderIcon(v.icon) : undefined + })) + ], + icon: renderIcon(ExtensionPuzzleOutline) + } : undefined ].filter((v) => v) as DropdownMixedOption[] ) const handleMenuSelect = async (key: string) => { if (key === 'share') { + // 立即显示模态框 showModal.value = true + + // 设置模态框数据,将serverAddressesService传递给组件 modalData.value = { - component: (await import('naive-ui/es/qr-code')).NQrCode, - props: { - value: baseUrl, - size: 300, - errorCorrectionLevel: 'H' - } + component: ShareQrCodeCard, + props: {} } } else if (key.startsWith('plugin:')) { const pluginName = key.split(':')[1] @@ -113,6 +114,13 @@ const handleMenuSelect = async (key: string) => { } } +const handleModalClose = () => { + showModal.value = false + nextTick(() => { + modalData.value = undefined + }) +} + onMounted(async () => { // @ts-ignore await init(messageProviderRef.value?.alertTip) @@ -127,25 +135,15 @@ provide('rootDirectory', rootDirectory) diff --git a/src/components/APP/ShareQrCodeCard.vue b/src/components/APP/ShareQrCodeCard.vue new file mode 100644 index 0000000..9f375ad --- /dev/null +++ b/src/components/APP/ShareQrCodeCard.vue @@ -0,0 +1,139 @@ + + + + + \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 6425164..10fab78 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,7 +14,8 @@ export default { query: '/query', rename: '/rename', mkdir: '/mkdir', - cut: '/cut' + cut: '/cut', + addresses: '/addresses' }, enableUpload: true } diff --git a/src/hooks/useServerAddresses.ts b/src/hooks/useServerAddresses.ts new file mode 100644 index 0000000..0b82f70 --- /dev/null +++ b/src/hooks/useServerAddresses.ts @@ -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([]) + const loading = ref(false) + const error = ref(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 + } +} \ No newline at end of file diff --git a/src/utils/getLocalIpAddress.ts b/src/utils/getLocalIpAddress.ts new file mode 100644 index 0000000..f078f6c --- /dev/null +++ b/src/utils/getLocalIpAddress.ts @@ -0,0 +1,70 @@ +/** + * 获取本机的局域网IP地址 + * 使用Web API获取可用的网络接口信息(如果浏览器支持) + * @returns 一个Promise,解析为包含可用局域网IP地址的数组 + */ +export async function getLocalIpAddresses(): Promise { + // 部分浏览器未实现RTCPeerConnection对象 + if (!window.RTCPeerConnection) { + console.warn('当前浏览器不支持获取局域网IP') + return [] + } + + return new Promise((resolve) => { + const ips: string[] = [] + const pc = new RTCPeerConnection({ + // 使用Google的STUN服务器 + iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] + }) + + pc.createDataChannel('') + + // 创建一个offer来触发ICE候选者的收集 + pc.createOffer() + .then(offer => pc.setLocalDescription(offer)) + .catch(err => console.error('获取IP地址时出错:', err)) + + // 监听ICE候选者事件 + pc.onicecandidate = (event) => { + if (!event.candidate) { + // ICE收集完成 + pc.close() + resolve(ips) + return + } + + // 解析候选者信息中的IP地址 + const ipMatch = /([0-9]{1,3}(\.[0-9]{1,3}){3})/.exec(event.candidate.candidate) + const ipAddress = ipMatch && ipMatch[1] + + if (ipAddress && !ips.includes(ipAddress) && isPrivateIp(ipAddress)) { + ips.push(ipAddress) + } + } + + // 5秒超时,确保不会无限等待 + setTimeout(() => { + pc.close() + resolve(ips) + }, 5000) + }) +} + +/** + * 判断一个IP地址是否为私有局域网IP + * @param ip IP地址 + * @returns 是否为私有IP + */ +function isPrivateIp(ip: string): boolean { + const parts = ip.split('.').map(part => parseInt(part, 10)) + + // 检查是否是局域网IP + // 10.0.0.0 - 10.255.255.255 + // 172.16.0.0 - 172.31.255.255 + // 192.168.0.0 - 192.168.255.255 + return ( + (parts[0] === 10) || + (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || + (parts[0] === 192 && parts[1] === 168) + ) +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index d8c3fa3..c9cd788 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,7 +40,7 @@ export default defineConfig({ }, server: { proxy: { - '^/(get|upload|delete|query|rename|cut|mkdir)': { + '^/(get|upload|delete|query|rename|cut|mkdir|addresses)': { target: 'http://127.0.0.1:17705', changeOrigin: true, rewrite: (path) => path