feat: 添加基于webview实现的包含GUI的版本,添加通过upx进行压缩的版本,优化分享功能。
This commit is contained in:
1 parent
7f26b46cde
commit
f9b93624e4
14 files changed
+512
-45
No files matched your search
+37
-39
@@ -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<boolean>(false)
|
||||
const viewRef = ref<HTMLDivElement>()
|
||||
@@ -36,6 +37,7 @@ const views = useVirtualPages()
|
||||
let pluginCenter = ref<PluginCenter>()
|
||||
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)
|
||||
<template>
|
||||
<div class="view" ref="viewRef">
|
||||
<miao-message-provider ref="messageProviderRef">
|
||||
<ViewController
|
||||
:views="views"
|
||||
:handleClickTitle="handleClickTitle"
|
||||
:createView="createView"
|
||||
:deleteView="deleteView"
|
||||
:openMenuOption="openMenuOption"
|
||||
:handleMenuSelect="handleMenuSelect" />
|
||||
<ViewContainer
|
||||
:views="views"
|
||||
:deleteView="deleteView"
|
||||
:handleClickTitle="handleClickTitle" />
|
||||
<ViewController :views="views" :handleClickTitle="handleClickTitle" :createView="createView"
|
||||
:deleteView="deleteView" :openMenuOption="openMenuOption" :handleMenuSelect="handleMenuSelect" />
|
||||
<ViewContainer :views="views" :deleteView="deleteView" :handleClickTitle="handleClickTitle" />
|
||||
</miao-message-provider>
|
||||
</div>
|
||||
<!-- 模态框展示,用来显示分享二维码,设置菜单之类的东西 -->
|
||||
<MiaoMask v-model:show="showModal" @click="showModal = false">
|
||||
<component
|
||||
@click="(e: any) => e.stopPropagation()"
|
||||
:is="modalData?.component"
|
||||
v-bind="modalData?.props"></component>
|
||||
<MiaoMask v-model:show="showModal" @click="handleModalClose">
|
||||
<component @click="(e: any) => e.stopPropagation()" :is="modalData?.component" v-bind="modalData?.props">
|
||||
</component>
|
||||
</MiaoMask>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<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">
|
||||
<n-grid :cols="2" :x-gap="16" :y-gap="16">
|
||||
<n-grid-item v-for="(item, index) in shareLinks" :key="index">
|
||||
<n-card embedded class="qrcode-card">
|
||||
<template #header>
|
||||
<div class="card-header bg-zinc-100 p-3 rounded-md">
|
||||
<n-text>{{ item.title }}</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>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</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: 680px;
|
||||
max-width: 800px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.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>
|
||||
+2
-1
@@ -14,7 +14,8 @@ export default {
|
||||
query: '/query',
|
||||
rename: '/rename',
|
||||
mkdir: '/mkdir',
|
||||
cut: '/cut'
|
||||
cut: '/cut',
|
||||
addresses: '/addresses'
|
||||
},
|
||||
enableUpload: true
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 获取本机的局域网IP地址
|
||||
* 使用Web API获取可用的网络接口信息(如果浏览器支持)
|
||||
* @returns 一个Promise,解析为包含可用局域网IP地址的数组
|
||||
*/
|
||||
export async function getLocalIpAddresses(): Promise<string[]> {
|
||||
// 部分浏览器未实现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)
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user