💥 破坏性更新 咱不推荐更新
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<script setup lang="ts">
|
||||
import { RefreshCcw,XCircle,ArrowRightLeft,ArrowLeftFromLine,ArrowRightFromLine } from 'lucide-vue-next';
|
||||
import {useTabStore} from "~/stores/admin/tab";
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
currentPath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
x: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
y: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:show'])
|
||||
|
||||
const tabStore = useTabStore()
|
||||
|
||||
const options = computed(() => [
|
||||
{
|
||||
label: '重新加载',
|
||||
key: 'reload',
|
||||
disabled: props.currentPath !== tabStore.activeTab,
|
||||
icon: () => h(RefreshCcw),
|
||||
},
|
||||
{
|
||||
label: '关闭',
|
||||
key: 'close',
|
||||
disabled: tabStore.tabs.length <= 1,
|
||||
icon: () => h(XCircle),
|
||||
},
|
||||
{
|
||||
label: '关闭其他',
|
||||
key: 'close-other',
|
||||
disabled: tabStore.tabs.length <= 1,
|
||||
icon: () => h(ArrowRightLeft),
|
||||
},
|
||||
{
|
||||
label: '关闭左侧',
|
||||
key: 'close-left',
|
||||
disabled: tabStore.tabs.length <= 1 || props.currentPath === tabStore.tabs[0].path,
|
||||
icon: () => h(ArrowLeftFromLine),
|
||||
},
|
||||
{
|
||||
label: '关闭右侧',
|
||||
key: 'close-right',
|
||||
disabled:
|
||||
tabStore.tabs.length <= 1 ||
|
||||
props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
|
||||
icon: () => h(ArrowRightFromLine),
|
||||
},
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
const actionMap = new Map([
|
||||
[
|
||||
'reload',
|
||||
() => {
|
||||
tabStore.reloadTab(route.fullPath)
|
||||
},
|
||||
],
|
||||
[
|
||||
'close',
|
||||
() => {
|
||||
tabStore.removeTab(props.currentPath)
|
||||
},
|
||||
],
|
||||
[
|
||||
'close-other',
|
||||
() => {
|
||||
tabStore.removeOther(props.currentPath)
|
||||
},
|
||||
],
|
||||
[
|
||||
'close-left',
|
||||
() => {
|
||||
tabStore.removeLeft(props.currentPath)
|
||||
},
|
||||
],
|
||||
[
|
||||
'close-right',
|
||||
() => {
|
||||
tabStore.removeRight(props.currentPath)
|
||||
},
|
||||
],
|
||||
])
|
||||
|
||||
function handleHideDropdown() {
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
function handleSelect(key:any) {
|
||||
const actionFn = actionMap.get(key)
|
||||
actionFn && actionFn()
|
||||
handleHideDropdown()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-dropdown
|
||||
:show="show"
|
||||
:options="options"
|
||||
:x="x"
|
||||
:y="y"
|
||||
placement="bottom-start"
|
||||
@clickoutside="handleHideDropdown"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import ContextMenu from './ContextMenu.vue'
|
||||
import {useTabStore} from "~/stores/admin/tab";
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
// const appStore = useAppStore()
|
||||
const tabStore = useTabStore()
|
||||
|
||||
const contextMenuOption = reactive({
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
currentPath: '',
|
||||
})
|
||||
|
||||
const handleItemClick = (path:any) => {
|
||||
tabStore.setActiveTab(path)
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function showContextMenu() {
|
||||
contextMenuOption.show = true
|
||||
}
|
||||
function hideContextMenu() {
|
||||
contextMenuOption.show = false
|
||||
}
|
||||
function setContextMenu(x:any, y:any, currentPath:any) {
|
||||
Object.assign(contextMenuOption, { x, y, currentPath })
|
||||
}
|
||||
|
||||
// 右击菜单
|
||||
async function handleContextMenu(e:any, tagItem:any) {
|
||||
const { clientX, clientY } = e
|
||||
hideContextMenu()
|
||||
setContextMenu(clientX, clientY, tagItem.path)
|
||||
await nextTick()
|
||||
showContextMenu()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-tabs
|
||||
:value="tabStore.activeTab"
|
||||
:closable="tabStore.tabs.length > 1"
|
||||
:style="`--selected-bg: ${appStore.isDark ? '#1b2429' : '#eaf0f1'}`"
|
||||
type="card"
|
||||
@close="(path) => tabStore.removeTab(path)"
|
||||
>
|
||||
<n-tab
|
||||
v-for="item in tabStore.tabs"
|
||||
:key="item.path"
|
||||
:name="item.path"
|
||||
@click="handleItemClick(item.path)"
|
||||
@contextmenu.prevent="handleContextMenu($event, item)"
|
||||
>
|
||||
{{ item.title }}
|
||||
</n-tab>
|
||||
</n-tabs>
|
||||
|
||||
<ContextMenu
|
||||
v-if="contextMenuOption.show"
|
||||
v-model:show="contextMenuOption.show"
|
||||
:current-path="contextMenuOption.currentPath"
|
||||
:x="contextMenuOption.x"
|
||||
:y="contextMenuOption.y"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.n-tabs) {
|
||||
.n-tabs-tab {
|
||||
padding-left: 16px;
|
||||
height: 36px;
|
||||
background: transparent !important;
|
||||
border-radius: 4px !important;
|
||||
margin-right: 4px;
|
||||
&:hover {
|
||||
border: 1px solid var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
.n-tabs-tab--active {
|
||||
border: 1px solid var(--primary-color) !important;
|
||||
background-color: var(--selected-bg) !important;
|
||||
}
|
||||
.n-tabs-pad,
|
||||
.n-tabs-tab-pad,
|
||||
.n-tabs-scroll-padding {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+77
-89
@@ -1,89 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import {useStyleStore} from "~/stores/style";
|
||||
import {useApisStore} from "~/stores/api";
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const state = reactive({
|
||||
domain: '',
|
||||
})
|
||||
|
||||
const toast = useToast();
|
||||
const {t} = useI18n()
|
||||
const router = useRouter();
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const localePath = useLocalePath()
|
||||
const settingsStore = useSettingsStore()
|
||||
const domainStore = useDomainStore()
|
||||
|
||||
const SupportedTLDs = new Set(Object.keys(domainStore.SupportedTLDs));
|
||||
const handleAction = async (url: any) => {
|
||||
if (!state.domain) return toast.add({ title: '请输入域名' })
|
||||
let domain = trimDomain(state.domain);
|
||||
const parts = splitDomain(domain);
|
||||
|
||||
if (!validateDomain(parts) || !isTLDValid(parts)) return;
|
||||
|
||||
domain = updateDomainForTLD(parts);
|
||||
state.domain = domain;
|
||||
|
||||
const isLink = ref({})
|
||||
isLink.value = settingsStore.linkOpenType != 'currentWindow'
|
||||
await router.push(localePath(`/${url}/${state.domain.replace(/\./g, '_')}.html`))
|
||||
}
|
||||
|
||||
|
||||
const trimDomain = (domain: string): string => {
|
||||
return domain.trim().toLowerCase(); // 确保域名为小写
|
||||
};
|
||||
|
||||
const splitDomain = (domain: string): string[] => {
|
||||
return domain.split('.');
|
||||
};
|
||||
|
||||
const validateDomain = (parts: string[]): boolean => {
|
||||
if (parts.length < 2) {
|
||||
toast.add({ title: '域名格式不正确' });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
const isTLDValid = (parts: string[]): boolean => {
|
||||
const lastPart = parts[parts.length - 1].toLowerCase(); // 获取最后一部分,并确保为小写
|
||||
const potentialTLD = parts.slice(-2).join('.').toLowerCase(); // 获取可能的多部分TLD,并确保为小写
|
||||
|
||||
if (!SupportedTLDs.has(lastPart) && !SupportedTLDs.has(potentialTLD)) {
|
||||
toast.add({ title: '域名后缀不合法' });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
const updateDomainForTLD = (parts: string[]): string => {
|
||||
const potentialTLD = parts.slice(-2).join('.').toLowerCase(); // 确保为小写
|
||||
let domainToKeep: string;
|
||||
if (SupportedTLDs.has(potentialTLD)) {
|
||||
domainToKeep = parts.length > 2 ? parts.slice(-3).join('.') : parts.join('.');
|
||||
} else {
|
||||
domainToKeep = parts.slice(-2).join('.');
|
||||
}
|
||||
return domainToKeep;
|
||||
};
|
||||
|
||||
const styleStore = useStyleStore()
|
||||
const clientMounted = ref(false);
|
||||
const apisStore = useApisStore()
|
||||
|
||||
const message = useMessage()
|
||||
const handleAction = async (url: any) => {
|
||||
if (!settingsStore.getDomain) return message.error('请输入域名')
|
||||
|
||||
// 正则表达式匹配域名
|
||||
const domainPattern = /^(?!:\/\/)([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9]{2,11}?$/;
|
||||
// 正则表达式匹配IPv4地址
|
||||
const ipPattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
|
||||
// 检查state.domain是否匹配域名或IP地址的格式
|
||||
if (!domainPattern.test(settingsStore.getDomain) && !ipPattern.test(settingsStore.getDomain)) {
|
||||
message.error('请输入正确的域名或IP地址')
|
||||
return;
|
||||
}
|
||||
|
||||
await router.push(localePath(`/${url}/${settingsStore.getDomain.replace(/\./g, '_')}.html`))
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
clientMounted.value = true;
|
||||
});
|
||||
|
||||
const selectOptions = ref([
|
||||
{
|
||||
label: 'Whois',
|
||||
value: 'whois'
|
||||
}, {
|
||||
label: 'Dns',
|
||||
value: 'dns'
|
||||
}, {
|
||||
label: 'Domain',
|
||||
value: 'domain'
|
||||
}
|
||||
])
|
||||
const {selectedOption} = storeToRefs(settingsStore)
|
||||
const handleSelectOptions = (value: any) => {
|
||||
settingsStore.setSelectedOption(value);
|
||||
console.log(selectedOption.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full text-xs bg-[#F1F3F4] dark:bg-transparent"
|
||||
class="w-full text-xs dark:bg-transparent"
|
||||
:class="{ 'h-[90vh]': !styleStore.getIsPage && clientMounted }"
|
||||
>
|
||||
<div
|
||||
@@ -99,34 +70,51 @@ onMounted(() => {
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
<div class="mt-6">
|
||||
<UForm :state="state"
|
||||
class="flex items-center space-x-2 mb-3 dark:text-white"
|
||||
@submit="handleAction('whois')">
|
||||
<div class="flex items-center space-x-2 mb-3 dark:text-white"
|
||||
>
|
||||
<!-- 容器div用于水平布局 -->
|
||||
<div class="flex-grow">
|
||||
<UInput
|
||||
v-model="state.domain"
|
||||
:placeholder="t('index.placeholder')"
|
||||
color="sky"
|
||||
size="xl"
|
||||
class="w-full " />
|
||||
<NInputGroup>
|
||||
<n-select
|
||||
:style="{ width: '20%' }"
|
||||
size="large"
|
||||
v-model:value="selectedOption"
|
||||
:options="selectOptions"
|
||||
@update:value="handleSelectOptions"
|
||||
/>
|
||||
<NInput
|
||||
v-model:value="settingsStore.domainSearch"
|
||||
@keyup.enter="handleAction(settingsStore.selectedOption)"
|
||||
type="text"
|
||||
:placeholder="t('index.placeholder')"
|
||||
size="large"
|
||||
clearable
|
||||
autofocus
|
||||
class="w-full "/>
|
||||
</NInputGroup>
|
||||
</div>
|
||||
<!-- 使用v-if或v-show基于state.domain的值来控制按钮的显示 -->
|
||||
<UButton type="submit" color="sky" size="xl" v-if="state.domain">
|
||||
<!-- 使用v-if基于state.domain的值来控制按钮的显示 -->
|
||||
<NButton type="primary"
|
||||
size="large"
|
||||
@click="handleAction(settingsStore.selectedOption)"
|
||||
v-if="settingsStore.domainSearch">
|
||||
{{ t('index.onSubmit') }}
|
||||
</UButton>
|
||||
</UForm>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
<CommonBulletin
|
||||
v-if="!styleStore.isPage && clientMounted"
|
||||
:text="`➡️ ${t('index.tips') }`"
|
||||
/>
|
||||
|
||||
<TabList @action="handleAction" />
|
||||
<slot />
|
||||
<ClientOnly>
|
||||
<CommonBulletin
|
||||
v-if="settingsStore.isBulletin && !styleStore.isPage"
|
||||
:text="`➡️ ${t('index.tips') }`"
|
||||
/>
|
||||
</ClientOnly>
|
||||
|
||||
<TabList @action="handleAction"/>
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
<CommonFooter />
|
||||
<CommonFooter/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-[90vh] text-xs bg-[#F1F3F4] dark:bg-transparent">
|
||||
<div class="max-w-screen-lg mx-auto pt-[15vh] px-[1em] pb-[10vh] ">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<CommonFooter />
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full text-xs bg-[#F1F3F4] dark:bg-transparent">
|
||||
<div class="max-w-screen-lg mx-auto pt-[5vh] px-[1em] pb-[10vh] ">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
|
||||
import {useAppStore} from "~/stores/admin/app";
|
||||
|
||||
const appStore = useAppStore()
|
||||
const isDark = useDark()
|
||||
const toggleDark = () => {
|
||||
appStore.toggleDark()
|
||||
useToggle(isDark)()
|
||||
}
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen()
|
||||
|
||||
function handleLinkClick(link:string) {
|
||||
window.open(link)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CommonAppCard class="flex items-center px-12" border-b="1px solid light_border dark:dark_border">
|
||||
<MenuCollapse />
|
||||
|
||||
<BreadCrumb />
|
||||
|
||||
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18" >
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="mr-16 f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover" @click="toggleDark">
|
||||
<!-- 根据 isDark 条件动态切换图标 -->
|
||||
<IconMoon v-if="isDark" />
|
||||
<IconSun v-else />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 根据 isDark 条件动态切换提示文本 -->
|
||||
<span>{{ isDark ? '夜间模式' : '日间模式' }}</span>
|
||||
</n-popover>
|
||||
|
||||
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="mr-16 f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover" @click="toggle">
|
||||
<IconMinimize v-if="isFullscreen" />
|
||||
<IconMaximize v-else />
|
||||
</div>
|
||||
</template>
|
||||
<span>{{ isFullscreen ? '退出全屏' : '全屏模式' }}</span>
|
||||
</n-popover>
|
||||
|
||||
<UserAvatar />
|
||||
</div>
|
||||
</CommonAppCard>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import AppTab from '@/layouts/components/tab/index'
|
||||
import SideBar from './sidebar/index.vue'
|
||||
import AppHeader from './header/index.vue'
|
||||
import {useAppStore} from "~/stores/admin/app";
|
||||
|
||||
const appStore = useAppStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wh-full flex">
|
||||
<aside
|
||||
class="flex-col flex-shrink-0 transition-width-300"
|
||||
:class="appStore.collapsed ? 'w-64' : 'w-220'"
|
||||
border-r="1px solid light_border dark:dark_border"
|
||||
>
|
||||
<SideBar />
|
||||
</aside>
|
||||
|
||||
<article class="w-0 flex-col flex-1">
|
||||
<AppHeader class="h-60 flex-shrink-0" />
|
||||
<div class="p-12" border-b="1px solid light_border dark:dark_border">
|
||||
<AppTab class="flex-shrink-0" />
|
||||
</div>
|
||||
<slot />
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SideLogo border-b="1px solid light_border dark:dark_border" />
|
||||
<SideMenu class="cus-scroll-y mt-4 h-0 flex-1" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user