迁移UI框架至naive-ui,重构组件和样式,添加Gallery和Mask组件

This commit is contained in:
2025-12-28 16:45:58 +08:00
parent 4c097e4c40
commit 96edada2ff
54 changed files with 2482 additions and 2979 deletions

4
auto-imports.d.ts vendored
View File

@ -68,9 +68,13 @@ declare global {
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useDialog: typeof import('naive-ui')['useDialog']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useModel: typeof import('vue')['useModel']
const useNotification: typeof import('naive-ui')['useNotification']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']

51
components.d.ts vendored
View File

@ -10,35 +10,36 @@ declare module 'vue' {
export interface GlobalComponents {
Aplayer: typeof import('./src/components/aplayer.vue')['default']
ContextMenu: typeof import('./src/components/contextMenu.vue')['default']
DAside: typeof import('vue-devui/layout/index.es.js')['Aside']
DAvatar: typeof import('vue-devui/avatar/index.es.js')['Avatar']
DButton: typeof import('vue-devui/button/index.es.js')['Button']
DCard: typeof import('vue-devui/card/index.es.js')['Card']
DContent: typeof import('vue-devui/layout/index.es.js')['Content']
DDropdown: typeof import('vue-devui/dropdown/index.es.js')['Dropdown']
DFooter: typeof import('vue-devui/layout/index.es.js')['Footer']
DForm: typeof import('vue-devui/form/index.es.js')['Form']
DFormItem: typeof import('vue-devui/form/index.es.js')['FormItem']
DHeader: typeof import('vue-devui/layout/index.es.js')['Header']
DIcon: typeof import('vue-devui/icon/index.es.js')['Icon']
DInput: typeof import('vue-devui/input/index.es.js')['Input']
DLayout: typeof import('vue-devui/layout/index.es.js')['Layout']
DMenu: typeof import('vue-devui/menu/index.es.js')['Menu']
DMenuItem: typeof import('vue-devui/menu/index.es.js')['MenuItem']
DModal: typeof import('vue-devui/modal/index.es.js')['Modal']
DPopover: typeof import('vue-devui/popover/index.es.js')['Popover']
DSearch: typeof import('vue-devui/search/index.es.js')['Search']
DSelect: typeof import('vue-devui/select/index.es.js')['Select']
DTab: typeof import('vue-devui/tabs/index.es.js')['Tab']
DTabs: typeof import('vue-devui/tabs/index.es.js')['Tabs']
DTag: typeof import('vue-devui/tag/index.es.js')['Tag']
Gallery: typeof import('./src/components/Gallery.vue')['default']
HomeSide: typeof import('./src/components/homeSide.vue')['default']
Login: typeof import('./src/components/Login.vue')['default']
Mask: typeof import('./src/components/mask.vue')['default']
MenuH: typeof import('./src/components/menuH.vue')['default']
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDropdown: typeof import('naive-ui')['NDropdown']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface GlobalDirectives {
vImagePreview: typeof import('vue-devui/image-preview/index.es.js')['ImagePreviewDirective']
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.8 MiB

4
dist/index.html vendored
View File

@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="这是柚子的网站,做一些分享类的功能"/>
<title>柚子の网站</title>
<script type="module" crossorigin src="/blog/assets/index-CIE_IpA5.js"></script>
<link rel="stylesheet" crossorigin href="/blog/assets/index-DYCSKHr7.css">
<script type="module" crossorigin src="/blog/assets/index-C1l6Y9EA.js"></script>
<link rel="stylesheet" crossorigin href="/blog/assets/index-Cpzn2D0V.css">
</head>
<body>

3
extra.d.ts vendored
View File

@ -1,2 +1,3 @@
declare module "aplayer";
declare module 'vue3-video-play';
declare module "vue3-video-play";
declare module "vue3-masonry-plus";

View File

@ -14,29 +14,28 @@
"type-check": "vue-tsc --build"
},
"dependencies": {
"@devui-design/icons": "^1.4.0",
"@imengyu/vue3-context-menu": "^1.5.2",
"@meting/core": "^1.5.13",
"@unocss/reset": "^66.3.3",
"aplayer": "^1.10.1",
"axios": "^1.11.0",
"crypto-js": "^4.2.0",
"devui-theme": "^0.0.7",
"es-toolkit": "^1.39.8",
"less": "^4.4.0",
"ng-devui": "^18.0.0",
"pinia": "^3.0.3",
"qweather-icons": "^1.7.0",
"unocss": "^66.3.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"unplugin-vue-router": "^0.15.0",
"v-infinite-scroll": "^1.0.4",
"vditor": "^3.11.2",
"vite-svg-loader": "^5.1.0",
"vue": "^3.6.0-alpha.2",
"vue-devui": "^1.6.33",
"vue-masonry": "^0.16.0",
"vue-router": "^4.5.1",
"vue3-cookies": "^1.0.6",
"vue3-masonry-plus": "^1.2.5",
"vue3-perfect-scrollbar": "^2.0.0",
"vue3-video-play": "^1.3.2"
},
@ -45,10 +44,13 @@
"@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.7.0",
"naive-ui": "^2.43.2",
"npm-run-all2": "^8.0.4",
"typescript": "~5.8.0",
"vfonts": "^0.0.3",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-devui": "^1.6.35",
"vue-tsc": "^3.0.4"
}
}

View File

@ -1,71 +1,29 @@
<template>
<div>
<PerfectScrollbar>
<n-config-provider :theme-overrides="themeOverrides">
<n-message-provider>
<n-dialog-provider>
<n-modal-provider>
<n-scrollbar style="max-height: 100vh">
<lay-index></lay-index>
</PerfectScrollbar>
</div>
<d-modal class="!w-80" v-model="modal.visible" :title="modal.title">
{{ modal.content }}
<div class="mt-4 w-full flex justify-between">
<!-- <d-button @click="modal.handdleCancel" variant="text"
class="w-[49%] hover:bg-[#8a6684] hover:!text-white">{{modal.cancelText}}</d-button>
<span class="text-[20px]"> | </span>
<d-button @click="modal.handdleSubmit" variant="text" class="w-[49%] hover:bg-[#5c866a] hover:!text-white"
color="primary">{{modal.submitText}}</d-button> -->
<d-button class="w-[48%]" variant="solid" color="secondary" @click="modal.handdleCancel">{{modal.cancelText}}</d-button>
<d-button class="w-[48%]" variant="solid" color="primary" @click="modal.handdleSubmit">{{modal.submitText}}</d-button>
</div>
</d-modal>
</n-scrollbar>
</n-modal-provider>
</n-dialog-provider>
</n-message-provider>
</n-config-provider>
</template>
<script setup lang="ts">
import layIndex from './Index.vue'
import themeOverrides from '@/util/theme.ts';
import layIndex from './Index.vue';
// 空白项目入口
const route = useRoute()
const logStatus = $store.log.useLogStore()
const modal = reactive<any>({
visible: false,
title: "",
content: "",
handdleCancel: () => { modal.visible = false },
handdleSubmit: () => { },
})
function comemodal(mv: any) {
if (!mv) return
modal.visible = true
modal.title = mv.title
modal.content = mv.content
modal.cancelText = mv.cancelText || "取消"
modal.submitText = mv.submitText || "确定"
if (mv.handdleCancel) modal.handdleCancel = () => {
mv.handdleCancel()
modal.visible = false
}
modal.handdleSubmit = () => {
mv.handdleSubmit()
modal.visible = false
}
}
window.$modal = comemodal
// 全局禁用右键菜单
document.oncontextmenu = function () {
return false;
};
onMounted(() => {
if($cookies.get('userinfo')) logStatus.setIsLogin(true)
if ($cookies.get('userinfo')) logStatus.setIsLogin(true)
})
</script>
<style scoped lang="less">
.ps {
width: 100vw;
height: 100vh;
padding-inline-start: 0;
}
</style>
<style scoped lang="less"></style>

View File

@ -1,38 +1,112 @@
<template>
<d-layout>
<d-header class="dheader-1">
<n-layout>
<n-layout-header class="dheader-1">
<menu-h />
</d-header>
<d-content class="dcontent-1">
</n-layout-header>
<n-layout-content class="dcontent-1">
<router-view />
</d-content>
<d-footer v-show="route.path == '/home'" class="dfooter-1">
</n-layout-content>
<n-layout-footer v-show="route.path == '/home'" class="flex justify-center dfooter-1">
<div class="beian" ref="footer">
<a class="text-primary" href="https://beian.miit.gov.cn" target="_blank">皖ICP备2021017362号-1</a>
<a class="swag text-primary" target="_blank" href="https://www.hxyouzi.com/swag">api文档</a>
</div>
</d-footer>
</d-layout>
</n-layout-footer>
</n-layout>
<n-modal v-model:show="modal.visible" preset="dialog" title="Dialog">
<template #header>
<div>{{ modal.title }}</div>
</template>
<div>
<div class="my-2">{{ modal.content }}</div>
<div v-if="modal.contType == 'input'">
<n-input @change="modal.handdleInputChange($event)" :placeholder="modal.placeholder"></n-input>
</div>
</div>
<template #action>
<n-button @click="modal.handdleCancel">{{ modal.cancelText }}</n-button>
<n-button type="primary" @click="modal.handdleSubmit">{{ modal.submitText }}</n-button>
</template>
</n-modal>
</template>
<script setup lang="ts">
const route = useRoute()
const footer = ref<HTMLElement | null>(null)
window.$msg = useMessage()
const modal: NmodalItem = reactive({
visible: false,
title: '',
content: '',
contType: 'text',
cancelText: '取消',
submitText: '确定',
placeholder: '请输入内容',
handdleCancel: () => { },
handdleSubmit: () => { },
handdleInputChange: (e: any) => { }
})
interface NmodalItem {
visible: boolean
title: string
content: string
contType?: string
placeholder?: string
cancelText?: string
submitText?: string
handdleCancel?: () => void
handdleSubmit?: () => void
handdleInputChange: (e: any) => void
}
//mark method
function comemodal(mv: NmodalItem) {
if (!mv) return
modal.visible = true
modal.title = mv.title
modal.content = mv.content
modal.contType = mv.contType || "text"
modal.placeholder = mv.placeholder || "请输入内容"
modal.cancelText = mv.cancelText || "取消"
modal.submitText = mv.submitText || "确定"
modal.handdleCancel = (): void => {
if (mv.handdleCancel) {
mv.handdleCancel()
}
modal.visible = false
}
modal.handdleSubmit = () => {
if (mv.handdleSubmit) {
mv.handdleSubmit()
}
modal.visible = false
}
modal.handdleInputChange = (e) => {
if (mv.handdleInputChange) {
mv.handdleInputChange(e)
}
}
}
window.$modal = comemodal
onMounted(() => {
// 计算footer的高度
const footerHeight = footer.value?.clientHeight || 0;
console.log('footerHeight', footerHeight);
})
</script>
<style scoped>
<style scoped lang="less">
.dcontent-1 {
background-color: #fbfbfb;
}
.beian {
width: 100%;
padding: 20px 0;
display: flex;
justify-content: center;

View File

@ -1,6 +1,3 @@
@import "./base.less";
@import "@devui-design/icons/icomoon/devui-icon.css";
@import "qweather-icons/font/qweather-icons.css";
@import "@devui-design/icons/icomoon/devui-icon.css";
@import "vue-devui/style.css";
@import "vue3-perfect-scrollbar/style.css";

281
src/components/Gallery.vue Normal file
View File

@ -0,0 +1,281 @@
<template>
<PerfectScrollbar ref="scrollbar" @ps-scroll-y="handleScroll">
<div ref="myCon" class="gallery-page py-5 px-[10%]">
<d-search class="mt-0 mb-8 w-2/3 mx-auto rounded-full" v-model="kw" is-keyup-search :delay="1000"
@search="onSearch"></d-search>
<div class="gallery-container w-full box-border">
<!-- 瀑布流容器 -->
<div ref="waterfallContainer" v-image-preview
class="waterfall-container flex justify-between flex-nowrap w-full overflow-hidden">
<!-- 动态生成的列 -->
<div v-for="(column, index) in columns" :key="index" class="waterfall-column flex flex-col w-[240px]">
<div v-for="item in column" :key="item.id"
class="gallery-item group relative my-[10px] rounded-lg overflow-hidden transition-transform duration-300 box-border hover:-translate-y-1.5">
<div
class="absolute px-2 truncate hidden group-hover:block top-0 text-center w-full bg-[#00000070] text-white">
{{ item.filename }}</div>
<img :src="item.filepath" alt="" class="gallery-image block w-full h-auto object-cover rounded-md">
<div class="px-2 absolute bottom-0 flex justify-between w-full bg-[#00000060]">
<div class="text-white "> <span class="text-[#f1d9db] font-600">{{ item.nickname }}</span> 上传</div>
<d-popover content="下载" trigger="hover" class="!bg-primary" style="color: #fff">
<icon-download @click="downloadFile(item.filepath)"
class="w-5 h-5 text-white hover-text-primary"></icon-download>
</d-popover>
</div>
</div>
</div>
</div>
<!-- 加载中指示器 -->
<div v-if="loading" class="loading-indicator text-center p-5 text-gray-600">加载中...</div>
<div v-else class="loading-indicator text-center p-5 text-gray-600">已全部加载完成</div>
</div>
</div>
</PerfectScrollbar>
</template>
<script setup lang="ts">
import { throttle } from 'es-toolkit';
definePage({
name: 'gallery',
meta: {
title: '画廊',
}
})
// 画廊页逻辑
const fileList = ref<any[]>([]);
const pn = ref(1);
const ps = ref(40);
const loading = ref(false);
const waterfallContainer = ref<HTMLDivElement | null>(null);
const columns = ref<Array<Array<any>>>([]);
const columnHeights = ref<number[]>([]);
const columnCount = ref(4); // 默认列数
const itemWidth = ref(240); // 图片宽度固定为240px
const kw = ref<string>('');
// const uploadOptions = ref({
// uri: 'https://www.hxyouzi.com/api/files/upload',
// method: 'POST',
// maximumSize: 5 * 1024 * 1024,
// headers: {
// 'Authorization': 'Bearer ' + $cookies.get('token'),
// },
// });
// 计算列数 based on 屏幕宽度
function calculateColumnCount() {
if (!waterfallContainer.value) return;
const containerWidth = waterfallContainer.value.clientWidth;
const newColumnCount = Math.max(1, Math.floor(containerWidth / itemWidth.value));
if (newColumnCount !== columnCount.value) {
columnCount.value = newColumnCount;
resetWaterfall();
}
}
// 重置瀑布流
function resetWaterfall() {
columns.value = Array(columnCount.value).fill(0).map(() => []);
columnHeights.value = Array(columnCount.value).fill(0);
// 重新分配图片
fileList.value.forEach(async (item:any) =>await addToWaterfall(item));
console.log('>>> --> resetWaterfall --> fileList:', fileList.value)
}
// 添加图片到瀑布流
async function addToWaterfall(item: any) {
if (columns.value.length === 0) return;
const { height, width } =await getImageSizeByCheck(item.filepath)
// 找到高度最小的列
let minHeight = Math.min(...columnHeights.value);
let minIndex = columnHeights.value.indexOf(minHeight);
console.log('>>> --> addToWaterfall --> item:', item)
console.log('>>> --> addToWaterfall --> minIndex:', minIndex)
// 添加到该列
columns.value[minIndex].push(item);
// 估算列高 - 实际高度会在图片加载后更新
const estimatedHeight = itemWidth.value * height / width
columnHeights.value[minIndex] += estimatedHeight + 20; // 加上padding和margin
}
function getImageSizeByCheck(url: string): any {
return new Promise(function (resolve, reject) {
let image = new Image();
image.src = url;
let height = 0
let width = 0
// let timer = setTimeout(() => {
image.onload = () => {
if (image.width > 0 && image.height > 0) {
height = image.height
width = image.width
resolve({ height, width })
// clearTimeout(timer)
}
}
// }, 100)
});
}
// 获取文件列表
async function getFileList() {
if (loading.value) return;
loading.value = true;
try {
const res = await $http.file.getFileList({
keyword: kw.value,
page_num: pn.value,
page_size: ps.value,
});
// console.log('>>> --> getFileList --> res:', res);
if (pn.value === 1) {
fileList.value = res.data;
resetWaterfall();
} else {
// 追加新数据
res.data.forEach((item: any) => {
fileList.value.push(item);
addToWaterfall(item);
});
}
} catch (error) {
console.error('获取文件列表失败:', error);
} finally {
loading.value = false;
}
}
// // 获取我的文件列表
// async function getMyList() {
// if (loading.value) return;
// loading.value = true;
// try {
// const res = await $http.file.getMyList({
// page_num: pn.value,
// page_size: ps.value,
// });
// // console.log('>>> --> getFileList --> res:', res);
// if (pn.value === 1) {
// fileList.value = res.data;
// resetWaterfall();
// } else {
// // 追加新数据
// res.data.forEach((item: any) => {
// fileList.value.push(item);
// addToWaterfall(item);
// });
// }
// } catch (error) {
// console.error('获取文件列表失败:', error);
// } finally {
// loading.value = false;
// }
// }
// 监听滚动事件
const handleScroll: any = throttle((e: any) => {
console.log('>>> --> handleScroll --> loading:', e)
if (loading.value) return;
const scrollTop = e.target.scrollTop
console.log('>>> --> handleScroll --> scrollTop:', scrollTop)
const scrollHeight = e.target.scrollHeight
// console.log('>>> --> handleScroll --> scrollHeight:', scrollHeight)
const clientHeight = e.target.offsetHeight;
// 当滚动到距离底部20%时加载更多
// console.log('>>> --> clientHeight --> clientHeight:', clientHeight)
if (scrollTop + clientHeight >= scrollHeight * 0.8) {
console.log('>>> --> handleScroll --> 加载更多')
pn.value++;
getFileList();
}
}, 1000)
function onSearch() {
pn.value = 1;
getFileList();
}
function downloadFile(url: string) {
console.log('>>> --> downloadFile --> url:', url)
// 创建临时a标签
const link = document.createElement('a');
// 设置下载链接
link.href = url;
// 提取文件名
const fileName = url.split('/').pop() || 'downloaded-file';
// 设置下载属性和文件名
link.download = fileName;
// 设置为隐藏元素
link.style.display = 'none';
// 添加到文档
document.body.appendChild(link);
// 触发点击事件
link.click();
// 移除临时元素
document.body.removeChild(link);
}
onMounted(() => {
getFileList();
// 计算初始列数
calculateColumnCount();
// 添加窗口大小变化监听
window.addEventListener('resize', calculateColumnCount);
// 添加滚动监听
window.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
pn.value = 1;
kw.value = '';
fileList.value = [];
resetWaterfall();
// 移除监听
window.removeEventListener('resize', calculateColumnCount);
window.removeEventListener('scroll', handleScroll);
});
</script>
<style scoped lang="less">
/* 画廊页样式 */
:deep(.devui-tabs__nav) {
display: flex;
justify-content: center;
width: 100% !important;
// padding: 0 10%;
li {
width: 50%;
display: flex;
justify-content: center;
}
li a span {
font-size: 18px !important;
font-weight: 500;
}
}
.ps {
height: calc(100vh - 65px);
width: 100%;
}
:deep(.devui-upload) {
width: 100%;
&>div {
width: 100%;
}
}
</style>

View File

@ -1,44 +1,47 @@
<template>
<d-card class="mt-10 bg-[#ffffff60] rounded-[10px]" shadow="never">
<d-tabs v-model="tid" type="pills">
<d-tab id="tab1" title="登录">
<d-form ref="formLogin" layout="vertical" :data="loginData" :rules="rules">
<d-form-item field="username">
<d-input v-model="loginData.username" placeholder="请输入用户名" />
</d-form-item>
<d-form-item field="password">
<d-input v-model="loginData.password" show-password placeholder="请输入密码" />
</d-form-item>
<d-form-item class="form-operation-wrap">
<d-button class="w-full" variant="solid" @click="login"> </d-button>
</d-form-item>
</d-form>
</d-tab>
<d-tab id="tab2" title="注册">
<d-form ref="formReg" layout="vertical" :data="regData" :rules="rrules">
<d-form-item field="username">
<d-input v-model="regData.username" placeholder="请输入用户名" />
</d-form-item>
<d-form-item field="password">
<d-input v-model="regData.password" show-password placeholder="请输入用密码" />
</d-form-item>
<d-form-item field="nickname">
<d-input v-model="regData.nickname" placeholder="请输入昵称" />
</d-form-item>
<d-form-item class="form-operation-wrap">
<d-button class="w-full" variant="solid" @click="register">注册</d-button>
</d-form-item>
</d-form>
</d-tab>
</d-tabs>
</d-card>
<n-card class="mt-10 rounden-[10px] w-[500px]" shadow="never">
<n-tabs v-model:value="tid" justify-content="space-evenly" animated>
<n-tab-pane name="tab1" tab="登录">
<n-form ref="formLogin" layout="vertical" :data="loginData" :rules="rules">
<n-form-item field="username">
<n-input v-model:value="loginData.username" placeholder="请输入用户名" />
</n-form-item>
<n-form-item field="password">
<n-input v-model:value="loginData.password" type="password" show-password-on="click" placeholder="请输入密码" />
</n-form-item>
<n-form-item class="form-operation-wrap">
<n-button class="w-full" type="primary" @click="login"> </n-button>
</n-form-item>
</n-form>
</n-tab-pane>
<n-tab-pane name="tab2" tab="注册">
<n-form ref="formReg" layout="vertical" :data="regData" :rules="rrules">
<n-form-item field="username">
<n-input v-model:value="regData.username" placeholder="请输入用户名" />
</n-form-item>
<n-form-item field="password">
<n-input v-model:value="regData.password" type="password" show-password-on="click" placeholder="请输入用密码" />
</n-form-item>
<n-form-item field="nickname">
<n-input v-model:value="regData.nickname" placeholder="请输入昵称" />
</n-form-item>
<n-form-item class="form-operation-wrap">
<n-button class="w-full" type="primary" @click="register">注册</n-button>
</n-form-item>
</n-form>
</n-tab-pane>
</n-tabs>
</n-card>
</template>
<script setup lang="ts">
const router = useRouter()
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void
}>()
const props = defineProps({
setVisible: {
type: Function,
default: () => { },
}
})
// 登录注册逻辑
const tid = ref("tab1");
const formLogin: any = ref(null);
@ -77,8 +80,8 @@ const rrules: any = reactive({
})
function login() {
console.log('>>> --> login --> formLogin.value:', usrLog.isLogin)
formLogin.value.validate(async (is: boolean, b: any) => {
if (!is) {
formLogin.value?.validate(async (is: boolean) => {
if (is) {
$msg.error('信息填写不正确,请检查后再提交')
} else {
const res = await $http.user.login(loginData)
@ -90,15 +93,14 @@ function login() {
$cookies.set('userinfo', res.data.userinfo, '1d')
$msg.success(res.msg)
usrLog.setIsLogin(true)
// router.push({ path: "/" })
emit("update:visible",false)
props.setVisible(false)
}
})
}
function register() {
formReg.value.validate(async (is: boolean, b: any) => {
if (!is) {
formReg.value.validate(async (is: boolean) => {
if (is) {
$msg.error('信息填写不正确,请检查后再提交')
} else {
const res = await $http.user.register(regData)
@ -108,8 +110,10 @@ function register() {
}
$msg.success(res.msg)
tid.value = 'tab1'
formReg.value.resetForm()
loginData.username = regData.username
regData.username = ''
regData.nickname = ''
regData.password = ''
}
})
}

View File

@ -1,14 +1,14 @@
<template>
<div class="pr-8">
<d-card shadow="never" class="mt-4 bg-white">
<n-card embedded class="mt-4 shadow">
<!-- <div class="dt-card mt-10 bg-white"> -->
<template #title>
<template #header>
<div class="flex items-center">
<icon-time class="w-5 mr-2"></icon-time>
时间日期
</div>
</template>
<template #content>
<template #default>
<div class="w-full pr-20 text-center text-[#ec66ab] font-500 text-4xl font-[yj]">{{ t }}</div>
<div class="mt-3 flex justify-between">
<span>今年已过了{{ jq.dayOfYear }}</span>
@ -18,19 +18,20 @@
<img v-else class="absolute top-0 right-4" width="120" src="@/assets/images/onwork.png" alt="">
</template>
<!-- </div> -->
</d-card>
</n-card>
<d-card shadow="never" class="mt-4 bg-white">
<template #title>
<n-card embedded class="mt-4 shadow">
<template #header>
<div class="flex items-center">
<icon-news class="w-5 mr-2"></icon-news>
百度新闻
</div>
</template>
<template #content>
<template #default>
<div class="py-1" v-for="i in bdNews" :key="i.id">
<a class="devui-link flex justify-between truncate" :href="i.url" target="_blank">
<!-- 淡蓝色 -->
<a class="text-[#526ecc] no-underline hover:text-primary hover:underline flex justify-between truncate" :href="i.url" target="_blank">
<span class="flex items-center">{{ i.index }}. {{ i.title }}
<icon-hot v-show="i.index < 4" class="ml-2 w-4 text-[red] inline-block"></icon-hot>
</span>
@ -39,7 +40,7 @@
</div>
<div class="mt-2 justify-between flex items-center">
<div class="w-2/5 h-px bg-[#ec66ab]"></div>
<a class="devui-link text-[#ec66ab] flex items-center" href="https://www.baidu.com/s?ie=utf-8&wd=百度新闻"
<a class="text-[#526ecc] no-underline text-[#ec66ab] flex items-center" href="https://www.baidu.com/s?ie=utf-8&wd=百度新闻"
target="_blank">
更多
<icon-right class="ml-1 w-4 text-primary inline-block"></icon-right>
@ -47,18 +48,18 @@
<div class="w-2/5 h-px bg-[#ec66ab]"></div>
</div>
</template>
</d-card>
</n-card>
<d-card shadow="never" class="mt-4 bg-white">
<template #title>
<n-card embedded class="mt-4 shadow">
<template #header>
<div class="flex items-center">
<icon-date class="w-5 mr-2"></icon-date>
农历节气
<div class="ml-12 text-[#ec66ab] font-500">{{ jq.yearTips }} {{ jq.lunarCalendar }}</div>
</div>
</template>
<template #content>
<template #default>
<div class="truncate text-sm mx-[5%]">
{{ jq.suit }}
</div>
@ -70,7 +71,7 @@
</div>
<div class="mt-2 text-center">{{ jq.solarTerms }}</div>
</template>
</d-card>
</n-card>
</div>
</template>
@ -132,8 +133,14 @@ onUnmounted(() => {
src: url('@/assets/font/LCDML.woff2');
}
:deep(.n-card > .n-card__content, .n-card > .n-card__footer){
padding: 8px 25px !important;
}
// .dt-card {
// background-image: url('@/assets/images/中秋节中国风边框34.png');
// background-size: 100% 100%;
// padding: 20px 40px;
// }</style>
// }
</style>

35
src/components/mask.vue Normal file
View File

@ -0,0 +1,35 @@
<template>
<Teleport to="body">
<div v-if="visible"
class="fixed top-0 left-0 z-[1000] w-[100vw] h-[100vh] flex items-center justify-center bg-[rgba(0,0,0,0.5)]">
<slot></slot>
<div @click.prevent="handdleClose" class="absolute w-8 h-8 top-4 right-4 rounded-full bg-white flex items-center justify-center text-[#ec66ab] cursor-pointer">X</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
//mark import
//mark data
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
setVisible: {
type: Function,
default: () => { },
}
});
//mark method
function handdleClose() {
props.setVisible(false)
}
//mark 周期、内置函数等
</script>
<style scoped lang="less"></style>

View File

@ -5,36 +5,15 @@
<img :src="logo" alt="柚子的网站" class="h-9 align-middle" />
</div>
<!-- 主导航菜单 -->
<d-menu mode="horizontal" router class="ml-5 h-14 text-[16px] " :default-select-keys="[key]">
<d-menu-item key="home">
<d-icon :component="homeSvg" class="w-5 mr-1"></d-icon>
首页
</d-menu-item>
<d-menu-item key="gallery">
<d-icon :component="picSvg" class="w-5 mr-1"></d-icon>
画廊
</d-menu-item>
<d-menu-item key="article">
<d-icon :component="artiSvg" class="w-5 mr-1 "></d-icon>
文章
</d-menu-item>
<d-menu-item key="widget">
<d-icon :component="settingSvg" class="w-5 mr-1 "></d-icon>
工具
</d-menu-item>
<d-menu-item key="appshare">
<d-icon :component="downSvg" class="w-5 mr-1 "></d-icon>
软件分享
</d-menu-item>
<d-menu-item key="plink">
<d-icon :component="linkSvg" class="w-5 mr-1 "></d-icon>
友链
</d-menu-item>
</d-menu>
<div>
<n-menu :icon-size="12" v-model:value="activeKey" class="" mode="horizontal" :options="menuOptions" />
</div>
<!-- 用户区域 -->
<div class="!text-[#ec66ab] flex items-center" @click="gotoHf">
<span class="flex items-center location-info truncate">
<d-icon color="#ec66ab" class="mr-1" name="location-new"></d-icon>
<n-icon class="mr-1">
<icon-loc class="w-[26px] h-[26px]"></icon-loc>
</n-icon>
{{ locationInfo }}
</span>
<span class="mx-3 text-gray-300">|</span>
@ -44,35 +23,23 @@
</div>
<div class="flex items-center mr-8">
<d-dropdown class="cursor-pointer w-[100px]" v-if="userinfo" trigger="hover">
<n-dropdown class="cursor-pointer w-[100px]" v-if="userinfo" :options="oprOp" @select="handleSelect"
trigger="hover">
<div class="flex items-center">
<d-avatar :img-src="userinfo.ava_url" class="cursor-pointer" alt="用户的头" />
<n-avatar round :src="userinfo.ava_url" class="cursor-pointer" alt="用户的头" />
<div class="cursor-pointer ml-2 text-gray text-sm">{{ userinfo.nickname }}</div>
</div>
<template #menu>
<ul class="list-menu">
<!-- hover为淡粉色 -->
<li class="w-full p-2 text-center hover:text-primary hover:bg-[#f5f0f0] cursor-pointer" @click="logout">
登出
</li>
<li class="w-full p-2 text-center hover:text-primary hover:bg-[#f5f0f0] cursor-pointer" @click="">
控制台
</li>
<li class="w-full p-2 text-center hover:text-primary hover:bg-[#f5f0f0] cursor-pointer" @click="">
设置
</li>
</ul>
</template>
</d-dropdown>
<div v-else class="flex items-center">
<d-avatar class="cursor-pointer" @click="toLogin"></d-avatar>
<div class="cursor-pointer ml-2 text-gray text-sm" @click="toLogin">登录</div>
</n-dropdown>
<div v-else class="flex items-center" @click="toLogin">
<n-avatar round class="cursor-pointer"></n-avatar>
<div class="cursor-pointer ml-2 text-gray text-sm" >登录</div>
</div>
</div>
<!-- 登录弹窗 -->
<d-modal class="!w-120" v-model="visible">
<login-modal v-model:visible="visible"></login-modal>
</d-modal>
<masked :visible="visible" :setVisible="setVisible">
<loginModal :setVisible="setVisible" />
</masked>
</div>
<div class="flex justify-between bg-white lg:hidden">
<div class="pl-2 items-centerflex" slot="brand" @click="">
@ -80,11 +47,11 @@
</div>
<div class="flex items-center mr-2">
<div v-if="userinfo" class="flex items-center">
<d-avatar :img-src="userinfo.ava_url" class="cursor-pointer" alt="用户的头" />
<n-avatar :src="userinfo.ava_url" round class="cursor-pointer" alt="用户的头" />
<div class="cursor-pointer ml-2 text-gray text-sm">{{ userinfo.nickname }}</div>
</div>
<div v-else class="flex items-center">
<d-avatar class="cursor-pointer" @click="toLogin"></d-avatar>
<n-avatar round class="cursor-pointer" @click="toLogin"></n-avatar>
<div class="cursor-pointer ml-2 text-gray text-sm" @click="toLogin">登录</div>
</div>
</div>
@ -101,16 +68,16 @@ import linkSvg from '@/icon/menu/link.svg';
import picSvg from '@/icon/menu/pic.svg';
import settingSvg from '@/icon/menu/setting.svg';
import loginModal from '@/components/Login.vue'
import masked from '@/components/mask.vue'
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useRoute, useRouter, RouterLink } from 'vue-router';
const activeKey = ref('home')
const route = useRoute();
const visible = ref(false);
const router = useRouter();
const key = ref("home");
const locationInfo = ref("获取位置中...");
const latitude = ref<number | null>(null);
const longitude = ref<number | null>(null);
@ -122,7 +89,69 @@ const userinfo: any = ref(null)
const nav: any = useTemplateRef('nav')
const navx = $store.nav.useNavStore()
const usrLog = $store.log.useLogStore()
console.log('>>> --> route:', route)
const menuOptions = ref([
{
label: () => h(RouterLink, { to: '/home', class: 'flex items-center justify-center' }, { default: () => '首页' }),
key: "home",
icon: () => h(homeSvg)
},
{
label: () => h(RouterLink, { to: '/gallery', class: 'flex items-center justify-center' }, { default: () => '画廊管理' }),
key: "gallery",
icon: () => h(picSvg)
},
{
label: () => h(RouterLink, { to: '/blog', class: 'flex items-center justify-center' }, { default: () => '文章管理' }),
key: "blog",
icon: () => h(artiSvg)
},
{
label: () => h(RouterLink, { to: '/widget', class: 'flex items-center justify-center' }, { default: () => '工具管理' }),
key: "widget",
icon: () => h(downSvg)
},
{
label: () => h(RouterLink, { to: '/apps', class: 'flex items-center justify-center' }, { default: () => '软件管理' }),
key: "apps",
icon: () => h(settingSvg)
},
{
label: () => h(RouterLink, { to: '/plink', class: 'flex items-center justify-center' }, { default: () => '友链管理' }),
key: "plink",
icon: () => h(linkSvg)
},
]);
const oprOp = ref([
{
key: 'logout',
label: '退出登录',
},
{
key: 'console',
label: '控制台',
},
{
key: 'setting',
label: '设置',
}
])
function setVisible(v: any) {
visible.value = v
}
function handleSelect(key: string) {
console.log('>>> --> handleSelect --> key:', key)
if (key == 'logout') {
logout()
return
}
if (key == 'console') {
router.push('/console')
return
}
}
// 获取地理位置
const getLocation = () => {
if (!navigator.geolocation) {
@ -198,7 +227,7 @@ async function handdleJw(jw: string) {
}
watch(() => route.name, (newVal) => {
key.value = newVal as string
activeKey.value = newVal as string
})
function goHome() {
@ -220,6 +249,9 @@ function logout() {
usrLog.setIsLogin(false)
userinfo.value = null;
}
function gotoConsole() {
window.open("https://www.hxyouzi.com/console/home", "_BLACK")
}
function gotoHf() {
console.log('>>> --> gotoHf --> fxlink:', fxlink)
@ -230,7 +262,10 @@ onMounted(() => {
userinfo.value = $cookies.get('userinfo');
console.log('>>>>>>>>>>', userinfo.value);
key.value = route.name as string;
setTimeout(() => {
console.log('>>>>>>>>>>', route)
activeKey.value = route.name as string;
}, 20);
console.log('>>> --> route.name:', route.name)
getLocation(); // 组件挂载时获取位置
@ -242,7 +277,7 @@ onMounted(() => {
});
onBeforeUpdate(() => {
userinfo.value = $cookies.get('userinfo');
key.value = route.name as string;
activeKey.value = route.name as string;
const h: number = nav.value.clientHeight
// console.log('******>>> --> nav.value:', nav.value)
// console.log('()()()()()>>> --> h:', h)
@ -257,32 +292,10 @@ onBeforeUpdate(() => {
margin-bottom: 1px;
}
:deep(.devui-menu-horizontal .devui-menu-item:hover span .icon) {
color: var(--devui-brand, #5e7ce0) !important;
fill: var(--devui-brand, #5e7ce0) !important;
:deep(.n-menu-item-content__icon) {
width: 16px !important;
height: 16px !important;
}
:deep(.devui-menu-item-select span svg) {
color: var(--devui-brand, #5e7ce0) !important;
fill: var(--devui-brand, #5e7ce0) !important;
line-height: 100% !important;
}
:deep(.devui-menu-item-select span) {
color: var(--devui-brand, #5e7ce0) !important;
}
:deep(.devui-menu-horizontal) {
padding: 14px 20px 6px;
}
:deep(.devui-menu-item span) {
display: flex;
align-items: center;
.devui-icon__container {
display: flex;
align-items: center;
}
}
</style>

View File

@ -1,5 +0,0 @@
import { Message } from 'vue-devui';
const msg:any = Message;
export default msg;

1
src/icon/loc.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766884807799" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10346" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M774.826667 365.226667c0 117.76-194.56 392.533333-245.76 464.213333-6.826667 8.533333-18.773333 8.533333-25.6 0C450.56 757.76 256 482.986667 256 365.226667 256 230.4 372.053333 119.466667 515.413333 119.466667s259.413333 110.933333 259.413334 245.76z" fill="#ec66ab" p-id="10347"></path><path d="M208.213333 841.386667a307.2 76.8 0 1 0 614.4 0 307.2 76.8 0 1 0-614.4 0Z" fill="#ec66ab" opacity=".5" p-id="10348"></path></svg>

After

Width:  |  Height:  |  Size: 759 B

1
src/icon/search.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1766900628434" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11570" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 512a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#F2ECFF" p-id="11571"></path><path d="M801.723733 695.330133l-273.066666-238.933333a51.2 51.2 0 1 0-67.447467 77.073067l273.066667 238.933333a51.2 51.2 0 0 0 67.447466-77.073067z" fill="#DBBBFF" p-id="11572"></path><path d="M238.933333 494.933333a256 256 0 1 0 512 0 256 256 0 1 0-512 0Z" fill="#BC86F9" p-id="11573"></path><path d="M494.933333 315.733333q74.24 0 126.702934 52.497067 52.497067 52.462933 52.497066 126.702933 0 2.525867-0.477866 4.983467-0.512 2.491733-1.467734 4.8128-0.955733 2.321067-2.389333 4.437333-1.365333 2.082133-3.1744 3.857067-1.774933 1.809067-3.857067 3.208533-2.116267 1.365333-4.437333 2.3552-2.321067 0.955733-4.778667 1.467734-2.491733 0.477867-5.0176 0.477866t-4.983466-0.477866q-2.491733-0.512-4.8128-1.467734-2.321067-0.955733-4.437334-2.389333-2.082133-1.365333-3.857066-3.1744-1.809067-1.774933-3.208534-3.857067-1.365333-2.116267-2.3552-4.437333-0.955733-2.321067-1.467733-4.778667-0.477867-2.491733-0.477867-5.0176 0-53.009067-37.4784-90.5216-37.512533-37.4784-90.5216-37.4784-2.525867 0-4.983466-0.477866-2.491733-0.512-4.8128-1.467734-2.321067-0.955733-4.437334-2.389333-2.082133-1.365333-3.857066-3.1744-1.809067-1.774933-3.208534-3.857067-1.365333-2.116267-2.3552-4.437333-0.955733-2.321067-1.467733-4.778667-0.477867-2.491733-0.477867-5.0176t0.477867-4.983466q0.512-2.491733 1.467733-4.8128 0.955733-2.321067 2.389334-4.437334 1.365333-2.082133 3.1744-3.857066 1.774933-1.809067 3.857066-3.208534 2.116267-1.365333 4.437334-2.3552 2.321067-0.955733 4.778666-1.467733 2.491733-0.477867 5.0176-0.477867z" fill="#FFFFFF" p-id="11574"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 B

BIN
src/lib/assets/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

View File

@ -0,0 +1,183 @@
<template>
<div class="lazy__box">
<div class="lazy__resource">
<div class="image-container">
<el-image
ref="lazyRef"
class="lazy__img"
:src="url"
:preview-src-list="previewSrcList.length > 0 ? previewSrcList : []"
fit="contain"
:preview-teleported="true"
:initial-index="0"
:hide-on-click-modal="hideOnClickModal"
@load="handleLoad"
@error="handleError"
>
<template #placeholder>
<img :src="loading" alt="loading" />
</template>
<template #error>
<img :src="errorImg" alt="error" />
</template>
</el-image>
<div
class="overlay"
v-if="previewSrcList.length > 0 && previewIcon"
@click="hanldeShowPreview"
>
<div class="preview-icon">
<img :src="previewIcon" alt="preview" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ElImage } from "element-plus";
import type { PropType } from "vue";
import { inject, onMounted, ref, toRefs } from "vue";
import loadError from "../assets/loadError.png";
import loadingImg from "../assets/loading.gif";
import type { Nullable } from "../types/util";
const props = defineProps({
previewIcon: {
type: String,
default: "",
},
url: {
type: String,
default: "",
},
loading: {
type: String,
default: loadingImg,
},
errorImg: {
type: String,
default: loadError,
},
previewSrcList: {
type: Array as PropType<string[]>,
default: () => [],
},
hideOnClickModal: {
// 是否可以通过点击遮罩层关闭预览
type: Boolean,
default: false,
},
});
const { url, loading, errorImg, previewSrcList } = toRefs(props);
const imgLoaded = inject("imgLoaded") as () => void;
const lazyRef = ref<Nullable<InstanceType<typeof ElImage>>>(null);
const handleLoad = () => {
imgLoaded();
};
const handleError = () => {
// 可以在这里添加错误处理逻辑
};
// 显示预览
const hanldeShowPreview = () => {
if (lazyRef.value) {
lazyRef.value.showPreview();
}
};
onMounted(() => {
if (lazyRef.value) {
imgLoaded();
}
});
</script>
<style scoped>
.lazy__box {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.lazy__resource {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.lazy__img {
width: 100%;
height: auto;
display: block;
}
:deep(.el-image) {
width: 100%;
display: block;
}
:deep(.el-image__inner) {
width: 100%;
height: auto;
object-fit: cover;
}
.lazy__img img[alt="loading"],
.lazy__img img[alt="error"] {
width: 48px;
height: 48px;
padding: 1em;
margin: 0 auto;
display: block;
}
.image-container {
position: relative;
width: 100%;
cursor: pointer;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-container:hover .overlay {
opacity: 1;
}
.preview-icon {
color: white;
width: 32px;
height: 32px;
}
:deep(.el-image__placeholder),
:deep(.el-image__error) {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
:deep(.el-image__placeholder) img,
:deep(.el-image__error) img {
width: 48px;
height: 48px;
object-fit: contain;
}
</style>

View File

@ -0,0 +1,208 @@
<template>
<div
ref="waterfallWrapper"
class="waterfall-list"
:style="{ height: `${wrapperHeight}px` }"
>
<div
v-for="(item, index) in list"
:key="getKey(item, index)"
class="waterfall-item"
>
<div class="waterfall-card">
<slot
name="item"
:item="item"
:index="index"
:url="getRenderURL(item)"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { PropType } from "vue";
import { provide, ref, watch } from "vue";
import { useDebounceFn } from "@vueuse/core";
import { useCalculateCols, useLayout } from "../use";
import Lazy from "../utils/Lazy";
import { getValue } from "../utils/util";
import type { ViewCard } from "../types/waterfall";
const props = defineProps({
list: {
type: Array as PropType<any[]>,
default: () => [],
},
rowKey: {
type: String,
default: "id",
},
imgSelector: {
type: String,
default: "src",
},
width: {
type: Number,
default: 200,
},
columns: {
type: Number,
default: 3,
},
gutter: {
type: Number,
default: 10,
},
hasAroundGutter: {
type: Boolean,
default: true,
},
animationPrefix: {
type: String,
default: "animate__animated",
},
animationEffect: {
type: String,
default: "fadeIn",
},
animationDuration: {
type: Number,
default: 1000,
},
animationDelay: {
type: Number,
default: 300,
},
backgroundColor: {
type: String,
default: "#fff",
},
lazyload: {
type: Boolean,
default: true,
},
loadProps: {
type: Object,
default: () => {},
},
crossOrigin: {
type: Boolean,
default: true,
},
delay: {
type: Number,
default: 300,
},
});
const lazy = new Lazy(props.lazyload, props.loadProps, props.crossOrigin);
provide("lazy", lazy);
// 容器块信息
const { waterfallWrapper, wrapperWidth, colWidth, cols, offsetX } =
useCalculateCols(props);
// 容器高度,块定位
const { wrapperHeight, layoutHandle } = useLayout(
props,
colWidth,
cols,
offsetX,
waterfallWrapper
);
// 1s内最多执行一次排版减少性能开销
const renderer = useDebounceFn(() => {
layoutHandle();
// console.log("强制更新排版");
}, props.delay);
// 列表发生变化直接触发排版
watch(
() => [wrapperWidth, colWidth, props.list],
() => {
renderer();
},
{ deep: true }
);
// 尺寸宽度变化防抖触发
const sizeChangeTime = ref(0);
provide("sizeChangeTime", sizeChangeTime);
// 图片加载完成
provide("imgLoaded", renderer);
// 根据选择器获取图片地址
const getRenderURL = (item: ViewCard): string => {
return getValue(item, props.imgSelector)[0];
};
// 获取唯一值
const getKey = (item: ViewCard, index: number): string => {
return item[props.rowKey] || index;
};
const clearAndReload = () => {
const originalList = [...props.list];
props.list.length = 0;
setTimeout(() => {
props.list.push(...originalList);
renderer();
}, 0);
};
defineExpose({
waterfallWrapper,
wrapperHeight,
getRenderURL,
getKey,
list: props.list,
backgroundColor: props.backgroundColor,
renderer,
clearAndReload,
});
</script>
<style scoped>
.waterfall-list {
width: 100%;
position: relative;
overflow: hidden;
background-color: v-bind(backgroundColor);
}
.waterfall-item {
position: absolute;
left: 0;
top: 0;
/* transition: .3s; */
/* 初始位置设置到屏幕以外,避免懒加载失败 */
transform: translate3d(0, 3000px, 0);
visibility: hidden;
}
/* 初始的入场效果 */
@-webkit-keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fadeIn {
-webkit-animation-name: fadeIn;
animation-name: fadeIn;
}
</style>

3
src/lib/index.ts Normal file
View File

@ -0,0 +1,3 @@
import Waterfall from './components/Waterfall.vue'
import LazyImg from './components/LazyImg.vue'
export { Waterfall, LazyImg }

4
src/lib/types/images.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.gif" {
const value: string;
export default value;
}

13
src/lib/types/jsx.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import { VNode } from "vue";
declare global {
namespace JSX {
interface Element extends VNode {}
interface ElementClass {
$props: {};
}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

77
src/lib/types/lazy.d.ts vendored Normal file
View File

@ -0,0 +1,77 @@
export interface LazyOptions {
error?: string;
loading?: string;
observerOptions?: IntersectionObserverInit;
log?: boolean;
}
export interface ValueFormatterObject {
src: string;
error?: string;
loading?: string;
}
export default class Lazy {
lazyActive: boolean;
options: LazyOptions;
_images = new WeakMap();
constructor(flag = true, options: LazyOptions);
config(options = {}): void;
// mount
mount(
el: HTMLImageElement,
binding: string | ValueFormatterObject,
callback: () => void
): void;
// unmount
unmount(el: HTMLElement): void;
resize(el: HTMLElement, callback: () => void): void;
/**
* 设置img的src
* @param {*} el - img
* @param {*} src - 原图
* @param {*} error - 错误图片
* @param {*} callback - 完成的回调函数,通知组件刷新布局
* @returns
*/
_setImageSrc(
el: HTMLImageElement,
src: string,
callback: () => void,
error?: string
): void;
_isOpenLazy(): boolean;
/**
* 添加img和对应的observer到weakMap中
* 开启监听
* 当出现在可视区域后取消监听
* @param {*} el - img
* @param {*} src - 图片
* @param {*} error - 错误图片
* @param {*} callback - 完成的回调函数,通知组件刷新布局
*/
_initIntersectionObserver(
el: HTMLImageElement,
src: string,
callback: () => void,
error?: string
): void;
// 格式化参数
_valueFormatter(value: ValueFormatterObject | string): ValueFormatterObject;
// 日志
_log(callback: () => void): void;
// 在map中获取对应img的observer事件
_realObserver(el: HTMLElement): IntersectionObserver | undefined;
}

4
src/lib/types/shims-vue.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module "*.png" {
const src: string;
export default src;
}

3
src/lib/types/util.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export type Nullable<T> = T | null
export type CssStyleObject = Partial<CSSStyleDeclaration> & Record<string, string | null>

34
src/lib/types/waterfall.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
export interface ViewCard {
src?: any;
id?: string;
name?: string;
star?: boolean;
backgroundColor?: string;
[key: string]: any;
}
export interface WaterfallProps {
columns: number;
width: number;
animationDuration: number;
animationDelay: number;
animationEffect: string;
hasAroundGutter: boolean;
gutter: number;
list: ViewCard[];
animationPrefix: string;
backgroundColor: string;
lazyload: boolean;
loadProps: Record<string, any>;
crossOrigin: boolean;
delay: number;
rowKey: string;
imgSelector: string;
}
export interface ItemWidthProps {
wrapperWidth: number;
gutter: number;
hasAroundGutter: boolean;
columns: number;
}

3
src/lib/use/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { useCalculateCols } from './useCalculateCols'
export { useLayout } from './useLayout'

View File

@ -0,0 +1,46 @@
import { computed, ref } from "vue";
import { useResizeObserver } from "@vueuse/core";
import { getItemWidth } from "../utils/itemWidth";
import type { WaterfallProps } from "../types/waterfall";
import type { Nullable } from "../types/util";
export function useCalculateCols(props: WaterfallProps) {
const wrapperWidth = ref<number>(0);
const waterfallWrapper = ref<Nullable<HTMLElement>>(null);
useResizeObserver(waterfallWrapper, (entries) => {
const entry = entries[0];
const { width } = entry.contentRect;
if (width === 0) return;
wrapperWidth.value = width;
});
// 列实际宽度
const colWidth = computed(() => {
return getItemWidth({
wrapperWidth: wrapperWidth.value,
gutter: props.gutter,
hasAroundGutter: props.hasAroundGutter,
columns: props.columns,
});
});
// 列
const cols = computed(() => props.columns);
// 偏移
const offsetX = computed(() => {
const offset = props.hasAroundGutter ? props.gutter : -props.gutter;
const contextWidth =
cols.value * (colWidth.value + props.gutter) + offset;
return (wrapperWidth.value - contextWidth) / 2;
});
return {
waterfallWrapper,
wrapperWidth,
colWidth,
cols,
offsetX,
};
}

118
src/lib/use/useLayout.ts Normal file
View File

@ -0,0 +1,118 @@
import type { Ref } from "vue";
import { ref } from "vue";
import { addClass, hasClass, prefixStyle } from "../utils/dom";
import type { WaterfallProps } from "../types/waterfall";
import type { CssStyleObject, Nullable } from "../types/util";
const transform = prefixStyle("transform");
const duration = prefixStyle("animation-duration");
const delay = prefixStyle("animation-delay");
const transition = prefixStyle("transition");
const fillMode = prefixStyle("animation-fill-mode");
export function useLayout(
props: WaterfallProps,
colWidth: Ref<number>,
cols: Ref<number>,
offsetX: Ref<number>,
waterfallWrapper: Ref<Nullable<HTMLElement>>
) {
const posY = ref<number[]>([]);
const wrapperHeight = ref(0);
// 获取对应y下标的x的值
const getX = (index: number): number => {
const count = props.hasAroundGutter ? index + 1 : index;
return props.gutter * count + colWidth.value * index + offsetX.value;
};
// 初始y
const initY = (): void => {
posY.value = new Array(cols.value).fill(
props.hasAroundGutter ? props.gutter : 0
);
};
// 添加入场动画
const animation = addAnimation(props);
// 排版
const layoutHandle = async () => {
// 初始化y集合
initY();
// 构造列表
const items: HTMLElement[] = [];
if (waterfallWrapper && waterfallWrapper.value) {
waterfallWrapper.value.childNodes.forEach((el: any) => {
if (el!.className === "waterfall-item") items.push(el);
});
}
// 获取节点
if (items.length === 0) return false;
// 遍历节点
for (let i = 0; i < items.length; i++) {
const curItem = items[i] as HTMLElement;
// 最小的y值
const minY = Math.min.apply(null, posY.value);
// 最小y的下标
const minYIndex = posY.value.indexOf(minY);
// 当前下标对应的x
const curX = getX(minYIndex);
// 设置x,y,width
const style = curItem.style as CssStyleObject;
// 设置偏移
if (transform) style[transform] = `translate3d(${curX}px,${minY}px, 0)`;
style.width = `${colWidth.value}px`;
// 更新当前index的y值
const { height } = curItem.getBoundingClientRect();
posY.value[minYIndex] += height + props.gutter;
// 添加入场动画
animation(curItem, () => {
// 添加动画时间
if (transition) style[transition] = "transform .3s";
});
}
wrapperHeight.value = Math.max.apply(null, posY.value);
};
return {
wrapperHeight,
layoutHandle,
};
}
// 动画
function addAnimation(props: WaterfallProps) {
return (item: HTMLElement, callback?: () => void) => {
const content = item!.firstChild as HTMLElement;
if (content && !hasClass(content, props.animationPrefix)) {
const durationSec = `${props.animationDuration / 1000}s`;
const delaySec = `${props.animationDelay / 1000}s`;
const style = content.style as CssStyleObject;
style.visibility = "visible";
if (duration) style[duration] = durationSec;
if (delay) style[delay] = delaySec;
if (fillMode) style[fillMode] = "both";
addClass(content, props.animationPrefix);
addClass(content, props.animationEffect);
// 确保动画完成后item可见
setTimeout(() => {
const itemStyle = item.style as CssStyleObject;
itemStyle.visibility = "visible";
if (callback) callback();
}, props.animationDuration + props.animationDelay);
}
};
}

188
src/lib/utils/Lazy.ts Normal file
View File

@ -0,0 +1,188 @@
import type { LazyOptions, ValueFormatterObject } from '../types/lazy'
import type { CssStyleObject } from '../types/util'
import { loadImage } from './loader'
import { assign, hasIntersectionObserver, isObject } from './util'
enum LifecycleEnum {
LOADING = 'loading',
LOADED = 'loaded',
ERROR = 'error',
}
const DEFAULT_OBSERVER_OPTIONS = {
rootMargin: '0px',
threshold: 0,
}
const DEFAULT_LOADING = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
const DEFAULT_ERROR = ''
export default class Lazy {
lazyActive = true // 是否开启懒加载
crossOrigin = true // 开启跨域
options: LazyOptions = {
loading: DEFAULT_LOADING,
error: DEFAULT_ERROR,
observerOptions: DEFAULT_OBSERVER_OPTIONS,
log: true,
}
_images = new WeakMap()
constructor(flag = true, options: LazyOptions, crossOrigin = true) {
this.lazyActive = flag
this.crossOrigin = crossOrigin
this.config(options)
}
config(options = {}) {
assign(this.options, options)
}
// mount
mount(el: HTMLImageElement, binding: string | ValueFormatterObject, callback: () => void): void {
const { src, loading, error } = this._valueFormatter(binding)
el.setAttribute('lazy', LifecycleEnum.LOADING)
el.setAttribute('src', loading || DEFAULT_LOADING)
if (!this.lazyActive) {
this._setImageSrc(el, src, callback, error)
}
else {
if (!hasIntersectionObserver) {
this._setImageSrc(el, src, callback, error)
this._log(() => {
throw new Error('Not support IntersectionObserver!')
})
}
this._initIntersectionObserver(el, src, callback, error)
}
}
// resize
resize(el: HTMLImageElement, callback: () => void) {
const lazy = el.getAttribute('lazy')
const src = el.getAttribute('src')
if (lazy && lazy === LifecycleEnum.LOADED && src) {
loadImage(src, this.crossOrigin).then((image) => {
const { width, height } = image
const curHeight = (el.width / width) * height
el.height = curHeight
const style = el.style as CssStyleObject
style.height = `${curHeight}px`
callback()
})
}
}
// unmount
unmount(el: HTMLElement): void {
const imgItem = this._realObserver(el)
imgItem && imgItem.unobserve(el)
this._images.delete(el)
}
/**
* 设置img的src
* @param {*} el - img
* @param {*} src - 原图
* @param {*} error - 错误图片
* @param {*} callback - 完成的回调函数,通知组件刷新布局
* @returns
*/
_setImageSrc(el: HTMLImageElement, src: string, callback: () => void, error?: string): void {
if (!src)
return
const preSrc = el.getAttribute('src')
if (preSrc === src)
return
loadImage(src, this.crossOrigin)
.then((image) => {
// 修改容器
const { width, height } = image
const ratio = height / width
const lazyBox = el.parentNode!.parentNode as HTMLElement
lazyBox.style.paddingBottom = `${ratio * 100}%`
// 设置图片
el.setAttribute('lazy', LifecycleEnum.LOADED)
el.removeAttribute('src')
el.setAttribute('src', src)
callback()
})
.catch(() => {
const imgItem = this._realObserver(el)
imgItem && imgItem.disconnect()
if (error) {
el.setAttribute('lazy', LifecycleEnum.ERROR)
el.setAttribute('src', error)
}
this._log(() => {
throw new Error(`Image failed to load!And failed src was: ${src} `)
})
callback()
})
}
_isOpenLazy(): boolean {
return hasIntersectionObserver && this.lazyActive
}
/**
* 添加img和对应的observer到weakMap中
* 开启监听
* 当出现在可视区域后取消监听
* @param {*} el - img
* @param {*} src - 图片
* @param {*} error - 错误图片
* @param {*} callback - 完成的回调函数,通知组件刷新布局
*/
_initIntersectionObserver(el: HTMLImageElement, src: string, callback: () => void, error?: string): void {
const observerOptions = this.options.observerOptions
this._images.set(
el,
new IntersectionObserver((entries) => {
Array.prototype.forEach.call(entries, (entry) => {
if (entry.isIntersecting) {
const imgItem = this._realObserver(el)
imgItem && imgItem.unobserve(entry.target)
this._setImageSrc(el, src, callback, error)
}
})
}, observerOptions),
)
const imgItem = this._realObserver(el)
imgItem && imgItem.observe(el)
}
// 格式化参数
_valueFormatter(value: ValueFormatterObject | string): ValueFormatterObject {
let src = value as string
let loading = this.options.loading
let error = this.options.error
if (isObject(value)) {
src = (value as ValueFormatterObject).src
loading = (value as ValueFormatterObject).loading || this.options.loading
error = (value as ValueFormatterObject).error || this.options.error
}
return {
src,
loading,
error,
}
}
// 日志
_log(callback: () => void): void {
if (this.options.log)
callback()
}
// 在map中获取对应img的observer事件
_realObserver(el: HTMLElement): IntersectionObserver | undefined {
return this._images.get(el)
}
}

53
src/lib/utils/dom.ts Normal file
View File

@ -0,0 +1,53 @@
import type { CssStyleObject } from '../types/util'
export function hasClass(el: HTMLElement, className: string) {
const reg = new RegExp(`(^|\\s)${className}(\\s|$)`)
return reg.test(el.className)
}
export function addClass(el: HTMLElement, className: string) {
if (hasClass(el, className))
return
const newClass = el.className.split(/\s+/)
newClass.push(className)
el.className = newClass.join(' ')
}
export function removeClass(el: HTMLElement, className: string) {
if (hasClass(el, className)) {
const newClass = el.className.split(/\s+/).filter(name => name !== className)
el.className = newClass.join(' ')
}
}
const elementStyle = document.createElement('div').style as CssStyleObject
const vendor = (() => {
const transformNames: Record<string, string> = {
webkit: 'webkitTransform',
Moz: 'MozTransform',
O: 'OTransform',
ms: 'msTransform',
standard: 'transform',
}
for (const key in transformNames) {
const val = transformNames[key]
if (elementStyle[val] !== undefined)
return key
}
return false
})()
export function prefixStyle(style: string) {
if (vendor === false)
return false
if (vendor === 'standard')
return style
return vendor + style.charAt(0).toUpperCase() + style.substr(1)
}

View File

@ -0,0 +1,19 @@
import type { ItemWidthProps } from "../types/waterfall";
/**
* @description: 获取当前窗口尺寸下格子的宽度
* @param {ItemWidthProps} param1
* @return {*}
*/
export const getItemWidth = ({
wrapperWidth,
gutter,
hasAroundGutter,
columns,
}: ItemWidthProps) => {
if (hasAroundGutter) {
return (wrapperWidth - gutter) / columns - gutter;
} else {
return (wrapperWidth - (columns - 1) * gutter) / columns;
}
};

20
src/lib/utils/loader.ts Normal file
View File

@ -0,0 +1,20 @@
/**
* load images
* @param {Array[String]} images - 图片链接数组
*/
export function loadImage(url: string, crossOrigin: Boolean): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => {
resolve(image)
}
image.onerror = () => {
reject(new Error('Image load error'))
}
if (crossOrigin)
image.crossOrigin = 'Anonymous' // 支持跨域图片
image.src = url
})
}

156
src/lib/utils/util.ts Normal file
View File

@ -0,0 +1,156 @@
export const inBrowser = typeof window !== 'undefined' && window !== null
export const hasIntersectionObserver = checkIntersectionObserver()
const isEnumerable = Object.prototype.propertyIsEnumerable
const getSymbols = Object.getOwnPropertySymbols
/**
* 取值
* @param {Object | Array} form
* @param {...any} selectors
* @returns
*/
export function getValue(form: any, ...selectors: string[]) {
const res = selectors.map((s) => {
return s
.replace(/\[(\w+)\]/g, '.$1')
.split('.')
.reduce((prev, cur) => {
return prev && prev[cur]
}, form)
})
return res
}
/**
* 防抖
* @param {*} fn
* @param {*} delay
* @returns
*/
export function debounce(fn: (args?: any) => void, delay: number) {
let timer: any
return function(this: any, ...args: any) {
timer && clearTimeout(timer)
timer = null
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
/**
* 判断是否支持IntersectionObserver
* @returns {boolean}
*/
export function checkIntersectionObserver(): boolean {
if (
inBrowser
&& 'IntersectionObserver' in window
&& 'IntersectionObserverEntry' in window
&& 'intersectionRatio' in window.IntersectionObserverEntry.prototype
) {
// Minimal polyfill for Edge 15's lack of `isIntersecting`
// See: https://github.com/w3c/IntersectionObserver/issues/211
if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', {
get() {
return this.intersectionRatio > 0
},
})
}
return true
}
return false
}
/**
* is object
*
* @param {*} val
* @returns {boolean}
*/
export function isObject(val: any): boolean {
return typeof val === 'function' || toString.call(val) === '[object Object]'
}
/**
* is primitive
*
* @param {*} val
* @returns {boolean}
*/
export function isPrimitive(val: any): boolean {
return typeof val === 'object' ? val === null : typeof val !== 'function'
}
/**
* check private key
*
* @export
* @param {*} key
* @returns {boolean}
*/
export function isValidKey(key: any): boolean {
return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'
}
/**
* Assign the enumerable es6 Symbol properties from one
* or more objects to the first object passed on the arguments.
* Can be used as a supplement to other extend, assign or
* merge methods as a polyfill for the Symbols part of
* the es6 Object.assign method.
* https://github.com/jonschlinkert/assign-symbols
*
* @param {*} target
* @param {Array} args
* @returns
*/
function assignSymbols(target: any, ...args: any[]) {
if (!isObject(target))
throw new TypeError('expected the first argument to be an object')
if (args.length === 0 || typeof Symbol !== 'function' || typeof getSymbols !== 'function')
return target
for (const arg of args) {
const names = getSymbols(arg)
for (const key of names) {
if (isEnumerable.call(arg, key))
target[key] = arg[key]
}
}
return target
}
/**
* Deeply assign the values of all enumerable-own-properties and symbols
* from one or more source objects to a target object. Returns the target object.
* https://github.com/jonschlinkert/assign-deep
*
* @param {*} target
* @param {Array} args
* @returns
*/
export function assign(target: any, ...args: any[]): void {
let i = 0
if (isPrimitive(target)) target = args[i++]
if (!target) target = {}
for (; i < args.length; i++) {
if (isObject(args[i])) {
for (const key of Object.keys(args[i])) {
if (isValidKey(key)) {
if (isObject(target[key]) && isObject(args[i][key]))
assign(target[key], args[i][key])
else
target[key] = args[i][key]
}
}
assignSymbols(target, args[i])
}
}
return target
}

View File

@ -3,23 +3,16 @@ import { useConfig } from "@/config";
import icon from "@/icon/index.ts";
import { createPinia } from "pinia";
import "virtual:uno.css";
import { createApp, vaporInteropPlugin } from "vue";
import 'vue-devui/tag/style.css';
import App from "./App.vue";
import router from "./router";
// 自定义主题配置 - 设置主色和二级色
import { ThemeServiceInit, infinityTheme, sweetTheme } from "devui-theme";
// 自定义主题配置 - 设置主色和二级色\
import "vfonts/FiraCode.css";
import Tag from 'vue-devui/tag';
import { PerfectScrollbarPlugin } from "vue3-perfect-scrollbar";
// import vue3videoPlay from "vue3-video-play";
// import "vue3-video-play/dist/style.css";
//main.js
// ThemeServiceInit({ customTheme }, "customTheme");
const themeService = ThemeServiceInit({ infinityTheme }, "infinityTheme");
themeService?.applyTheme(sweetTheme);
const app = createApp(App);
app.use(vaporInteropPlugin)
app.use(Tag)
app.use(createPinia());
app.use(router);
@ -30,4 +23,3 @@ for (const key in icon) {
}
app.use(PerfectScrollbarPlugin);
app.mount("#app");

View File

@ -1,4 +1,3 @@
import router from "@/router";
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios";
import axios from "axios";
import { useCookies } from "vue3-cookies";
@ -45,16 +44,16 @@ request.interceptors.response.use(
console.log("Response error", error);
if (error.response?.status === 401) {
// window.$msg.warning("无效的token");
window.$msg.warning("无效的token");
cookies.remove("token");
cookies.remove("userinfo");
window.$modal({
title: "无效的token",
content: "token已失效需要登录请登录 =>",
handdleSubmit: () => {
router.replace("/login");
},
});
// window.$modal({
// title: "无效的token",
// content: "token已失效需要登录请登录 =>",
// handdleSubmit: () => {
// router.replace("/login");
// },
// });
// router.replace("/login");
return "Unauthorized";

15
src/util/theme.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* js 文件下使用这个做类型提示
* @type import('naive-ui').GlobalThemeOverrides
*/
const themeOverrides = {
common: {
primaryColor: '#ec66ab',
primaryColorHover: '#ec66ab',
primaryColorPressed: '#ec66ab',
primaryColorSuppl: '#ec66ab',
},
// ...
}
export default themeOverrides;

View File

@ -1,30 +0,0 @@
<template>
<div class="article-page">
<div id="vditor"></div>
</div>
</template>
<script setup lang="ts">
import Vditor from 'vditor';
import 'vditor/dist/index.css';
import { onMounted, ref } from 'vue';
definePage({
name: 'article',
meta: {
title: '文章',
}
})
// 文章页逻辑
const vditor:any = ref(null);
onMounted(() => {
vditor.value = new Vditor('vditor', {
height: '100vh',
width: '100vw'
});
});
</script>
<style scoped>
/* 文章页样式 */
</style>

18
src/views/Blog.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<div class="article-page">
<h1>文章页</h1>
</div>
</template>
<script setup lang="ts">
definePage({
name: 'blog',
meta: {
title: '文章',
}
})
</script>
<style scoped>
/* 文章页样式 */
</style>

View File

@ -1,126 +1,75 @@
<template>
<PerfectScrollbar ref="scrollbar" @ps-scroll-y="handleScroll">
<!-- <PerfectScrollbar ref="scrollbar" @ps-scroll-y="handleScroll"> -->
<div ref="myCon" class="gallery-page py-5 px-[10%]">
<d-search class="mt-0 mb-8 w-2/3 mx-auto rounded-full" v-model="kw" is-keyup-search :delay="1000"
@search="onSearch"></d-search>
<div class="gallery-container w-full box-border">
<!-- 瀑布流容器 -->
<div ref="waterfallContainer" v-image-preview
class="waterfall-container flex justify-between flex-nowrap w-full overflow-hidden">
<!-- 动态生成的列 -->
<div v-for="(column, index) in columns" :key="index" class="waterfall-column flex flex-col w-[240px]">
<div v-for="item in column" :key="item.id"
class="gallery-item group relative my-[10px] rounded-lg overflow-hidden transition-transform duration-300 box-border hover:-translate-y-1.5">
<!-- <d-search class="mt-0 mb-8 w-2/3 mx-auto rounded-full" v-model="kw" is-keyup-search :delay="1000"
@search="onSearch"></d-search> -->
<n-input class="mb-8 !w-[90%] ml-[5%]" size="large" v-model:value="kw" @keyup.enter="onSearch" placeholder="请输入">
<template #suffix>
<n-icon size="large">
<icon-search />
</n-icon>
</template>
</n-input>
<!-- <div v-infinite-scroll="loadMore"> -->
<Waterfall ref="waterfall" :list="fileList" :width="cwidth" :gutter="gutter" :columns="column" img-selector="url"
animation-effect="fadeIn" :animation-duration="1000" :animation-delay="300" backgroundColor="transparent"> >
<template #item="{ item }">
<div
class="absolute px-2 truncate hidden group-hover:block top-0 text-center w-full bg-[#00000070] text-white">
{{ item.filename }}</div>
<img :src="item.filepath" alt="" class="gallery-image block w-full h-auto object-cover rounded-md">
<div class="px-2 absolute bottom-0 flex justify-between w-full bg-[#00000060]">
<div class="text-white "> <span class="text-[#f1d9db] font-600">{{ item.nickname }}</span> 上传</div>
<d-popover content="下载" trigger="hover" class="!bg-primary" style="color: #fff">
<icon-download @click="downloadFile(item.filepath)"
class="w-5 h-5 text-white hover-text-primary"></icon-download>
</d-popover>
class="card rounded-md overflow-hidden group shadow-md transition-transform duration-300 box-border hover:-translate-y-1.5">
<!-- <div class="image-wrapper"> -->
<LazyImg class="rounded-md overflow-hidden" :url="item.filepath" />
<!-- </div> -->
<div
class="hidden truncate group-hover:block absolute rounded-md z-10 truncate top-0 text-center w-full bg-[#00000070] text-white">
{{ item.filename }}
</div>
<div
class="absolute rounded-md flex z-10 truncate bottom-0 w-full bg-[#00000070] text-white justify-between items-center">
<div> <span class="text-[#f1d9db] font-600">{{ item.nickname }}</span> 分享</div>
<icon-download class="cursor-pointer w-4 h-4" @click="downloadFile(item.filepath)" />
</div>
</div>
</template>
</Waterfall>
</div>
</div>
<!-- 加载中指示器 -->
<div v-if="loading" class="loading-indicator text-center p-5 text-gray-600">加载中...</div>
<div v-else class="loading-indicator text-center p-5 text-gray-600">已全部加载完成</div>
</div>
</div>
</PerfectScrollbar>
<!-- </div> -->
<!-- </PerfectScrollbar> -->
</template>
<script setup lang="ts">
import { throttle } from 'es-toolkit';
import { onMounted, onUnmounted, ref } from 'vue';
import { LazyImg, Waterfall } from '../lib';
definePage({
name: 'gallery',
meta: {
title: '画廊',
}
})
declare module "vue" {
interface HTMLAttributes {
"v-infinite-scroll"?: () => void;
}
}
const waterfall = ref<any>(null);
// 画廊页逻辑
const fileList = ref<any[]>([]);
const pn = ref(1);
const ps = ref(40);
const loading = ref(false);
const waterfallContainer = ref<HTMLDivElement | null>(null);
const columns = ref<Array<Array<any>>>([]);
const columnHeights = ref<number[]>([]);
const columnCount = ref(4); // 默认列数
const itemWidth = ref(240); // 图片宽度固定为240px
const kw = ref<string>('');
const cwidth = ref<number>(220);
const column = ref<number>(5);
const gutter = ref<number>(20);
// const uploadOptions = ref({
// uri: 'https://www.hxyouzi.com/api/files/upload',
// method: 'POST',
// maximumSize: 5 * 1024 * 1024,
// headers: {
// 'Authorization': 'Bearer ' + $cookies.get('token'),
// },
// });
// 计算列数 based on 屏幕宽度
function calculateColumnCount() {
if (!waterfallContainer.value) return;
const containerWidth = waterfallContainer.value.clientWidth;
const newColumnCount = Math.max(1, Math.floor(containerWidth / itemWidth.value));
if (newColumnCount !== columnCount.value) {
columnCount.value = newColumnCount;
resetWaterfall();
}
}
// 重置瀑布流
function resetWaterfall() {
columns.value = Array(columnCount.value).fill(0).map(() => []);
columnHeights.value = Array(columnCount.value).fill(0);
// 重新分配图片
fileList.value.forEach(async item =>await addToWaterfall(item));
console.log('>>> --> resetWaterfall --> fileList:', fileList.value)
}
// 添加图片到瀑布流
async function addToWaterfall(item: any) {
if (columns.value.length === 0) return;
const { height, width } =await getImageSizeByCheck(item.filepath)
// 找到高度最小的列
let minHeight = Math.min(...columnHeights.value);
let minIndex = columnHeights.value.indexOf(minHeight);
console.log('>>> --> addToWaterfall --> item:', item)
console.log('>>> --> addToWaterfall --> minIndex:', minIndex)
// 添加到该列
columns.value[minIndex].push(item);
// 估算列高 - 实际高度会在图片加载后更新
const estimatedHeight = itemWidth.value * height / width
columnHeights.value[minIndex] += estimatedHeight + 20; // 加上padding和margin
}
function getImageSizeByCheck(url: string): any {
return new Promise(function (resolve, reject) {
let image = new Image();
image.src = url;
let height = 0
let width = 0
// let timer = setTimeout(() => {
image.onload = () => {
if (image.width > 0 && image.height > 0) {
height = image.height
width = image.width
resolve({ height, width })
// clearTimeout(timer)
}
}
// }, 100)
});
// 计算列数
function calculateColumns() {
const totalWidth = window.innerWidth * 0.8; // 画廊宽度为视口宽度的80%
const col = Math.floor(totalWidth / (cwidth.value + gutter.value));
column.value = col > 0 ? col : 1;
waterfall.value?.renderer()
}
// 获取文件列表
@ -137,55 +86,27 @@ async function getFileList() {
if (pn.value === 1) {
fileList.value = res.data;
resetWaterfall();
} else {
// 追加新数据
res.data.forEach((item: any) => {
fileList.value.push(item);
addToWaterfall(item);
});
}
} catch (error) {
console.error('获取文件列表失败:', error);
} finally {
loading.value = false;
waterfall.value?.renderer();
}
}
// // 获取我的文件列表
// async function getMyList() {
// if (loading.value) return;
// loading.value = true;
// try {
// const res = await $http.file.getMyList({
// page_num: pn.value,
// page_size: ps.value,
// });
// // console.log('>>> --> getFileList --> res:', res);
// if (pn.value === 1) {
// fileList.value = res.data;
// resetWaterfall();
// } else {
// // 追加新数据
// res.data.forEach((item: any) => {
// fileList.value.push(item);
// addToWaterfall(item);
// });
// }
// } catch (error) {
// console.error('获取文件列表失败:', error);
// } finally {
// loading.value = false;
// }
// }
// 监听滚动事件
const handleScroll: any = throttle((e: any) => {
console.log('>>> --> handleScroll --> loading:', e)
// console.log('>>> --> handleScroll --> loading:', e)
if (loading.value) return;
const scrollTop = e.target.scrollTop
console.log('>>> --> handleScroll --> scrollTop:', scrollTop)
// console.log('>>> --> handleScroll --> scrollTop:', scrollTop)
const scrollHeight = e.target.scrollHeight
// console.log('>>> --> handleScroll --> scrollHeight:', scrollHeight)
const clientHeight = e.target.offsetHeight;
@ -224,12 +145,10 @@ function downloadFile(url: string) {
}
onMounted(() => {
calculateColumns()
getFileList();
// 计算初始列数
calculateColumnCount();
// 添加窗口大小变化监听
window.addEventListener('resize', calculateColumnCount);
// 添加滚动监听
window.addEventListener('resize', calculateColumns);
window.addEventListener('scroll', handleScroll);
});
@ -237,10 +156,7 @@ onUnmounted(() => {
pn.value = 1;
kw.value = '';
fileList.value = [];
resetWaterfall();
// 移除监听
window.removeEventListener('resize', calculateColumnCount);
window.removeEventListener('scroll', handleScroll);
});
</script>

View File

@ -1,37 +1,42 @@
<template>
<div class="home-page" :style="contentStyle">
<d-layout>
<d-content class="main-content">
<div class="home-page" :content-style="contentStyle">
<n-layout has-sider>
<n-layout-content class="main-content">
<div class="pt-8 px-12 relative hidden lg:block">
<d-input class="devui-input-demo__mt" size="lg" v-model="searchWord" @blur="cancelSbox" placeholder="请输入">
<template #prepend>
<d-select class="w-48" size="lg" v-model="broswer" :options="options" @click="cancelSbox"></d-select>
<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" v-model:value="searchWord" @blur="cancelSbox" placeholder="请输入">
<template #suffix>
<n-icon size="large">
<icon-search />
</n-icon>
</template>
<template #append>
<d-icon name="search" style="font-size: inherit;" @click="search" />
</template>
</d-input>
<div v-if="searchBox" class="absolute left-34 mt-2 z-10 bg-white text-sm text-gray-500 max-h-40 rounded-md shadow-md px-4 py-2 max-w-80">
</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>
: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">
<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> -->
<!-- 图片网格展示区域 -->
<PerfectScrollbar class="" :style="navStyle">
<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">
<d-card class="bg-[#ffffff80] h-24" v-for="(item, index) in navlist" :key="index"
@click="goExtra(item.menu_link)" @contextmenu.prevent="handdleContextMenu($event, item)">
<template #content>
<n-card embedded
class="bg-[#ffffff80] 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">
@ -42,56 +47,53 @@
<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>
</template>
</d-card>
<d-card class="bg-[#ffffff80] h-25">
</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-12 h-12 rounded-full text-2xl text-white flex items-center justify-center">
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>
</d-card>
</n-card>
</div>
</PerfectScrollbar>
</d-content>
<d-aside class="daside hidden w-120 lg:block">
</n-scrollbar>
</n-layout-content>
<n-layout-sider width="30rem">
<homeSide></homeSide>
</d-aside>
</d-layout>
</n-layout-sider>
</n-layout>
<!-- 新增导航弹窗 -->
<d-modal class="!w-120" v-model="visible" title="新增导航">
<d-form ref="formNav" layout="vertical" :data="navData">
<d-form-item class="h-8" field="username">
<d-input @blur="getIcon" v-model="navData.menu_link" placeholder="请输入单行链接(必填)" />
</d-form-item>
<d-form-item class="h-8" field="password">
<d-input v-model="navData.menu_name" placeholder="请输入导航名称(必填)" />
</d-form-item>
<d-form-item class="h-8" field="tag">
<d-input v-model="navData.tag" placeholder="请自定义一个标签(必填,只取前四字)" />
</d-form-item>
<d-form-item class="h-8 form-operation-wrap">
<div class="flex">
<d-input v-model="navData.menu_icon" placeholder="请输入图标链接" />
<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>
</d-form-item>
</d-form>
<!-- <div class="mt-14 w-full flex justify-between">
<d-button @click="navCancel" variant="text" class="w-[49%] hover:bg-[#8a6684] hover:!text-white">取消</d-button>
<span class="text-[20px]"> | </span>
<d-button @click="navSubmit" variant="text" class="w-[49%] hover:bg-[#5c866a] hover:!text-white"
color="primary">确定</d-button>
</div> -->
<!-- </div> -->
</n-form-item>
<div class="mt-14 flex justify-between">
<d-button class="w-[48%]" variant="solid" color="secondary" @click="navCancel">取消</d-button>
<d-button class="w-[48%]" variant="solid" color="primary" @click="navSubmit">确定</d-button>
<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>
</d-modal>
</n-form>
</maskX>
<!-- 音乐插件 -->
<aplayer></aplayer>
@ -116,25 +118,29 @@
</contextMenu>
<!-- 编辑弹窗 -->
<d-modal class="!w-120" v-model="editModal" title="导航修改">
<div class="mb-2">
<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>
<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>
<d-input v-model="editInput" placeholder="请输入修改内容"></d-input>
<n-input v-model:value="editInput" placeholder="请输入修改内容"></n-input>
<div class="mt-8 flex justify-between">
<d-button class="w-[48%]" variant="solid" color="secondary" @click="handdleItemCancel">取消</d-button>
<d-button class="w-[48%]" variant="solid" color="primary" @click="handdleItemSubmit">确定</d-button>
<n-button class="w-[48%]" secondary @click="handdleItemCancel">取消</n-button>
<n-button class="w-[48%]" type="primary" @click="handdleItemSubmit">确定</n-button>
</div>
</d-modal>
</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',
@ -144,24 +150,48 @@ definePage({
})
// 定义接口类型
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: any = reactive({});
const currentItem = ref<any>(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 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: any = ref<boolean>(false)
const navData: any = reactive({
const visible = ref<boolean>(false)
const navData = reactive<NavItem>({
menu_link: '',
menu_name: '',
tag: '',
menu_icon: ''
})
const formNav: any = ref(null)
const navcards: any = ref(null)
const formNav = ref<any>(null)
const navcards = ref<any>(null)
// 首页逻辑
const nav: any = $store.nav.useNavStore()
@ -174,22 +204,22 @@ const selecedIdx = ref(0)
const searchBox = ref(false)
const options = ref([
{
name: '必应',
label: '必应',
value: 'bing',
url: 'https://cn.bing.com/search?q='
},
{
name: '百度',
label: '百度',
value: 'baidu',
url: 'https://www.baidu.com/s?wd='
},
{
name: '谷歌',
label: '谷歌',
value: 'google',
url: 'https://www.google.com/search?q='
},
{
name: '翻译',
label: '翻译',
value: 'trans',
url: 'https://translate.volcengine.com?text='
},
@ -197,9 +227,9 @@ const options = ref([
])
const navlist: any = ref([])
let cloneNavlist: any = []
const tagList: any = ref([
const navlist = ref<NavItem[]>([])
let cloneNavlist: NavItem[] = []
const tagList = ref<TagItem[]>([
{
name: '全部',
color: '',
@ -209,11 +239,11 @@ const tagList: any = ref([
const usrLog = $store.log.useLogStore()
let timer: any = null;
let timer: any = null
// 输入搜索内容时监听searchWord在navlist中模糊搜索
watch(searchWord, () => {
handdleInput()
handleInput()
})
function cancelSbox() {
@ -224,7 +254,7 @@ function cancelSbox() {
}, 200)
}
function handdleInput() {
function handleInput() {
if (!searchWord.value) {
searchItems.value = []
searchBox.value = false
@ -234,24 +264,23 @@ function handdleInput() {
searchBox.value = true
selecedIdx.value = 0
const keyword = searchWord.value.toLowerCase()
searchItems.value = navlist.value.filter((item: any) =>
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, "name")}中搜索"${searchWord.value}"`,
menu_name: `${getDictValue(options.value, "value", broswer.value, "label")}中搜索"${searchWord.value}"`,
menu_link: getDictValue(options.value, "value", broswer.value, "url") + searchWord.value,
tag: ''
})
}
function handdleKeyup(e: any) {
function handleKeyup(e: KeyboardEvent) {
if (!searchBox.value) return
// console.log('>>> --> haddleDown --> idx:', e.keyCode)
// 向下箭头
if (e.keyCode == 40) {
if (e.keyCode === 40) {
if (selecedIdx.value < searchItems.value.length - 1) {
selecedIdx.value += 1
} else {
@ -259,7 +288,7 @@ function handdleKeyup(e: any) {
}
}
// 向上箭头
else if (e.keyCode == 38) {
else if (e.keyCode === 38) {
if (selecedIdx.value > 0) {
selecedIdx.value -= 1
} else {
@ -267,7 +296,7 @@ function handdleKeyup(e: any) {
}
}
// 回车键
else if (e.keyCode == 13) {
else if (e.keyCode === 13) {
if (searchItems.value.length > 0) {
const selectedItem = searchItems.value[selecedIdx.value]
window.open(selectedItem.menu_link, "_BLANK")
@ -281,19 +310,23 @@ function handdleKeyup(e: any) {
// 2秒后自动隐藏菜单
const hideMenu = () => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
menuShow.value = false
}, 2000)
}
const removeTimer = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
function handdleMenuItem(type: number) {
function handleMenuItem(type: number) {
menuShow.value = false
currentClickedItem.value = type
if (type == 4) {
if (type === 4) {
// 删除导航
if (!currentItem.value) return
$modal({
@ -302,9 +335,8 @@ function handdleMenuItem(type: number) {
cancelText: '取消',
submitText: '删除',
handdleSubmit: async () => {
const res = await $http.nav.deleteNav(currentItem.value.nid)
console.log('>>> --> handdleMenuItem --> res:', res)
if (res.code == 200) {
const res = await $http.nav.deleteNav(currentItem.value?.nid)
if (res.code === 200) {
$msg.success('删除成功')
getNavList()
}
@ -315,27 +347,26 @@ function handdleMenuItem(type: number) {
editModal.value = true
}
function handdleItemCancel() {
function handleItemCancel() {
editModal.value = false
editInput.value = ''
}
async function handdleItemSubmit() {
async function handleItemSubmit() {
if (!editInput.value) {
$msg.error('请输入修改内容')
return
}
let updateData: any = {}
if (currentClickedItem.value == 1) {
let updateData: Partial<NavItem> = {}
if (currentClickedItem.value === 1) {
updateData.menu_name = editInput.value
} else if (currentClickedItem.value == 2) {
} else if (currentClickedItem.value === 2) {
updateData.menu_link = editInput.value
} else if (currentClickedItem.value == 3) {
} 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) {
const res = await $http.nav.editNav(currentItem.value!.nid!, updateData)
if (res.code === 200) {
$msg.success('修改成功')
editModal.value = false
editInput.value = ''
@ -350,23 +381,30 @@ async function getNavList() {
return
}
const res = await $http.nav.getNavList()
res.data?.forEach((i: any) => {
i.icon_error = false
i.first = i.menu_name.at(0)
i.color = getRandomDarkColor()
});
navlist.value = res.data
if (!res.data) return
console.log("&&&&&&&&&&&&&&", navlist.value);
// 合并两个循环,提高性能
res.data.forEach((i: any) => {
if (!tagList.value.find((t: any) => t.name == i.tag))
tagList.value.push({ name: i.tag.substring(0, 4), color: getRandomDarkColor(30, 128), checked: false })
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: any) {
const tag = tagList.value.find((t: any) => t.name == item.tag)
function getItemColor(item: NavItem) {
const tag = tagList.value.find((t) => t.name === item.tag)
return tag ? tag.color : getRandomDarkColor(30, 128)
}
@ -453,6 +491,8 @@ function addNav() {
}
async function getIcon() {
console.log(11111);
if (!navData.menu_link) return
const res = await $http.mix.getIcon({
url: navData.menu_link
@ -463,9 +503,15 @@ async function getIcon() {
}
}
function resetNav() {
navData.menu_link = ''
navData.menu_name = ''
navData.tag = ''
navData.menu_icon = ''
}
function navCancel() {
visible.value = false
formNav.value.resetFields()
resetNav()
}
async function navSubmit() {
@ -481,14 +527,10 @@ async function navSubmit() {
if (res.code == 200) {
$msg.success('添加成功')
visible.value = false
formNav.value.resetFields()
getNavList()
}
// 清空navData
navData.menu_link = ''
navData.menu_name = ''
navData.tag = ''
navData.menu_icon = ''
resetNav()
}
// 处理右键菜单
function handdleContextMenu(event: MouseEvent, item: any) {
@ -503,7 +545,7 @@ function handdleContextMenu(event: MouseEvent, item: any) {
// 监听store的登录状态
watch(() => usrLog.isLogin, (newVal) => {
watch(() => usrLog.isLogin, (newVal: any) => {
console.log('********** --> watch --> newVal:', newVal)
if (newVal) {
getNavList()
@ -511,17 +553,97 @@ watch(() => usrLog.isLogin, (newVal) => {
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(() => {
console.log("&&&&&&&&&&&&&&", nav.navH);
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 - 20}px`
height: `${window.innerHeight - navcards.value.getBoundingClientRect().y}px`
}
tagList.value = [
{
@ -536,7 +658,7 @@ onMounted(() => {
height: `calc(100vh - ${nav.navH}px)`
}
navStyle.value = {
height: `${window.innerHeight - navcards.value.getBoundingClientRect().top - 20}px`
height: `${navHeight.value}px`
}
});
window.addEventListener('keyup', (e: Event) => {

View File

@ -1,22 +1,6 @@
<template>
<div class="widget-page">
<h1>工具页</h1>
<d-card class="w-1/3 bg-[#ffffff60] rounded-[10px]" shadow="hover">
<template #title>
<div class="flex items-center">
<icon-widget class="w-5 mr-2"></icon-widget>
视频工具
</div>
</template>
<template #content>
<div class="p-4">
<vue3VideoPlay class="w-full" title="钢铁侠" src="https://cdn.jsdelivr.net/gh/xdlumia/files/video-play/IronMan.mp4" />
</div>
</template>
</d-card>
</div>
</template>

View File

@ -1,21 +0,0 @@
<template>
<div>
这里是控制台首页
</div>
</template>
<script setup>
//mark import
//mark data
//mark method
//mark 周期、内置函数等
</script>
<style scoped lang="less">
</style>

11
typed-router.d.ts vendored
View File

@ -19,8 +19,7 @@ declare module 'vue-router/auto-routes' {
*/
export interface RouteNamedMap {
'appshare': RouteRecordInfo<'appshare', '/AppShare', Record<never, never>, Record<never, never>>,
'article': RouteRecordInfo<'article', '/Article', Record<never, never>, Record<never, never>>,
'/console/home': RouteRecordInfo<'/console/home', '/console/home', Record<never, never>, Record<never, never>>,
'blog': RouteRecordInfo<'blog', '/Blog', Record<never, never>, Record<never, never>>,
'gallery': RouteRecordInfo<'gallery', '/Gallery', Record<never, never>, Record<never, never>>,
'home': RouteRecordInfo<'home', '/Home', Record<never, never>, Record<never, never>>,
'404': RouteRecordInfo<'404', '/NotFound', Record<never, never>, Record<never, never>>,
@ -43,12 +42,8 @@ declare module 'vue-router/auto-routes' {
routes: 'appshare'
views: never
}
'src/views/Article.vue': {
routes: 'article'
views: never
}
'src/views/console/home.vue': {
routes: '/console/home'
'src/views/Blog.vue': {
routes: 'blog'
views: never
}
'src/views/Gallery.vue': {

View File

@ -3,10 +3,10 @@ import { fileURLToPath, URL } from "node:url";
import { resolve } from "path";
import UnoCSS from "unocss/vite";
import AutoImport from "unplugin-auto-import/vite";
import { DevUiResolver } from "unplugin-vue-components/resolvers";
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
import Components from "unplugin-vue-components/vite";
import { VueRouterAutoImports } from 'unplugin-vue-router';
import VueRouter from 'unplugin-vue-router/vite';
import { VueRouterAutoImports } from "unplugin-vue-router";
import VueRouter from "unplugin-vue-router/vite";
import { defineConfig } from "vite";
// import vueDevTools from "vite-plugin-vue-devtools";
import svgLoader from "vite-svg-loader";
@ -20,11 +20,18 @@ export default defineConfig({
}),
AutoImport({
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
imports: ["vue", "pinia", VueRouterAutoImports],
imports: [
"vue",
"pinia",
VueRouterAutoImports,
{
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
},
],
}),
Components({
resolvers: [DevUiResolver()],
resolvers: [NaiveUiResolver()],
dirs: ["src/components"],
}),
UnoCSS(),
@ -39,7 +46,7 @@ export default defineConfig({
base: "/blog/",
server: {
host: "0.0.0.0",
port: 8080,
port: 8989,
},
resolve: {
alias: {

439
yarn.lock
View File

@ -10,15 +10,6 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@angular/cdk@^18.0.0":
version "18.2.14"
resolved "https://registry.npmmirror.com/@angular/cdk/-/cdk-18.2.14.tgz#6114f37a77f2f182de482da2273c70f74a6cc52d"
integrity sha512-vDyOh1lwjfVk9OqoroZAP8pf3xxKUvyl+TVR8nJxL4c5fOfUFkD7l94HaanqKSRwJcI2xiztuu92IVoHn8T33Q==
dependencies:
tslib "^2.3.0"
optionalDependencies:
parse5 "^7.1.2"
"@antfu/install-pkg@^1.0.0":
version "1.1.0"
resolved "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz#78fa036be1a6081b5a77a5cf59f50c7752b6ba26"
@ -279,11 +270,6 @@
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
"@babel/plugin-syntax-typescript" "^7.27.1"
"@babel/runtime@^7.21.0":
version "7.28.2"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.2.tgz#2ae5a9d51cc583bd1f5673b3bb70d6d819682473"
integrity sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==
"@babel/template@^7.26.9", "@babel/template@^7.27.2":
version "7.27.2"
resolved "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
@ -306,15 +292,7 @@
"@babel/types" "^7.28.0"
debug "^4.3.1"
"@babel/types@^7.24.7", "@babel/types@^7.26.9", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.0", "@babel/types@^7.28.2":
version "7.28.2"
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@babel/types@^7.28.5":
"@babel/types@^7.24.7", "@babel/types@^7.28.5":
version "7.28.5"
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b"
integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==
@ -322,16 +300,49 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/types@^7.26.9", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.0", "@babel/types@^7.28.2":
version "7.28.2"
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@braintree/sanitize-url@^6.0.0":
version "6.0.4"
resolved "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783"
integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==
"@devui-design/icons@^1.3.0", "@devui-design/icons@^1.4.0":
"@css-render/plugin-bem@^0.15.14":
version "0.15.14"
resolved "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz#de13fc9f59299c2b646119851763dfa08929b3c1"
integrity sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==
"@css-render/vue3-ssr@^0.15.10", "@css-render/vue3-ssr@^0.15.14":
version "0.15.14"
resolved "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz#a2f4dedc3e86211a3ce1445555265095b7736491"
integrity sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==
"@ctrl/tinycolor@^3.4.1":
version "3.6.1"
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31"
integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==
"@devui-design/icons@^1.3.0":
version "1.4.0"
resolved "https://registry.npmmirror.com/@devui-design/icons/-/icons-1.4.0.tgz#41fe1ce82e1aa6b111ca30c6a553cfc4a073fd47"
integrity sha512-taAX1RNW0QHUqQTRPqLTYTB2PZIqUplhWeF4hcmWkSTjpWlDNI40DssG/WRb3sISkfBk/4BMUxxC5XeTL3jo7A==
"@element-plus/icons-vue@^2.3.2":
version "2.3.2"
resolved "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz#7e9cb231fb738b2056f33e22c3a29e214b538dcf"
integrity sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==
"@emotion/hash@~0.8.0":
version "0.8.0"
resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
"@esbuild/aix-ppc64@0.25.8":
version "0.25.8"
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz#a1414903bb38027382f85f03dda6065056757727"
@ -462,10 +473,10 @@
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz#610d7ea539d2fcdbe39237b5cc175eb2c4451f9c"
integrity sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==
"@floating-ui/core@^1.2.4":
version "1.7.2"
resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.2.tgz#3d1c35263950b314b6d5a72c8bfb9e3c1551aefd"
integrity sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==
"@floating-ui/core@^1.2.4", "@floating-ui/core@^1.7.3":
version "1.7.3"
resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7"
integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==
dependencies:
"@floating-ui/utils" "^0.2.10"
@ -476,6 +487,14 @@
dependencies:
"@floating-ui/core" "^1.2.4"
"@floating-ui/dom@^1.0.1":
version "1.7.4"
resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77"
integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==
dependencies:
"@floating-ui/core" "^1.7.3"
"@floating-ui/utils" "^0.2.10"
"@floating-ui/utils@^0.2.10":
version "0.2.10"
resolved "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
@ -550,6 +569,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@juggle/resize-observer@^3.3.1":
version "3.4.0"
resolved "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@meting/core@^1.5.13":
version "1.5.13"
resolved "https://registry.npmmirror.com/@meting/core/-/core-1.5.13.tgz#d5178b99124e5ba8e5cef36ca70d6e63d7426339"
@ -560,10 +584,10 @@
resolved "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1"
integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
"@popperjs/core@^2.5.4":
version "2.11.8"
resolved "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@popperjs/core@npm:@sxzz/popperjs-es@^2.11.7":
version "2.11.7"
resolved "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz#a7f69e3665d3da9b115f9e71671dae1b97e13671"
integrity sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==
"@quansync/fs@^0.1.1":
version "0.1.3"
@ -709,7 +733,12 @@
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/lodash-es@^4.17.4":
"@types/katex@^0.16.2":
version "0.16.7"
resolved "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz#03ab680ab4fa4fbc6cb46ecf987ecad5d8019868"
integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==
"@types/lodash-es@^4.17.12", "@types/lodash-es@^4.17.4":
version "4.17.12"
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
@ -721,6 +750,11 @@
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93"
integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==
"@types/lodash@^4.17.20":
version "4.17.21"
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.21.tgz#b806831543d696b14f8112db600ea9d3a1df6ea4"
integrity sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==
"@types/node@^22.16.5":
version "22.16.5"
resolved "https://registry.npmmirror.com/@types/node/-/node-22.16.5.tgz#cc46ac3994cd957000d0c11095a0b1dae2ea2368"
@ -740,6 +774,11 @@
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz#94e175b53623384bff1f354cdb3197a8d63cdbe5"
integrity sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==
"@types/web-bluetooth@^0.0.20":
version "0.0.20"
resolved "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==
"@unocss/astro@66.3.3":
version "66.3.3"
resolved "https://registry.npmmirror.com/@unocss/astro/-/astro-66.3.3.tgz#707d1df627158540bb91bde2b5f8452c073599f9"
@ -1380,12 +1419,12 @@
"@vue/compiler-ssr" "3.6.0-alpha.2"
"@vue/shared" "3.6.0-alpha.2"
"@vue/shared@3.5.18", "@vue/shared@^3.2.33", "@vue/shared@^3.5.0", "@vue/shared@^3.5.13":
"@vue/shared@3.5.18", "@vue/shared@^3.5.0", "@vue/shared@^3.5.13":
version "3.5.18"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.18.tgz#529f24a88d3ed678d50fd5c07455841fbe8ac95e"
integrity sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==
"@vue/shared@3.5.26":
"@vue/shared@3.5.26", "@vue/shared@^3.2.33":
version "3.5.26"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f"
integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==
@ -1410,11 +1449,33 @@
"@vueuse/shared" "8.9.4"
vue-demi "*"
"@vueuse/core@^10.11.0":
version "10.11.1"
resolved "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz#15d2c0b6448d2212235b23a7ba29c27173e0c2c6"
integrity sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==
dependencies:
"@types/web-bluetooth" "^0.0.20"
"@vueuse/metadata" "10.11.1"
"@vueuse/shared" "10.11.1"
vue-demi ">=0.14.8"
"@vueuse/metadata@10.11.1":
version "10.11.1"
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
integrity sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==
"@vueuse/metadata@8.9.4":
version "8.9.4"
resolved "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-8.9.4.tgz#a4132db33e4c1b1023636acfa20aa7b37ab3d978"
integrity sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==
"@vueuse/shared@10.11.1":
version "10.11.1"
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz#62b84e3118ae6e1f3ff38f4fbe71b0c5d0f10938"
integrity sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==
dependencies:
vue-demi ">=0.14.8"
"@vueuse/shared@8.9.4":
version "8.9.4"
resolved "https://registry.npmmirror.com/@vueuse/shared/-/shared-8.9.4.tgz#c9741c30ffb666b50d62f0dd80b76119fd47573e"
@ -1437,6 +1498,11 @@ alien-signals@^2.0.5:
resolved "https://registry.npmmirror.com/alien-signals/-/alien-signals-2.0.5.tgz#7528fc28de7cd76ccb6aad1d46d5b5635ce10805"
integrity sha512-PdJB6+06nUNAClInE3Dweq7/2xVAYM64vvvS1IHVHSJmgeOtEdrAGyp7Z2oJtYm0B342/Exd2NT0uMJaThcjLQ==
animate.css@^4.1.1:
version "4.1.1"
resolved "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz#614ec5a81131d7e4dc362a58143f7406abd68075"
integrity sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==
ansi-styles@^6.2.1:
version "6.2.1"
resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
@ -1485,7 +1551,7 @@ ast-walker-scope@^0.8.1:
"@babel/parser" "^7.28.3"
ast-kit "^2.1.2"
async-validator@^4.0.7:
async-validator@^4.0.7, async-validator@^4.2.5:
version "4.2.5"
resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
@ -1504,11 +1570,6 @@ axios@^1.11.0:
form-data "^4.0.4"
proxy-from-env "^1.1.0"
balanced-match@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
balloon-css@^0.5.0:
version "0.5.2"
resolved "https://registry.npmmirror.com/balloon-css/-/balloon-css-0.5.2.tgz#9e2163565a136c9d4aa20e8400772ce3b738d3ff"
@ -1682,6 +1743,14 @@ crypto-js@^4.2.0:
resolved "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
css-render@^0.15.10, css-render@^0.15.14:
version "0.15.14"
resolved "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz#c23d8c8b9c0b44cd20b426f5e9de7ef7bade69b8"
integrity sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==
dependencies:
"@emotion/hash" "~0.8.0"
csstype "~3.0.5"
css-select@^5.1.0:
version "5.2.2"
resolved "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e"
@ -1717,14 +1786,6 @@ css-tree@~2.2.0:
mdn-data "2.0.28"
source-map-js "^1.0.1"
css-vars-ponyfill@^2.3.2:
version "2.4.9"
resolved "https://registry.npmmirror.com/css-vars-ponyfill/-/css-vars-ponyfill-2.4.9.tgz#ad04f0ee5937f816616b7fae3cfa945a33e7bfd1"
integrity sha512-aZyLue5bdiGVNCiCclNjo123D8I7kyoYNUaAvz+H1JalX1ye4Ilz7jNRRH5YbM+dYD6ucejiydGwk7lol/GCXQ==
dependencies:
balanced-match "^1.0.2"
get-css-data "^2.0.2"
css-what@^6.1.0:
version "6.2.2"
resolved "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea"
@ -1752,6 +1813,11 @@ csstype@^3.2.3:
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
csstype@~3.0.5:
version "3.0.11"
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
version "1.2.4"
resolved "https://registry.npmmirror.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
@ -2262,17 +2328,20 @@ dagre@^0.8.5:
graphlib "^2.1.8"
lodash "^4.17.15"
date-fns@^2.20.0:
version "2.30.0"
resolved "https://registry.npmmirror.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
date-fns-tz@^3.2.0:
version "3.2.0"
resolved "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz#647dc56d38ac33a3e37b65e9d5c4cda5af5e58e6"
integrity sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==
dayjs@^1.11.3:
version "1.11.13"
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
date-fns@^4.1.0:
version "4.1.0"
resolved "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14"
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
dayjs@^1.11.19, dayjs@^1.11.3:
version "1.11.19"
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938"
integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==
de-indent@^1.0.2:
version "1.0.2"
@ -2326,6 +2395,11 @@ delegate@^3.1.2:
resolved "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
desandro-matches-selector@^2.0.0:
version "2.0.2"
resolved "https://registry.npmmirror.com/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz#717beed4dc13e7d8f3762f707a6d58a6774218e1"
integrity sha512-+1q0nXhdzg1IpIJdMKalUwvvskeKnYyEe3shPRwedNcWtnhEKT3ZxvFjzywHDeGcKViIxTCAoOYQWP1qD7VNyg==
destr@^2.0.3:
version "2.0.5"
resolved "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz#7d112ff1b925fb8d2079fac5bdb4a90973b51fdb"
@ -2336,11 +2410,6 @@ devui-theme@^0.0.1:
resolved "https://registry.npmmirror.com/devui-theme/-/devui-theme-0.0.1.tgz#9539023f0fdc1c90202bcfa00b0e945fd6bf1f78"
integrity sha512-5nF6fChlsXKeAtvkaAF4bZ0NMiEAbzwqQ9XZQiNuM0RRFz5lW29nnbhfDCNPkmnw5ZyCVyXqYwVRBIZrwZHXrA==
devui-theme@^0.0.7:
version "0.0.7"
resolved "https://registry.npmmirror.com/devui-theme/-/devui-theme-0.0.7.tgz#978a8a32aa7c097430350c586f0ba819e63e6a9f"
integrity sha512-sJWnkqOvqqVyvqaqyXn3/F+SIlNuoPUGdxG6TyDXuzZSAigWMuQsz8v8JJYTKL2Ash1q8kAKfRT01SqZmvlByA==
diff-match-patch@^1.0.5:
version "1.0.5"
resolved "https://registry.npmmirror.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
@ -2423,16 +2492,31 @@ electron-to-chromium@^1.5.173:
resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz#6dfc57a41846a57b18f9c0121821a6df1e165cc1"
integrity sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==
element-plus@^2.7.4:
version "2.13.0"
resolved "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.0.tgz#e2ae817c1ed4fa9e94456af69a0f88cd824ee0c7"
integrity sha512-qjxS+SBChvqCl6lU6ShiliLMN6WqFHiXQENYbAY3GKNflG+FS3jqn8JmQq0CBZq4koFqsi95NT1M6SL4whZfrA==
dependencies:
"@ctrl/tinycolor" "^3.4.1"
"@element-plus/icons-vue" "^2.3.2"
"@floating-ui/dom" "^1.0.1"
"@popperjs/core" "npm:@sxzz/popperjs-es@^2.11.7"
"@types/lodash" "^4.17.20"
"@types/lodash-es" "^4.17.12"
"@vueuse/core" "^10.11.0"
async-validator "^4.2.5"
dayjs "^1.11.19"
lodash "^4.17.21"
lodash-es "^4.17.21"
lodash-unified "^1.0.3"
memoize-one "^6.0.0"
normalize-wheel-es "^1.2.0"
entities@^4.2.0, entities@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
entities@^6.0.0:
version "6.0.1"
resolved "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694"
integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
entities@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/entities/-/entities-7.0.0.tgz#2ae4e443f3f17d152d3f5b0f79b932c1e59deb7a"
@ -2541,6 +2625,16 @@ estree-walker@^3.0.3:
dependencies:
"@types/estree" "^1.0.0"
ev-emitter@^1.0.0:
version "1.1.1"
resolved "https://registry.npmmirror.com/ev-emitter/-/ev-emitter-1.1.1.tgz#8f18b0ce5c76a5d18017f71c0a795c65b9138f2a"
integrity sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q==
evtd@^0.2.2, evtd@^0.2.4:
version "0.2.4"
resolved "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz#0aac39ba44d6926e6668948ac27618e0795b9d07"
integrity sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==
execa@^9.6.0:
version "9.6.0"
resolved "https://registry.npmmirror.com/execa/-/execa-9.6.0.tgz#38665530e54e2e018384108322f37f35ae74f3bc"
@ -2583,6 +2677,13 @@ fill-range@^7.1.1:
dependencies:
to-regex-range "^5.0.1"
fizzy-ui-utils@^2.0.0:
version "2.0.7"
resolved "https://registry.npmmirror.com/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz#7df45dcc4eb374a08b65d39bb9a4beedf7330505"
integrity sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==
dependencies:
desandro-matches-selector "^2.0.0"
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
@ -2623,11 +2724,6 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
get-css-data@^2.0.2:
version "2.1.1"
resolved "https://registry.npmmirror.com/get-css-data/-/get-css-data-2.1.1.tgz#be1ef413f5a0854b75967f3b694a6ce164557070"
integrity sha512-JpMa/f5P4mDXKg6l5/2cHL5xNY77Jap7tHyduMa6BF0E2a7bQ6Tvaz2BIMjeVYZYLcmOZ5w2Ro0yVJEI41tMbw==
get-intrinsic@^1.2.6:
version "1.3.0"
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
@ -2652,6 +2748,11 @@ get-proto@^1.0.1:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-size@^2.0.2:
version "2.0.3"
resolved "https://registry.npmmirror.com/get-size/-/get-size-2.0.3.tgz#54a1d0256b20ea7ac646516756202769941ad2ef"
integrity sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q==
get-stream@^9.0.0:
version "9.0.1"
resolved "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz#95157d21df8eb90d1647102b63039b1df60ebd27"
@ -2732,7 +2833,7 @@ highlight.js@11.9.0:
resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
highlight.js@^11.6.0:
highlight.js@^11.6.0, highlight.js@^11.8.0:
version "11.11.1"
resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz#fca06fa0e5aeecf6c4d437239135fabc15213585"
integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==
@ -2779,6 +2880,13 @@ image-size@~0.5.0:
resolved "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
imagesloaded@4.1.4:
version "4.1.4"
resolved "https://registry.npmmirror.com/imagesloaded/-/imagesloaded-4.1.4.tgz#1376efcd162bb768c34c3727ac89cc04051f3cc7"
integrity sha512-ltiBVcYpc/TYTF5nolkMNsnREHW+ICvfQ3Yla2Sgr71YFwQ86bDwV9hgpFhFtrGPuwEx5+LqOHIrdXBdoWwwsA==
dependencies:
ev-emitter "^1.0.0"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
@ -2893,9 +3001,9 @@ json5@^2.2.3:
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
version "6.2.0"
resolved "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62"
integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==
dependencies:
universalify "^2.0.0"
optionalDependencies:
@ -2951,10 +3059,15 @@ local-pkg@^1.0.0, local-pkg@^1.1.1:
pkg-types "^2.0.1"
quansync "^0.2.8"
lodash-es@^4.17.15, lodash-es@^4.17.20:
version "4.17.21"
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash-es@^4.17.20, lodash-es@^4.17.21:
version "4.17.22"
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.22.tgz#eb7d123ec2470d69b911abe34f85cb694849b346"
integrity sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==
lodash-unified@^1.0.3:
version "1.0.3"
resolved "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz#80b1eac10ed2eb02ed189f08614a29c27d07c894"
integrity sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==
lodash@^4.17.15, lodash@^4.17.21:
version "4.17.21"
@ -3018,6 +3131,14 @@ markdown-it@12.2.0:
mdurl "^1.0.1"
uc.micro "^1.0.5"
masonry-layout@^4.2.2:
version "4.2.2"
resolved "https://registry.npmmirror.com/masonry-layout/-/masonry-layout-4.2.2.tgz#d57b44af13e601bfcdc423f1dd8348b5524de348"
integrity sha512-iGtAlrpHNyxaR19CvKC3npnEcAwszXoyJiI8ARV2ePi7fmYhIud25MHK8Zx4P0LCC4d3TNO9+rFa1KoK1OEOaA==
dependencies:
get-size "^2.0.2"
outlayer "^2.1.0"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
@ -3043,6 +3164,11 @@ mdurl@^1.0.1:
resolved "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
memoize-one@^6.0.0:
version "6.0.0"
resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
memorystream@^0.3.1:
version "0.3.1"
resolved "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2"
@ -3125,6 +3251,31 @@ muggle-string@^0.4.1:
resolved "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
naive-ui@^2.43.2:
version "2.43.2"
resolved "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.43.2.tgz#101ded965257119090d4f9e6f4479a5a09fbafa1"
integrity sha512-YlLMnGrwGTOc+zMj90sG3ubaH5/7czsgLgGcjTLA981IUaz8r6t4WIujNt8r9PNr+dqv6XNEr0vxkARgPPjfBQ==
dependencies:
"@css-render/plugin-bem" "^0.15.14"
"@css-render/vue3-ssr" "^0.15.14"
"@types/katex" "^0.16.2"
"@types/lodash" "^4.17.20"
"@types/lodash-es" "^4.17.12"
async-validator "^4.2.5"
css-render "^0.15.14"
csstype "^3.1.3"
date-fns "^4.1.0"
date-fns-tz "^3.2.0"
evtd "^0.2.4"
highlight.js "^11.8.0"
lodash "^4.17.21"
lodash-es "^4.17.21"
seemly "^0.3.10"
treemate "^0.3.11"
vdirs "^0.1.8"
vooks "^0.2.12"
vueuc "^0.4.65"
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
@ -3143,18 +3294,6 @@ needle@^3.1.0:
iconv-lite "^0.6.3"
sax "^1.2.4"
ng-devui@^18.0.0:
version "18.0.0"
resolved "https://registry.npmmirror.com/ng-devui/-/ng-devui-18.0.0.tgz#48cf945f0da4dbf1fc69b9131b64194efbf3e4ef"
integrity sha512-QZzDqZ2cUF8w5VF65QASLIeE/2vpDr4iw9FCIbbFaQc4eLMb4lHRwRQFPyVatCpxe05k8qT2larICJbfJA6NJA==
dependencies:
"@angular/cdk" "^18.0.0"
"@popperjs/core" "^2.5.4"
css-vars-ponyfill "^2.3.2"
date-fns "^2.20.0"
lodash-es "^4.17.15"
tslib "^2.0.0"
node-fetch-native@^1.6.4:
version "1.6.6"
resolved "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz#ae1d0e537af35c2c0b0de81cbff37eedd410aa37"
@ -3177,6 +3316,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-wheel-es@^1.2.0:
version "1.2.0"
resolved "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz#0fa2593d619f7245a541652619105ab076acf09e"
integrity sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==
npm-normalize-package-bin@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz#df79e70cd0a113b77c02d1fe243c96b8e618acb1"
@ -3235,6 +3379,15 @@ open@^10.2.0:
is-inside-container "^1.0.0"
wsl-utils "^0.1.0"
outlayer@^2.1.0:
version "2.1.1"
resolved "https://registry.npmmirror.com/outlayer/-/outlayer-2.1.1.tgz#29863b6de10ea5dadfffcadfa0d728907387e9a2"
integrity sha512-+GplXsCQ3VrbGujAeHEzP9SXsBmJxzn/YdDSQZL0xqBmAWBmortu2Y9Gwdp9J0bgDQ8/YNIPMoBM13nTwZfAhw==
dependencies:
ev-emitter "^1.0.0"
fizzy-ui-utils "^2.0.0"
get-size "^2.0.2"
package-manager-detector@^1.3.0:
version "1.3.0"
resolved "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.3.0.tgz#b42d641c448826e03c2b354272456a771ce453c0"
@ -3250,13 +3403,6 @@ parse-node-version@^1.0.1:
resolved "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
parse5@^7.1.2:
version "7.3.0"
resolved "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05"
integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==
dependencies:
entities "^6.0.0"
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
@ -3469,6 +3615,11 @@ scule@^1.3.0:
resolved "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz#6efbd22fd0bb801bdcc585c89266a7d2daa8fbd3"
integrity sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==
seemly@^0.3.10, seemly@^0.3.6:
version "0.3.10"
resolved "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz#acf5d22c85ce855bf3715bbb740d18f544e59746"
integrity sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==
select@^1.1.2:
version "1.1.2"
resolved "https://registry.npmmirror.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
@ -3607,12 +3758,17 @@ totalist@^3.0.0:
resolved "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
treemate@^0.3.11:
version "0.3.11"
resolved "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz#7d52f8f69ab9ce326f8d139e0a3d1ffb25e48222"
integrity sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==
tslib@2.3.0:
version "2.3.0"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0:
tslib@^2.1.0, tslib@^2.3.0:
version "2.8.1"
resolved "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
@ -3780,6 +3936,18 @@ uuid@^9.0.1:
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
v-infinite-scroll@^1.0.4:
version "1.0.4"
resolved "https://registry.npmmirror.com/v-infinite-scroll/-/v-infinite-scroll-1.0.4.tgz#56aad33b3d89759abd23702d7960763aeb8e0fc7"
integrity sha512-o2PeC0LNh3nSpsak+tcJEPRj9ENWWZTFvp1JdsraOom/d7lEbj7QfsJuF56AyrkqdSszaAwiVq2h4xTayl3RtQ==
vdirs@^0.1.4, vdirs@^0.1.8:
version "0.1.8"
resolved "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz#a103bc43baca738f8dea912a7e9737154a19dbc2"
integrity sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==
dependencies:
evtd "^0.2.2"
vditor@^3.11.2:
version "3.11.2"
resolved "https://registry.npmmirror.com/vditor/-/vditor-3.11.2.tgz#612a405c74b71278a4eea188db3c7d93c0884c11"
@ -3787,6 +3955,11 @@ vditor@^3.11.2:
dependencies:
diff-match-patch "^1.0.5"
vfonts@^0.0.3:
version "0.0.3"
resolved "https://registry.npmmirror.com/vfonts/-/vfonts-0.0.3.tgz#999d66fecea18efee3f2b966c81101ae8ce01a29"
integrity sha512-nguyw8L6Un8eelg1vQ31vIU2ESxqid7EYmy8V+MDeMaHBqaRSkg3dTBToC1PR00D89UzS/SLkfYPnx0Wf23IQQ==
vite-dev-rpc@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz#a54be63cc4dbb127bce1360e4b12d9038087c204"
@ -3864,20 +4037,32 @@ vite@^7.0.6:
optionalDependencies:
fsevents "~2.3.3"
vooks@^0.2.12, vooks@^0.2.4:
version "0.2.12"
resolved "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz#2b6e23330b77bac81c7f7a344c4ca3e9f4f6c373"
integrity sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==
dependencies:
evtd "^0.2.2"
vscode-uri@^3.0.8:
version "3.1.0"
resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
vue-demi@*:
vue-demi@*, vue-demi@>=0.14.8:
version "0.14.10"
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
vue-devui@^1.6.33:
version "1.6.33"
resolved "https://registry.npmmirror.com/vue-devui/-/vue-devui-1.6.33.tgz#7fb3638a9da78c75c5584379b832bb26fbf8fa5f"
integrity sha512-HQI1OAPyqEKa1BX8jlcsGq1pL99mUYJmzQxAZ7sWy19wR+sSvvl2OWcMDwr24ngVCx5uEQA1cSvM8TYum5249A==
vue-demi@^0.13.2:
version "0.13.11"
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"
integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==
vue-devui@^1.6.35:
version "1.6.35"
resolved "https://registry.npmmirror.com/vue-devui/-/vue-devui-1.6.35.tgz#9d3602957c0fd4d4368b8920083ee7938b952272"
integrity sha512-vjyR7fny+j5PoX0gC/JobTZW9kxcILHUfkibgAj1LwU/LPWnV/1xFkbrCYiKt+OlE1ONT5nv0WlYaNqZzbMHIA==
dependencies:
"@babel/helper-hoist-variables" "^7.22.5"
"@devui-design/icons" "^1.3.0"
@ -3916,7 +4101,24 @@ vue-flow-layout@^0.1.1:
resolved "https://registry.npmmirror.com/vue-flow-layout/-/vue-flow-layout-0.1.1.tgz#4095d9e79b80e845f110d4d015de6faf2c71f735"
integrity sha512-JdgRRUVrN0Y2GosA0M68DEbKlXMqJ7FQgsK8CjQD2vxvNSqAU6PZEpi4cfcTVtfM2GVOMjHo7GKKLbXxOBqDqA==
vue-router@^4.0.3, vue-router@^4.5.1:
vue-masonry@^0.16.0:
version "0.16.0"
resolved "https://registry.npmmirror.com/vue-masonry/-/vue-masonry-0.16.0.tgz#e374fa34024cee664f6983bf672f417aa8cad2d1"
integrity sha512-0Wi/BH4iYYRrAAFY/wokczz0lWWPg1vmOvFW4aC2nKybKf6kVqUvFfR9/+izDfzG657/vY+Om0gqOB5YPxPxuQ==
dependencies:
imagesloaded "4.1.4"
masonry-layout "^4.2.2"
mitt "^3.0.0"
vue-demi "^0.13.2"
vue-router@^4.0.3:
version "4.6.4"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
dependencies:
"@vue/devtools-api" "^6.6.4"
vue-router@^4.5.1:
version "4.5.1"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz#47bffe2d3a5479d2886a9a244547a853aa0abf69"
integrity sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==
@ -3938,6 +4140,14 @@ vue3-cookies@^1.0.6:
dependencies:
vue "^3.0.0"
vue3-masonry-plus@^1.2.5:
version "1.2.5"
resolved "https://registry.npmmirror.com/vue3-masonry-plus/-/vue3-masonry-plus-1.2.5.tgz#72d2af86471746921ee3b497d2ba073d91825d2a"
integrity sha512-MU6zVeGsD8H1RIS+LWGjd9Yd7W68UwjJksO71p3Se+owTuj5zt08FkbkYH5chbd51FuVmdDpAh1nO1/F9Aop8A==
dependencies:
animate.css "^4.1.1"
element-plus "^2.7.4"
vue3-perfect-scrollbar@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/vue3-perfect-scrollbar/-/vue3-perfect-scrollbar-2.0.0.tgz#51cedf1090d731ec7cc8163067e348867de0aa69"
@ -3988,6 +4198,19 @@ vue@^3.6.0-alpha.2:
"@vue/server-renderer" "3.6.0-alpha.2"
"@vue/shared" "3.6.0-alpha.2"
vueuc@^0.4.65:
version "0.4.65"
resolved "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.65.tgz#12c53aa9c92c307f26f6291ab40cab84cdfbe004"
integrity sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==
dependencies:
"@css-render/vue3-ssr" "^0.15.10"
"@juggle/resize-observer" "^3.3.1"
css-render "^0.15.10"
evtd "^0.2.4"
seemly "^0.3.6"
vdirs "^0.1.4"
vooks "^0.2.4"
webpack-virtual-modules@^0.6.2:
version "0.6.2"
resolved "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"