- Reorganize project structure with new electron and shared directories - Add comprehensive i18n support with Chinese, English, and Japanese locales - Update build configurations and TypeScript paths for new structure - Add various UI components including chat interface and task management - Include Windows release binaries and localization files - Update dependencies and fix import paths throughout the codebase
148 lines
5.3 KiB
Vue
148 lines
5.3 KiB
Vue
<template>
|
||
<div class="h-screen w-screen bg-gray-100">
|
||
<div class="flex items-center gap-1 px-3 h-10 border-b bg-white" style="-webkit-app-region: drag">
|
||
<div class="flex items-center gap-2 mr-2">
|
||
<button class="w-3 h-3 rounded-full bg-red-500" style="-webkit-app-region: no-drag" @click="onCloseWindow">
|
||
<RiCloseLine />
|
||
</button>
|
||
<button class="w-3 h-3 rounded-full bg-yellow-400" style="-webkit-app-region: no-drag"
|
||
@click="onMinimizeWindow">
|
||
<RiSubtractLine />
|
||
</button>
|
||
<button class="w-3 h-3 rounded-full bg-green-500" style="-webkit-app-region: no-drag" @click="onMaximizeWindow">
|
||
<RiSquareLine />
|
||
</button>
|
||
</div>
|
||
<div v-for="t in tabs" :key="t.id" class="flex items-center px-2 py-1 rounded cursor-pointer"
|
||
:class="t.id === activeId ? 'bg-blue-100' : 'hover:bg-gray-200'" @click="onSwitch(t.id)"
|
||
style="-webkit-app-region: no-drag">
|
||
<span class="text-sm mr-2 truncate max-w-[160px]">{{ t.title || t.url || '新标签页' }}</span>
|
||
<button class="text-gray-600 hover:text-black" style="-webkit-app-region: no-drag"
|
||
@click.stop="onCloseTabId(t.id)">✕</button>
|
||
</div>
|
||
<button class="ml-2 px-2 py-1 rounded bg-gray-200 hover:bg-gray-300" style="-webkit-app-region: no-drag"
|
||
@click="onNewTab">+</button>
|
||
</div>
|
||
<div class="flex items-center gap-2 px-3 h-12 border-b bg-gray-50">
|
||
<button class="px-2 py-1 bg-gray-200 rounded" @click="onBack" :disabled="!active?.canGoBack">
|
||
<RiArrowLeftSLine />
|
||
</button>
|
||
<button class="px-2 py-1 bg-gray-200 rounded" @click="onForward" :disabled="!active?.canGoForward">
|
||
<RiArrowRightSLine />
|
||
</button>
|
||
<button class="px-2 py-1 bg-gray-200 rounded" @click="onReload">
|
||
<RiResetLeftLine />
|
||
</button>
|
||
<input class="flex-1 px-3 py-1 border rounded-full" v-model="address" @keyup.enter="onNavigate"
|
||
placeholder="输入地址后回车" />
|
||
</div>
|
||
<div class="h-[calc(100vh-5.5rem)]"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { reactive, ref, onMounted, computed } from 'vue'
|
||
import { RiArrowRightSLine, RiArrowLeftSLine, RiResetLeftLine, RiCloseLine, RiSubtractLine, RiSquareLine } from '@remixicon/vue'
|
||
|
||
type TabInfo = { id: string; url: string; title: string; isLoading: boolean; canGoBack: boolean; canGoForward: boolean }
|
||
|
||
const tabs = reactive<TabInfo[]>([])
|
||
const activeId = ref<string>('')
|
||
const address = ref<string>('')
|
||
|
||
const active = computed(() => tabs.find(t => t.id === activeId.value))
|
||
|
||
const refreshActiveAddress = () => {
|
||
address.value = active.value?.url || ''
|
||
}
|
||
|
||
const syncList = async () => {
|
||
const list: TabInfo[] = await (window as any).api.tabs.list()
|
||
tabs.splice(0, tabs.length, ...list)
|
||
if (!activeId.value && list.length > 0) activeId.value = list[0].id
|
||
refreshActiveAddress()
|
||
}
|
||
|
||
const onNewTab = async () => {
|
||
const info: TabInfo = await (window as any).api.tabs.create('about:blank')
|
||
activeId.value = info.id
|
||
}
|
||
|
||
const onSwitch = async (id: string) => {
|
||
await (window as any).api.tabs.switch(id)
|
||
activeId.value = id
|
||
refreshActiveAddress()
|
||
}
|
||
|
||
const onCloseTab = async () => {
|
||
if (!activeId.value) return
|
||
await (window as any).api.tabs.close(activeId.value)
|
||
}
|
||
|
||
const normalizeUrl = (u: string) => {
|
||
const trimmed = u.trim()
|
||
if (!trimmed) return ''
|
||
if (/^(https?:|file:|about:)/i.test(trimmed)) return trimmed
|
||
return `https://${trimmed}`
|
||
}
|
||
|
||
const onNavigate = async () => {
|
||
if (!activeId.value || !address.value) return
|
||
const url = normalizeUrl(address.value)
|
||
if (!url) return
|
||
await (window as any).api.tabs.navigate(activeId.value, url)
|
||
}
|
||
|
||
const onReload = async () => {
|
||
if (!activeId.value) return
|
||
await (window as any).api.tabs.reload(activeId.value)
|
||
}
|
||
|
||
const onBack = async () => {
|
||
if (!activeId.value) return
|
||
await (window as any).api.tabs.back(activeId.value)
|
||
}
|
||
|
||
const onForward = async () => {
|
||
if (!activeId.value) return
|
||
await (window as any).api.tabs.forward(activeId.value)
|
||
}
|
||
|
||
const onCloseTabId = async (id: string) => {
|
||
await (window as any).api.tabs.close(id)
|
||
}
|
||
|
||
const onCloseWindow = () => (window as any).api.window.close()
|
||
const onMinimizeWindow = () => (window as any).api.window.minimize()
|
||
const onMaximizeWindow = () => (window as any).api.window.maximize()
|
||
|
||
onMounted(async () => {
|
||
await syncList()
|
||
; (window as any).api.tabs.on('tab-created', (info: TabInfo) => {
|
||
const i = tabs.findIndex(t => t.id === info.id)
|
||
if (i === -1) tabs.push(info)
|
||
activeId.value = info.id
|
||
refreshActiveAddress()
|
||
})
|
||
; (window as any).api.tabs.on('tab-updated', (info: TabInfo) => {
|
||
const i = tabs.findIndex(t => t.id === info.id)
|
||
if (i >= 0) tabs[i] = info
|
||
if (activeId.value === info.id) refreshActiveAddress()
|
||
})
|
||
; (window as any).api.tabs.on('tab-closed', ({ tabId }: { tabId: string }) => {
|
||
const i = tabs.findIndex(t => t.id === tabId)
|
||
if (i >= 0) tabs.splice(i, 1)
|
||
if (activeId.value === tabId) {
|
||
const next = tabs[0]
|
||
activeId.value = next?.id || ''
|
||
refreshActiveAddress()
|
||
}
|
||
})
|
||
; (window as any).api.tabs.on('tab-switched', ({ tabId }: { tabId: string }) => {
|
||
activeId.value = tabId
|
||
refreshActiveAddress()
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style scoped></style> |