Files
blog/src/views/Home.vue

690 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="home-page" :content-style="contentStyle">
<n-layout has-sider>
<n-layout-content class="main-content">
<div ref="searchRef" class="pt-8 px-12 relative hidden lg:block">
<n-input-group class="shadow ">
<n-select class="w-24" size="large" v-model:value="broswer" :options="options"
@click="cancelSbox"></n-select>
<n-input class="flex-1" size="large" autofocus v-model:value="searchWord" @blur="cancelSbox"
placeholder="请输入">
<template #suffix>
<n-icon size="large">
<icon-search />
</n-icon>
</template>
</n-input>
</n-input-group>
<div v-if="searchBox"
class="absolute left-34 mt-2 z-10 bg-white text-sm text-gray-500 rounded-md shadow-md px-4 py-2 max-w-80">
<div class="flex p-2 pr-20 truncate rounded-md items-center hover:text-primary cursor-pointer"
:class="selecedIdx === idx ? 'text-white bg-primary' : ''" v-for="(i, idx) in searchItems" :key="idx"
@click="goExtra(i.menu_link)" @keyup.enter="goExtra(i.menu_link)"><span v-if="idx">导航</span>
{{ i.menu_name }}</div>
</div>
</div>
<!-- 标签组 -->
<!-- <PerfectScrollbar class="w-full overflow-x-auto"> -->
<n-scrollbar class="w-full overflow-x-auto" :style="navStyle" trigger="none">
<div v-if="navlist.length" class="flex gap-6 px-2 mt-6 mb-3 flex-wrap lg:px-12">
<d-tag class="cursor-pointer truncate" hideBeyondTags v-for="tag in tagList" :checked="tag.checked"
:color="tag.color" @click="handdleTagClick(tag)">{{ tag.name }}</d-tag>
</div>
<!-- </PerfectScrollbar> -->
<!-- 图片网格展示区域 -->
<div ref="navcards"
class="navcard grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-6 gap-5 pt-3 pb-6 px-2 lg:px-12">
<n-card embedded
class="bg-[#f1fff180] h-24 shadow-md transition-transform duration-300 box-border hover:-translate-y-1.5"
v-for="(item, index) in navlist" :key="index" @click="goExtra(item.menu_link)"
@contextmenu.prevent="handdleContextMenu($event, item)">
<div class="mt-1 w-full flex flex-col items-center cursor-pointer hover:!text-primary">
<div :style="{ background: item.color }"
class="w-8 h-8 rounded-full text-white flex items-center justify-center" v-if="item.icon_error">
{{ item.first }}</div>
<img class="grid-image w-8 h-8 rounded-full" v-else :src="item.menu_icon"
@error="imgErr(index as number)" />
<div class="mt-2 w-full text-center text-lg truncate">{{ item.menu_name || "" }}</div>
<em class="absolute rounded-md top-0 left-0 px-2 text-white text-center text-sm"
:style="{ background: getItemColor(item) }">{{ item.tag }}</em>
</div>
</n-card>
<n-card embedded
class="bg-[#ffffff80] h-24 shadow-md transition-transform duration-300 box-border hover:-translate-y-1.5">
<div @click="addNav" class="w-full h-full flex flex-col items-center justify-center cursor-pointer">
<div :style="{ background: getRandomDarkColor() }"
class="w-8 h-8 rounded-full text-2xl text-white flex items-center justify-center">
+
</div>
<div class="mt-2 w-full text-center text-lg truncate text-primary">新增导航</div>
</div>
</n-card>
</div>
</n-scrollbar>
</n-layout-content>
<n-layout-sider width="30rem">
<homeSide></homeSide>
</n-layout-sider>
</n-layout>
<!-- 新增导航弹窗 -->
<maskX :visible="visible" :setVisible="navCancel">
<n-form class="w-[500px] bg-white rounded-md p-8 shadow-md" ref="formNav" layout="vertical" :data="navData">
<div class="text-center text-lg">新增导航</div>
<n-form-item class="h-8" path="menu_link">
<n-input @blur="getIcon" v-model:value="navData.menu_link" placeholder="请输入单行链接(必填)" />
</n-form-item>
<n-form-item class="h-8 mt-4" path="menu_name">
<n-input v-model:value="navData.menu_name" placeholder="请输入导航名称(必填)" />
</n-form-item>
<n-form-item class="h-8 mt-4" path="tag">
<n-input v-model:value="navData.tag" placeholder="请自定义一个标签(必填,只取前四字)" />
</n-form-item>
<n-form-item class="h-8 mt-4 form-operation-wrap">
<!-- <div class="flex"> -->
<n-input v-model:value="navData.menu_icon" placeholder="请输入图标链接" />
<img class="ml-5" v-if="navData.menu_icon" width="30" height="30" :src="navData.menu_icon" alt="">
<div v-else class="ml-5 w-[30px] h-[30px]"></div>
<!-- </div> -->
</n-form-item>
<div class="mt-14 flex justify-between">
<n-button class="w-[48%]" secondary variant="solid" @click="navCancel">取消</n-button>
<n-button class="w-[48%]" type="primary" variant="solid" @click="navSubmit">确定</n-button>
</div>
</n-form>
</maskX>
<!-- 音乐插件 -->
<aplayer></aplayer>
<!-- 右键菜单 -->
<contextMenu :show="menuShow" :options="MenuOptions">
<div class="!p-0 !m-0 bg-white cursor-pointer !rounded-md shadow-md" @mouseenter="removeTimer"
@mouseleave="hideMenu">
<div :class="menuS" @click="handdleMenuItem(1)">
修改名称
</div>
<div :class="menuS" @click="handdleMenuItem(2)">
修改链接
</div>
<div :class="menuS" @click="handdleMenuItem(3)">
修改标签
</div>
<div :class="menuS" @click="handdleMenuItem(4)">
删除导航
</div>
</div>
</contextMenu>
<!-- 编辑弹窗 -->
<maskX :visible="editModal" :setVisible="navCancel">
<div class="w-[500px] bg-white p-8 rounded-md">
<div class="text-center text-lg mb-8">修改导航内容</div>
<div class="mb-4">
<span class="text-primary" v-if="currentClickedItem === 1">导航名称{{ currentItem?.menu_name }}</span>
<span class="text-primary" v-if="currentClickedItem === 2">导航链接{{ currentItem?.menu_link }}</span>
<span class="text-primary" v-if="currentClickedItem === 3">导航标签{{ currentItem?.tag }}</span>
</div>
<n-input v-model:value="editInput" placeholder="请输入修改内容"></n-input>
<div class="mt-8 flex justify-between">
<n-button class="w-[48%]" secondary @click="handdleItemCancel">取消</n-button>
<n-button class="w-[48%]" type="primary" @click="handdleItemSubmit">确定</n-button>
</div>
</div>
</maskX>
</div>
</template>
<script setup lang="ts">
import contextMenu from '@/components/contextMenu.vue';
import maskX from '@/components/mask.vue';
import { deepclone, getDictValue } from '@/util/index.ts';
definePage({
name: 'home',
meta: {
title: '首页',
}
})
// 定义接口类型
interface NavItem {
nid?: number
menu_link: string
menu_name: string
tag: string
menu_icon: string
icon_error?: boolean
first?: string
color?: string
}
interface MenuItem {
menu_name: string
menu_link: string
tag: string
}
interface TagItem {
name: string
color: string
checked: boolean
}
// 右键菜单
const menuShow = ref(false)
const MenuOptions = reactive({ x: 0, y: 0 })
const currentItem = ref<NavItem | null>(null)
const menuS = '!m-0 py-3 px-6 text-sm text-gray-700 w-full flex items-center justify-center hover:bg-[#f5f0f0] hover:text-primary'
const currentClickedItem = ref<number>(0)
const editModal = ref<boolean>(false)
const editInput = ref<string>('')
// 新增导航弹窗
const visible = ref<boolean>(false)
const navData = reactive<NavItem>({
menu_link: '',
menu_name: '',
tag: '',
menu_icon: ''
})
const formNav = ref<any>(null)
const navcards = ref<any>(null)
const searchRef = useTemplateRef("searchRef")
// 首页逻辑
const nav: any = $store.nav.useNavStore()
const contentStyle: any = ref({})
const navStyle: any = ref({})
const searchWord: any = ref('')
const broswer: any = ref('bing')
const searchItems: any = ref([])
const selecedIdx = ref(0)
const searchBox = ref(false)
const options = ref([
{
label: '必应',
value: 'bing',
url: 'https://cn.bing.com/search?q='
},
{
label: '百度',
value: 'baidu',
url: 'https://www.baidu.com/s?wd='
},
{
label: '谷歌',
value: 'google',
url: 'https://www.google.com/search?q='
},
{
label: '翻译',
value: 'trans',
url: 'https://translate.volcengine.com?text='
},
])
const navlist = ref<NavItem[]>([])
let cloneNavlist: NavItem[] = []
const tagList = ref<TagItem[]>([
{
name: '全部',
color: '',
checked: true
}
])
const usrLog = $store.log.useLogStore()
let timer: any = null
// 输入搜索内容时监听searchWord在navlist中模糊搜索
watch(searchWord, () => {
handleInput()
})
function cancelSbox() {
setTimeout(() => {
searchBox.value = false
searchItems.value = []
selecedIdx.value = 0
}, 200)
}
function handleInput() {
if (!searchWord.value) {
searchItems.value = []
searchBox.value = false
selecedIdx.value = 0
return
}
searchBox.value = true
selecedIdx.value = 0
const keyword = searchWord.value.toLowerCase()
searchItems.value = navlist.value.filter((item) =>
item.menu_name.toLowerCase().includes(keyword) ||
item.menu_link.toLowerCase().includes(keyword) ||
item.tag.toLowerCase().includes(keyword)
)
// 在searchItems第一个位置插入一条原本搜索
searchItems.value.unshift({
menu_name: `${getDictValue(options.value, "value", broswer.value, "label")}中搜索"${searchWord.value}"`,
menu_link: getDictValue(options.value, "value", broswer.value, "url") + searchWord.value,
tag: ''
})
}
function handleKeyup(e: KeyboardEvent) {
if (!searchBox.value) return
// 向下箭头
if (e.key == "ArrowDown") {
if (selecedIdx.value < searchItems.value.length - 1) {
selecedIdx.value += 1
} else {
selecedIdx.value = 0
}
}
// 向上箭头
else if (e.key == "ArrowUp") {
if (selecedIdx.value > 0) {
selecedIdx.value -= 1
} else {
selecedIdx.value = searchItems.value.length - 1
}
}
// 回车键
else if (e.key == "Enter") {
if (searchItems.value.length > 0) {
const selectedItem = searchItems.value[selecedIdx.value]
window.open(selectedItem.menu_link, "_BLANK")
searchBox.value = false
selecedIdx.value = 0
}
}
}
// 2秒后自动隐藏菜单
const hideMenu = () => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
menuShow.value = false
}, 2000)
}
const removeTimer = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
function handleMenuItem(type: number) {
menuShow.value = false
currentClickedItem.value = type
if (type === 4) {
// 删除导航
if (!currentItem.value) return
$modal({
title: '删除导航',
content: `确定要删除【${currentItem.value.menu_name}】吗?删除后不可恢复哦~`,
cancelText: '取消',
submitText: '删除',
handdleSubmit: async () => {
const res = await $http.nav.deleteNav(currentItem.value?.nid)
if (res.code === 200) {
$msg.success('删除成功')
getNavList()
}
}
})
return
}
editModal.value = true
}
function handleItemCancel() {
editModal.value = false
editInput.value = ''
}
async function handleItemSubmit() {
if (!editInput.value) {
$msg.error('请输入修改内容')
return
}
let updateData: Partial<NavItem> = {}
if (currentClickedItem.value === 1) {
updateData.menu_name = editInput.value
} else if (currentClickedItem.value === 2) {
updateData.menu_link = editInput.value
} else if (currentClickedItem.value === 3) {
updateData.tag = editInput.value.slice(0, 4)
}
const res = await $http.nav.editNav(currentItem.value!.nid!, updateData)
if (res.code === 200) {
$msg.success('修改成功')
editModal.value = false
editInput.value = ''
getNavList()
}
}
async function getNavList() {
const userinfo = $cookies.get("userinfo")
if (!userinfo) {
navlist.value = []
return
}
const res = await $http.nav.getNavList()
if (!res.data) return
// 合并两个循环,提高性能
res.data.forEach((i: any) => {
i.icon_error = false
i.first = i.menu_name?.at(0) || ''
i.color = getRandomDarkColor()
// 同时处理标签列表
if (!tagList.value.find((t) => t.name === i.tag)) {
tagList.value.push({
name: i.tag.substring(0, 4),
color: getRandomDarkColor(30, 128),
checked: false
})
}
});
navlist.value = res.data
cloneNavlist = deepclone(res.data)
}
function getItemColor(item: NavItem) {
const tag = tagList.value.find((t) => t.name === item.tag)
return tag ? tag.color : getRandomDarkColor(30, 128)
}
function handdleTagClick(tag: any) {
tag.checked = !tag.checked
if (tag.name == '全部') {
// 全部标签被点击,重置其他标签状态
tagList.value.forEach((t: any) => {
if (t.name !== '全部') {
t.checked = false
}
})
// navlist.value = cloneNavlist
navlist.value = cloneNavlist
return
} else {
// 其他标签被点击,取消“全部”标签状态
const allTag = tagList.value.find((t: any) => t.name == '全部')
if (allTag) allTag.checked = false
}
const selectedTags = tagList.value.filter((t: any) => t.checked && t.name !== '全部').map((t: any) => t.name)
if (selectedTags.length === 0) {
// 如果没有选中任何标签,默认选中“全部”
const allTag = tagList.value.find((t: any) => t.name == '全部')
if (allTag) allTag.checked = true
// 数据从cloneNavlist中过滤
navlist.value = cloneNavlist
return
}
console.log('>>> --> handdleTagClick --> selectedTags:', selectedTags)
// 根据选中的标签过滤导航列表
navlist.value = cloneNavlist.filter((item: any) => selectedTags.includes(item.tag))
}
function imgErr(index: number) {
navlist.value[index].icon_error = true
}
function goExtra(link: string) {
window.open(link, "_BLANK")
}
function search() {
const res = getDictValue(options.value, "value", broswer.value, "url")
if (res) {
window.open(res + searchWord.value, "_BLANK")
}
}
const getRandomDarkColor = (min: number = 30, max: number = 128): string => {
// 确保最大值不超过255且最小值不小于0
min = Math.max(0, min);
max = Math.min(255, max);
// 确保最大值大于最小值
if (max <= min) {
max = min + 30;
}
// 生成随机RGB值 (深色)
const r = Math.floor(Math.random() * (max - min + 1)) + min;
const g = Math.floor(Math.random() * (max - min + 1)) + min;
const b = Math.floor(Math.random() * (max - min + 1)) + min;
// 转换为十六进制
const toHex = (c: number) => {
const hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function addNav() {
if (!$cookies.get("token")) {
$msg.error("请先登录");
return
}
visible.value = true
}
async function getIcon() {
console.log(11111);
if (!navData.menu_link) return
const res = await $http.mix.getIcon({
url: navData.menu_link
})
console.log('>>> --> getIcon --> res:', res)
if (res.code == 200) {
navData.menu_icon = res.data.url
}
}
function resetNav() {
navData.menu_link = ''
navData.menu_name = ''
navData.tag = ''
navData.menu_icon = ''
}
function navCancel() {
visible.value = false
resetNav()
}
async function navSubmit() {
console.log('>>> --> navSubmit --> navData:', navData)
if (!navData.menu_link || !navData.menu_name || !navData.tag) {
$msg.error('请输入链接、标签和名称')
return
}
navData.tag = navData.tag.slice(0, 4)
const res = await $http.nav.addNav(navData)
console.log('>>> --> navSubmit --> res:', res)
if (res.code == 200) {
$msg.success('添加成功')
visible.value = false
getNavList()
}
// 清空navData
resetNav()
}
// 处理右键菜单
function handdleContextMenu(event: MouseEvent, item: any) {
menuShow.value = true; // 先隐藏菜单以重置位置
MenuOptions.x = event.clientX;
MenuOptions.y = event.clientY;
currentItem.value = item; // 存储当前右键点击的项
}
// 监听store的登录状态
watch(() => usrLog.isLogin, (newVal: any) => {
console.log('********** --> watch --> newVal:', newVal)
if (newVal) {
getNavList()
} else {
navlist.value = []
}
})
function handdleKeyup(e: any) {
if (!searchBox.value) return
// console.log('>>> --> haddleDown --> idx:', e.keyCode)
// 向下箭头
if (e.keyCode == 40) {
if (selecedIdx.value < searchItems.value.length - 1) {
selecedIdx.value += 1
} else {
selecedIdx.value = 0
}
}
// 向上箭头
else if (e.keyCode == 38) {
if (selecedIdx.value > 0) {
selecedIdx.value -= 1
} else {
selecedIdx.value = searchItems.value.length - 1
}
}
// 回车键
else if (e.keyCode == 13) {
if (searchItems.value.length > 0) {
const selectedItem = searchItems.value[selecedIdx.value]
window.open(selectedItem.menu_link, "_BLANK")
searchBox.value = false
selecedIdx.value = 0
}
}
}
function handdleItemCancel() {
editModal.value = false
editInput.value = ''
}
async function handdleItemSubmit() {
if (!editInput.value) {
$msg.error('请输入修改内容')
return
}
let updateData: any = {}
if (currentClickedItem.value == 1) {
updateData.menu_name = editInput.value
} else if (currentClickedItem.value == 2) {
updateData.menu_link = editInput.value
} else if (currentClickedItem.value == 3) {
updateData.tag = editInput.value.slice(0, 4)
}
const res = await $http.nav.editNav(currentItem.value?.nid, updateData)
console.log('>>> --> handdleItemSubmit --> res:', res)
if (res.code == 200) {
$msg.success('修改成功')
editModal.value = false
editInput.value = ''
getNavList()
}
}
function handdleMenuItem(type: number) {
menuShow.value = false
currentClickedItem.value = type
if (type == 4) {
// 删除导航
if (!currentItem.value) return
$modal({
title: '删除导航',
content: `确定要删除【${currentItem.value.menu_name}】吗?删除后不可恢复哦~`,
cancelText: '取消',
submitText: '删除',
handdleSubmit: async () => {
const res = await $http.nav.deleteNav(currentItem.value?.nid)
console.log('>>> --> handdleMenuItem --> res:', res)
if (res.code == 200) {
$msg.success('删除成功')
getNavList()
}
}
})
return
}
editModal.value = true
}
const navHeight: any = ref(0)
onMounted(() => {
contentStyle.value = {
height: `${window.innerHeight - nav.navH}px)`
}
console.log('>>> --> onMounted --> navcards.value.getBoundingClientRect().y:', navcards.value.getBoundingClientRect().y)
navHeight.value = window.innerHeight - navcards.value.getBoundingClientRect().y
navStyle.value = {
// height: `calc(100vh - ${navcards.value.getBoundingClientRect().y}px - ${nav.navH}px)`,
height: `${window.innerHeight - navcards.value.getBoundingClientRect().y}px`
}
tagList.value = [
{
name: '全部',
color: getRandomDarkColor(30, 128),
checked: true
}
]
getNavList()
window.addEventListener('resize', () => {
// 获取屏幕高度
const screenHeight = window.innerHeight;
contentStyle.value = {
height: `${screenHeight - nav.navH}px)`
}
// searchRef的y
const searchHeight: number = searchRef.value?.getBoundingClientRect().y || 0
// searchRef的高度
const searchH: number = searchRef.value?.getBoundingClientRect().height || 0
navStyle.value = {
height: `${window.innerHeight - (searchHeight + searchH)}px`
}
});
// 处理键盘按键抬起事件,触发搜索框的键盘导航逻辑
window.addEventListener('keyup', (e: Event) => {
handdleKeyup(e)
});
})
</script>
<style scoped lang="less">
/* layer: default */
.navcard {
display: grid;
}
:deep(.devui-input-slot__prepend) {
width: 90px;
}
</style>