Compare commits

...

16 Commits

Author SHA1 Message Date
4704fe81e9 feat: 添加AI摘要功能并优化博客详情页布局 2026-01-27 14:51:44 +08:00
ac4f8dac82 feat: 添加NProgress加载进度条和优化UI样式
添加NProgress依赖并集成到路由中,实现页面切换时的加载进度条
优化友链页面、博客分类和文章列表的UI样式
调整登录按钮和图片懒加载的显示效果
新增标签随机颜色功能,提升视觉体验
2026-01-15 17:17:40 +08:00
d00c18d38a feat: 添加博客主题样式文件 2026-01-13 09:44:37 +08:00
8a3cc98330 feat: 实现友链页面功能并优化其他页面
添加友链页面表单和展示功能,包括申请友链和显示友链列表
优化首页搜索框自动聚焦和画廊页面图片样式
修复博客列表分页器显示条件
2026-01-07 16:06:44 +08:00
e924fcca2b feat: 添加博客分类和分页功能
为博客页面添加分类筛选功能,支持按分类查看文章
实现分页功能,优化文章列表展示
调整页面布局和样式,增加交互效果
2026-01-01 16:43:18 +08:00
a9d3854a55 移除未使用的依赖和组件,优化博客列表和详情页样式,调整菜单项和颜色配置 2026-01-01 10:20:49 +08:00
d36526b368 添加返回顶部组件,优化博客详情页布局和样式,更新Markdown编辑器配置和依赖声明 2025-12-30 21:00:01 +08:00
79192df508 添加博客模块,支持Markdown渲染和文章详情页,优化图片懒加载和瀑布流布局 2025-12-30 14:31:04 +08:00
42dcf4195a 添加头像上传功能,优化滚动条样式,调整Gallery页面下载按钮样式,新增自动登录API 2025-12-29 15:53:12 +08:00
cec3974cf3 优化Gallery页面滚动加载和布局,调整菜单项命名,添加Apps页面,更新依赖声明 2025-12-29 10:14:38 +08:00
64b377b84d 优化Gallery页面布局和样式,调整瀑布流间距,修复图片阴影和圆角,显示下载按钮和分享者信息 2025-12-28 21:12:45 +08:00
589696c0ad 重构图片组件,使用naive-ui的NImage替换el-image,移除冗余依赖和样式 2025-12-28 21:03:29 +08:00
be088ab15b 删除dist目录下的静态资源文件和index.html 2025-12-28 16:48:46 +08:00
96edada2ff 迁移UI框架至naive-ui,重构组件和样式,添加Gallery和Mask组件 2025-12-28 16:45:58 +08:00
4c097e4c40 更新.gitignore配置,排除dist目录下所有文件 2025-12-25 17:05:36 +08:00
5e4ee2c1a1 优化搜索框交互,添加失焦处理逻辑,更新.gitignore配置 2025-12-25 17:04:51 +08:00
96 changed files with 6946 additions and 4400 deletions

3
.gitignore vendored
View File

@ -10,6 +10,9 @@ lerna-debug.log*
node_modules
.DS_Store
dist
dist/
dist/*
dist/*.*
dist-ssr
coverage
*.local

5
auto-imports.d.ts vendored
View File

@ -68,9 +68,12 @@ 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']

60
components.d.ts vendored
View File

@ -9,36 +9,46 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Aplayer: typeof import('./src/components/aplayer.vue')['default']
BackTop: typeof import('./src/components/backTop.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']
NBreadcrumb: typeof import('naive-ui')['NBreadcrumb']
NBreadcrumbItem: typeof import('naive-ui')['NBreadcrumbItem']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
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']
NImage: typeof import('naive-ui')['NImage']
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']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NPagination: typeof import('naive-ui')['NPagination']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NUpload: typeof import('naive-ui')['NUpload']
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']
}
}

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
dist/favicon.ico vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

75
dist/index.html vendored
View File

@ -1,75 +0,0 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/blog/favicon.ico">
<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-BdOUjYHC.js"></script>
<link rel="stylesheet" crossorigin href="/blog/assets/index-DjAfv_Co.css">
</head>
<body>
<div id="app"></div>
</body>
<script type="text/javascript">
! function (e, t, a) {
function r() {
for (var e = 0; e < s.length; e++) s[e].alpha <= 0 ? (t.body.removeChild(s[e].el), s.splice(e, 1)) : (s[
e].y--, s[e].scale += .004, s[e].alpha -= .013, s[e].el.style.cssText = "left:" + s[e].x +
"px;top:" + s[e].y + "px;opacity:" + s[e].alpha + ";transform:scale(" + s[e].scale + "," + s[e]
.scale + ") rotate(45deg);background:" + s[e].color + ";z-index:99999");
requestAnimationFrame(r)
}
function n() {
var t = "function" == typeof e.onclick && e.onclick;
e.onclick = function (e) {
t && t(), o(e)
}
}
function o(e) {
var a = t.createElement("div");
a.className = "heart", s.push({
el: a,
x: e.clientX - 5,
y: e.clientY - 5,
scale: 1,
alpha: 1,
color: c()
}), t.body.appendChild(a)
}
function i(e) {
var a = t.createElement("style");
a.type = "text/css";
try {
a.appendChild(t.createTextNode(e))
} catch (t) {
a.styleSheet.cssText = e
}
t.getElementsByTagName("head")[0].appendChild(a)
}
function c() {
return "rgb(" + ~~(255 * Math.random()) + "," + ~~(255 * Math.random()) + "," + ~~(255 * Math
.random()) + ")"
}
var s = [];
e.requestAnimationFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e
.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (e) {
setTimeout(e, 1e3 / 60)
}, i(
".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}"
), n(), r()
}(window, document);
</script>
</html>

7
extra.d.ts vendored
View File

@ -1,2 +1,7 @@
declare module "aplayer";
declare module 'vue3-video-play';
declare module "vue3-video-play";
declare module "vue3-masonry-plus";
declare module "vite";
declare module "vue-devui/tag";
declare module "es-toolkit";
declare module "nprogress";

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="这是柚子的网站,做一些分享类的功能"/>
<meta name="description" content="这是柚子的网站,做一些分享类的功能"/>
<title>柚子の网站</title>
</head>
@ -17,8 +17,6 @@
<script type="text/javascript">
! function (e, t, a) {
function r() {
for (var e = 0; e < s.length; e++) s[e].alpha <= 0 ? (t.body.removeChild(s[e].el), s.splice(e, 1)) : (s[
e].y--, s[e].scale += .004, s[e].alpha -= .013, s[e].el.style.cssText = "left:" + s[e].x +

View File

@ -8,47 +8,44 @@
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"b": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"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",
"md-editor-v3": "^6.3.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",
"vditor": "^3.11.2",
"vite-svg-loader": "^5.1.0",
"vue": "^3.6.0-alpha.2",
"vue-devui": "^1.6.33",
"vue-router": "^4.5.1",
"vue3-cookies": "^1.0.6",
"vue3-perfect-scrollbar": "^2.0.0",
"vue3-video-play": "^1.3.2"
"vue3-cookies": "^1.0.6"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@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",
"nprogress": "^0.2.0",
"typescript": "^5.9.3",
"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,34 @@
<template>
<div>
<PerfectScrollbar>
<n-config-provider :theme-overrides="themeOverrides">
<n-loading-bar-provider>
<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-loading-bar-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,113 @@
<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>
<back-top />
</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;

24
src/api/blog/index.ts Normal file
View File

@ -0,0 +1,24 @@
import request from "@/util/request";
//getBlogList
export function getBlogList(params: any) {
return request({
url: "/art",
method: "get",
params
});
}
//getBlogDetail
export function getBlogDetail(id: string) {
return request({
url: `/art/${id}`,
method: "get",
});
}
//getCateList
export function getCateList() {
return request({
url: `/art/category`,
method: "get",
});
}

View File

@ -24,3 +24,4 @@ export function putShare(data: Record<string, string>) {
data,
});
}

17
src/api/plink/index.ts Normal file
View File

@ -0,0 +1,17 @@
import request from "@/util/request";
// getPlinkList
export function getPlinkList() {
return request({
url: "/plink?sh=1",
method: "get",
});
}
// addPlink
export function addPlink(data: any) {
return request({
url: "/plink",
method: "post",
data
});
}

View File

@ -16,3 +16,10 @@ export function register(data: Record<string, string>) {
data,
});
}
// autologin
export function autologin() {
return request({
url: "/user/autologin",
method: "post",
});
}

View File

@ -7,12 +7,16 @@
font-weight: normal;
}
.ps__thumb-y {
background-color: #f6cbe7 !important;
.n-scrollbar-rail__scrollbar {
background-color: #f6cbe770 !important;
}
.devui-image-preview {
background-color: #00000090 !important;
img {
border-radius: 8px;
}
/* 添加渐变背景色 */
#nprogress .bar {
background: linear-gradient(to right, #ec66ab, #f78c6c, #7ed6df);
}
/* 添加平滑过渡效果 */
#nprogress .bar {
transition: width 0.2s ease-in-out;
}

View File

@ -1,6 +1,5 @@
@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";
@import 'md-editor-v3/lib/preview.css';
@import "nprogress/nprogress.css";
@import "./base.less";

2118
src/assets/myblog.less Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,60 @@
<template>
<div id="goTop">
<div class="icons" v-show="visiable" @click="handleScrollTop">
<n-icon>
<icon-backtop width="60" height="60" />
</n-icon>
</div>
</div>
</template>
<script setup lang="ts">
import { throttle } from 'es-toolkit/function';
const scrollTop: any = ref(null)
const visiable = ref(false)
const scrollElement: any = document.querySelector('.n-scrollbar .n-scrollbar-container')
const handleScroll = throttle(() => {
scrollTop.value = scrollElement.scrollTop
visiable.value = scrollTop.value > 200;
},1000)
function handleScrollTop() {
let timer: any = null;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(function fn() {
if (scrollTop.value > 0) {
scrollTop.value -= 50;
scrollElement.scrollTop = document.documentElement.scrollTop = scrollTop.value;
timer = requestAnimationFrame(fn);
} else {
cancelAnimationFrame(timer);
visiable.value = false;
}
});
}
onMounted(() => {
scrollElement?.addEventListener('scroll', handleScroll);
})
onBeforeMount(() => {
window.removeEventListener('scroll', handleScroll);
})
</script>
<style scoped lang="less">
.icons {
position: fixed;
right: 60px;
bottom: 40px;
border-radius: 50%;
z-index: 9999999;
}
:deep(.n-icon svg) {
width: 30px;
height: 30px;
}
</style>

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>
<icon-time class="w-5 mr-2 text-primary"></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>
<icon-news class="w-5 mr-2 text-primary"></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>
<icon-date class="w-5 mr-2 h-[20px]" ></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

@ -1,40 +1,19 @@
<template>
<div ref="nav" class="main-nav hidden lg:flex justify-between bg-white">
<!-- 网站Logo -->
<div class="px-5 items-centerflex" slot="brand" @click="goHome">
<div class="px-5 items-centerflex cursor-pointer w-1/5" slot="brand" @click="goHome">
<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 class="w-2/5">
<n-menu :icon-size="12" v-model:value="activeKey" class="" mode="horizontal" :options="menuOptions" />
</div>
<!-- 用户区域 -->
<div class="!text-[#ec66ab] flex items-center" @click="gotoHf">
<div class="!text-[#ec66ab] flex items-center cursor-pointer" @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>
@ -43,36 +22,23 @@
<span class="weather-info ml-4">{{ temp }}°C</span>
</div>
<div class="flex items-center mr-8">
<d-dropdown class="cursor-pointer w-[100px]" v-if="userinfo" trigger="hover">
<div class="flex items-center justify-end mr-8 w-1/20">
<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-button size="small" type="primary" @click="toLogin">登录</n-button>
</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,37 +46,58 @@
</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>
<div class="cursor-pointer ml-2 text-gray text-sm" @click="toLogin">登录</div>
<!-- <n-avatar round class="cursor-pointer" @click="toLogin"></n-avatar>
<div class="cursor-pointer ml-2 text-gray text-sm" @click="toLogin">登录</div> -->
<n-button type="primary" size="small" @click="toLogin">登录</n-button>
</div>
</div>
</div>
<!-- 修改头像弹窗 -->
<masked v-if="usrLog.isLogin" :visible="editModal" :setVisible="handdleItemCancel">
<div class="w-[500px] bg-white p-8 rounded-md">
<div class="text-center text-lg mb-8">修改头像</div>
<n-upload class="flex justify-center items-center" action="https://www.hxyouzi.com/api/user/avaupload"
:headers="{ Authorization: `Bearer ${token}` }" @finish="handleUpload" @before-upload="beforeUpload"
:show-file-list="false" accept="image/*">
<div class="flex flex-col justify-center items-center">
<n-avatar :size="80" :src="userinfo.ava_url" round class="cursor-pointer" alt="用户的头" />
<em class="cursor-pointer mt-4 text-gray text-sm">点击头像上传不超过3m的图片</em>
</div>
</n-upload>
<div class="mt-8 flex justify-between">
<n-button class="w-[98%]" secondary @click="handdleItemCancel">取消</n-button>
</div>
</div>
</masked>
</template>
<script setup lang="ts">
// 从@/icon/menu引入所有的svg文件
import logo from '@/assets/images/logo.png';
import artiSvg from '@/icon/menu/arti.svg';
import downSvg from '@/icon/menu/download.svg';
// import downSvg from '@/icon/menu/download.svg';
import homeSvg from '@/icon/menu/home.svg';
import linkSvg from '@/icon/menu/link.svg';
import picSvg from '@/icon/menu/pic.svg';
import settingSvg from '@/icon/menu/setting.svg';
// import settingSvg from '@/icon/menu/setting.svg';
import loginModal from '@/components/Login.vue'
import masked from '@/components/mask.vue'
import type { UploadFileInfo } from 'naive-ui'
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 +109,120 @@ 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: 'avatar',
label: '更换头像',
}
])
const editModal = ref<boolean>(false)
const token = ref<string>("")
function beforeUpload(data: { file: UploadFileInfo }) {
console.log('>>> --> beforeUpload --> data:', data.file.file?.size)
const size = data.file.file?.size || 4 * 1024 * 1024
if (size > 3 * 1024 * 1024) {
$msg.error('上传的图片大小不能超过3M')
return false
}
return true
}
function handleUpload(f: any) {
console.log('上传完成', JSON.parse(f.event.target.response));
const res = JSON.parse(f.event.target.response)
if (res.code === 200) {
// $msg.success(res.msg);
autoLogin()
} else {
$msg.error(res.msg);
}
editModal.value = false
}
async function autoLogin() {
const res = await $http.user.autologin()
if (res?.code !== 200) {
$msg.error('登录失败')
usrLog.setIsLogin(false)
$cookies.remove('token')
$cookies.remove('userinfo')
return
}
$cookies.set('token', res.data.token, '1d')
$cookies.set('userinfo', res.data.userinfo, '1d')
$msg.success(res.msg)
usrLog.setIsLogin(true)
userinfo.value = res.data.userinfo
}
function handdleItemCancel() {
editModal.value = false
}
function setVisible(v: any) {
visible.value = v
}
function handleSelect(key: string) {
console.log('>>> --> handleSelect --> key:', key)
if (key == 'logout') {
logout()
return
}
if (key == 'console') {
gotoConsole()
return
}
if (key == 'avatar') {
editModal.value = true
return
}
}
// 获取地理位置
const getLocation = () => {
if (!navigator.geolocation) {
@ -198,7 +298,7 @@ async function handdleJw(jw: string) {
}
watch(() => route.name, (newVal) => {
key.value = newVal as string
activeKey.value = newVal as string
})
function goHome() {
@ -220,6 +320,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,19 +333,22 @@ 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(); // 组件挂载时获取位置
const h: number = nav.value.clientHeight
if (h > 0) navx.setNavH(h)
token.value = $cookies.get('token')
});
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 +363,8 @@ 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(.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;
}
:deep(.n-menu-item-content__icon) {
width: 16px !important;
height: 16px !important;
}
</style>

View File

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

4
src/icon/arti.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z" clip-rule="evenodd" />
<path d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" />
</svg>

After

Width:  |  Height:  |  Size: 652 B

1
src/icon/author.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="1767228361426" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9819" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M789.504 890.368H319.488c-18.432 0-33.28 14.848-33.28 33.28s14.848 33.28 33.28 33.28h470.016c18.432 0 33.28-14.848 33.28-33.28s-14.848-33.28-33.28-33.28z" fill="#ec66ab" p-id="9820"></path><path d="M731.648 392.192c68.096-118.272 79.36-283.136 79.872-289.792 1.024-12.288-5.632-24.576-16.384-30.72-10.752-6.144-24.064-6.144-34.816 0.512-24.576 15.872-241.152 156.16-287.744 235.52-68.096 114.688-235.52 420.864-237.056 423.936-1.536 2.56-2.048 5.12-3.072 7.68l-91.648 158.72c-9.216 15.872-3.584 36.352 12.288 45.568 5.12 3.072 10.752 4.608 16.384 4.608 11.264 0 22.528-6.144 28.672-16.896L291.84 769.536c58.368-39.936 371.2-258.048 439.808-377.344z m-201.728-51.2c23.04-39.424 122.368-115.2 206.336-173.056-9.216 54.272-27.648 130.56-61.952 190.976-39.936 68.608-190.976 189.952-310.272 278.016 54.272-98.816 126.464-229.376 165.888-295.936z" fill="#ec66ab" p-id="9821"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1
src/icon/backtop.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="1767098232410" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11412" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M552.96 875.229867c0 37.034667-40.96 94.685867-40.96 94.685866s-40.96-61.44-40.96-94.685866a41.915733 41.915733 0 1 1 81.92-18.3808 43.349333 43.349333 0 0 1 0 18.3808m14.609067-441.326934a82.3808 82.3808 0 1 0-116.258134-5.256533l0.1024 0.1024a82.5344 82.5344 0 0 0 116.155734 5.12M799.914667 743.406933v15.36l-177.493334-35.9424c-11.434667 38.3488-57.326933 67.822933-111.018666 67.822934s-99.6352-29.474133-111.121067-69.051734L224.085333 756.053333v-15.36c0-87.04 24.2688-149.589333 79.172267-202.0864C284.125867 290.56 395.144533 133.12 504.951467 69.188267l6.3488-3.7376 6.3488 3.7376C627.5584 133.12 737.1776 290.56 719.325867 538.7776c56.32 52.394667 80.4864 115.080533 80.5888 204.629333" p-id="11413" fill="#ec66ab"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
</svg>
<?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="1767228343993" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8778" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M810.666667 170.666667h-134.4V149.333333c0-12.8-8.533333-21.333333-21.333334-21.333333h-42.666666c-12.8 0-21.333333 8.533333-21.333334 21.333333v21.333334h-170.666666V149.333333c0-12.8-8.533333-21.333333-21.333334-21.333333h-42.666666c-12.8 0-21.333333 8.533333-21.333334 21.333333v21.333334H213.333333c-46.933333 0-85.333333 38.4-85.333333 85.333333v554.666667c0 46.933333 38.4 85.333333 85.333333 85.333333h597.333334c46.933333 0 85.333333-38.4 85.333333-85.333333V256c0-46.933333-38.4-85.333333-85.333333-85.333333z m0 597.333333c0 23.466667-19.2 42.666667-42.666667 42.666667H256c-23.466667 0-42.666667-19.2-42.666667-42.666667V469.333333c0-23.466667 19.2-42.666667 42.666667-42.666666h512c23.466667 0 42.666667 19.2 42.666667 42.666666v298.666667z m0-448c0 12.8-8.533333 21.333333-21.333334 21.333333H234.666667c-12.8 0-21.333333-8.533333-21.333334-21.333333v-42.666667c0-12.8 8.533333-21.333333 21.333334-21.333333h100.266666v21.333333c0 12.8 8.533333 21.333333 21.333334 21.333334h42.666666c12.8 0 21.333333-8.533333 21.333334-21.333334v-21.333333h170.666666v21.333333c0 12.8 8.533333 21.333333 21.333334 21.333334h42.666666c12.8 0 21.333333-8.533333 21.333334-21.333334v-21.333333H789.333333c12.8 0 21.333333 8.533333 21.333334 21.333333v42.666667z" fill="#ec66ab" p-id="8779"></path><path d="M298.666667 503.466667h42.666666c12.8 0 21.333333 10.666667 21.333334 21.333333v42.666667c0 12.8-10.666667 21.333333-21.333334 21.333333h-42.666666c-12.8 0-21.333333-10.666667-21.333334-21.333333v-42.666667c0-10.666667 10.666667-21.333333 21.333334-21.333333z" fill="#ec66ab" p-id="8780"></path><path d="M298.666667 648.533333h42.666666c12.8 0 21.333333 10.666667 21.333334 21.333334v42.666666c0 12.8-10.666667 21.333333-21.333334 21.333334h-42.666666c-12.8 0-21.333333-10.666667-21.333334-21.333334v-42.666666c0-10.666667 10.666667-21.333333 21.333334-21.333334zM426.666667 503.466667h42.666666c12.8 0 21.333333 10.666667 21.333334 21.333333v42.666667c0 12.8-10.666667 21.333333-21.333334 21.333333h-42.666666c-12.8 0-21.333333-10.666667-21.333334-21.333333v-42.666667c0-10.666667 10.666667-21.333333 21.333334-21.333333z" fill="#ec66ab" p-id="8781"></path><path d="M554.666667 503.466667h42.666666c12.8 0 21.333333 10.666667 21.333334 21.333333v42.666667c0 12.8-10.666667 21.333333-21.333334 21.333333h-42.666666c-12.8 0-21.333333-10.666667-21.333334-21.333333v-42.666667c0-10.666667 10.666667-21.333333 21.333334-21.333333z" fill="#ec66ab" p-id="8782"></path><path d="M682.666667 503.466667h42.666666c12.8 0 21.333333 10.666667 21.333334 21.333333v42.666667c0 12.8-10.666667 21.333333-21.333334 21.333333h-42.666666c-12.8 0-21.333333-10.666667-21.333334-21.333333v-42.666667c0-10.666667 10.666667-21.333333 21.333334-21.333333z" fill="#ec66ab" p-id="8783"></path><path d="M426.666667 648.533333h42.666666c12.8 0 21.333333 10.666667 21.333334 21.333334v42.666666c0 12.8-10.666667 21.333333-21.333334 21.333334h-42.666666c-12.8 0-21.333333-10.666667-21.333334-21.333334v-42.666666c0-10.666667 10.666667-21.333333 21.333334-21.333334z" fill="#ec66ab" p-id="8784"></path><path d="M554.666667 648.533333h42.666666c12.8 0 21.333333 10.666667 21.333334 21.333334v42.666666c0 12.8-10.666667 21.333333-21.333334 21.333334h-42.666666c-12.8 0-21.333333-10.666667-21.333334-21.333334v-42.666666c0-10.666667 10.666667-21.333333 21.333334-21.333334z" fill="#ec66ab" p-id="8785"></path><path d="M682.666667 648.533333h42.666666c12.8 0 21.333333 10.666667 21.333334 21.333334v42.666666c0 12.8-10.666667 21.333333-21.333334 21.333334h-42.666666c-12.8 0-21.333333-10.666667-21.333334-21.333334v-42.666666c0-10.666667 10.666667-21.333333 21.333334-21.333334z" fill="#ec66ab" p-id="8786"></path></svg>

Before

Width:  |  Height:  |  Size: 754 B

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -2,3 +2,4 @@
<path fill-rule="evenodd" d="M9.75 6.75h-3a3 3 0 0 0-3 3v7.5a3 3 0 0 0 3 3h7.5a3 3 0 0 0 3-3v-7.5a3 3 0 0 0-3-3h-3V1.5a.75.75 0 0 0-1.5 0v5.25Zm0 0h1.5v5.69l1.72-1.72a.75.75 0 1 1 1.06 1.06l-3 3a.75.75 0 0 1-1.06 0l-3-3a.75.75 0 1 1 1.06-1.06l1.72 1.72V6.75Z" clip-rule="evenodd" />
<path d="M7.151 21.75a2.999 2.999 0 0 0 2.599 1.5h7.5a3 3 0 0 0 3-3v-7.5c0-1.11-.603-2.08-1.5-2.599v7.099a4.5 4.5 0 0 1-4.5 4.5H7.151Z" />
</svg>

Before

Width:  |  Height:  |  Size: 529 B

After

Width:  |  Height:  |  Size: 530 B

17
src/icon/loading.svg Normal file
View File

@ -0,0 +1,17 @@
<svg class="mx-1 size-5 animate-spin text-[#FFC0CB] duration-10000" viewBox="0 0 512 512" fill="currentColor"><g><path fill="currentColor" d="M330.555,101.873c0-34.164-13.767-65.108-36.047-87.622c-2.432-2.465-9.63-4.349-14.212,3.466
c-4.589,7.808-17.205,27.431-17.205,27.431c-1.534,2.438-4.219,3.918-7.089,3.918c-2.883,0-5.555-1.48-7.089-3.918
c0,0-12.623-19.623-17.205-27.431c-4.582-7.815-11.781-5.931-14.219-3.466c-22.274,22.514-36.04,53.458-36.04,87.622
c0,51.02,30.663,94.847,74.553,114.142C299.892,196.72,330.555,152.892,330.555,101.873z"></path><path fill="currentColor" d="M204.51,253.425c-4.78-47.705-36.998-90.409-85.518-106.176c-32.492-10.554-66.177-7.027-94.47,7.205
c-3.096,1.555-7.117,7.822-1.096,14.589c6.007,6.774,20.766,24.842,20.766,24.842c1.849,2.212,2.432,5.22,1.542,7.952
c-0.891,2.74-3.13,4.829-5.918,5.528c0,0-22.562,5.945-31.404,7.89c-8.849,1.945-9.281,9.37-7.692,12.445
c14.527,28.144,39.698,50.801,72.197,61.362C121.436,304.828,172.6,289.212,204.51,253.425z"></path><path fill="currentColor" d="M96.772,362.484c-20.082,27.643-27.136,60.766-22.342,92.074c0.527,3.424,5.233,9.178,13.527,5.554
c8.308-3.63,30.054-12.088,30.054-12.088c2.672-1.062,5.698-0.692,8.041,1c2.329,1.692,3.623,4.466,3.418,7.335
c0,0-1.308,23.294-2.199,32.315c-0.883,9.006,6.048,11.712,9.466,11.15c31.252-5.116,60.581-22.054,80.662-49.704
c29.993-41.266,30.944-94.758,6.781-136.155C177.332,303.773,126.758,321.204,96.772,362.484z"></path><path fill="currentColor" d="M287.824,313.964c-24.165,41.397-23.212,94.888,6.78,136.155c20.082,27.65,49.41,44.588,80.663,49.704
c3.418,0.562,10.349-2.144,9.466-11.15c-0.89-9.021-2.205-32.315-2.205-32.315c-0.198-2.869,1.102-5.643,3.431-7.335
c2.322-1.692,5.363-2.062,8.034-1c0,0,21.746,8.458,30.047,12.088c8.294,3.623,13.007-2.13,13.534-5.554
c4.787-31.308-2.254-64.43-22.342-92.074C385.246,321.204,334.672,303.766,287.824,313.964z"></path><path fill="currentColor" d="M503.6,215.254c-8.849-1.945-31.41-7.89-31.41-7.89c-2.795-0.706-5.027-2.788-5.918-5.528
c-0.89-2.732-0.308-5.74,1.534-7.952c0,0,14.76-18.068,20.78-24.842c6.014-6.766,1.993-13.034-1.096-14.589
c-28.301-14.232-61.978-17.76-94.485-7.205c-48.519,15.767-80.731,58.472-85.512,106.176
c31.91,35.787,83.067,51.403,131.593,35.636c32.492-10.561,57.67-33.218,72.191-61.362
C512.867,224.624,512.449,217.199,503.6,215.254z"></path></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

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/pen.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="1767229323466" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11396" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M531.4 114.8L414.1 365l97.9 70.1L627.2 512l87.2-68.3L826 313.4z" fill="#ec66ab" p-id="11397"></path><path d="M831 282.1L565.1 98.7c-10.7-7.4-24.1-9.6-36.7-5.9-12.6 3.7-22.7 12.7-27.8 24.7L381.8 399.6c-4.3 10.1-10.9 19.2-19.3 26.2l-87 73.2c-28.7 24.2-44.9 60.9-43.2 98.4l9.2 209.4-37.6 54.4c-11.7 2.2-22.4 10.2-27.3 23.1-4.8 12.6-1.7 27.5 7.6 37.2 6.6 6.9 14.9 10.4 23.2 11.2v0.3h448.4v-51.5H252.4l31.4-45.5 199-65.7c35.6-11.8 64.3-39.9 76.7-75.4L597 587.6c3.6-10.3 9.7-19.7 17.6-27.3l221.6-211.2c9.5-9 14.3-21.7 13.3-34.7-1-13.1-7.8-24.8-18.5-32.3zM548.4 570.6L510.9 678c-7.1 20.4-23.7 36.7-44.2 43.5l-134.4 44.4 131.8-191c11.7-2.2 22.4-10.2 27.3-23.1 4.8-12.6 1.7-27.5-7.6-37.2-16.5-17.3-43.5-14.4-56.3 4.2-5.6 8.2-7.4 17.8-5.8 26.9l-131.8 191-6.2-141.4c-0.9-21.6 8.4-42.8 24.9-56.8l87-73.2c14.6-12.3 26.2-28 33.6-45.6l4.8-11.5 151.5 108.7-6.5 6.2c-13.7 13.1-24.3 29.5-30.6 47.5z m75.1-89.9L454.6 359.5l89.6-212.7 249.2 172-169.9 161.9z" fill="#ec66ab" p-id="11398"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

11
src/icon/plink/ava.svg Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<clipPath id="clipPath9237879253">
<path d="M0 0L16 0L16 16L0 16L0 0Z" fill-rule="nonzero" transform="matrix(1 0 0 1 0 0)"/>
</clipPath>
</defs>
<g clip-path="url(#clipPath9237879253)">
<path d="M12 0L2 0C1.44797 0.000602881 0.976774 0.196078 0.586426 0.586426C0.196078 0.976774 0.000602802 1.44797 0 2L0 10C0.000602802 10.552 0.196078 11.0232 0.586426 11.4136C0.976774 11.8039 1.44797 11.9994 2 12L12 12C12.552 11.9994 13.0232 11.8039 13.4136 11.4136C13.8039 11.0232 13.9994 10.552 14 10L14 2C13.9994 1.44796 13.8039 0.976774 13.4136 0.586426C13.0232 0.196078 12.552 0.000602563 12 0ZM9.5 2C9.91421 2 10.2678 2.14645 10.5607 2.43934C10.8536 2.73223 11 3.08579 11 3.5C11 3.91421 10.8536 4.26777 10.5607 4.56066C10.2678 4.85355 9.91421 5 9.5 5C9.08579 5 8.73223 4.85355 8.43934 4.56066C8.14645 4.26777 8 3.91421 8 3.5C8.00043 3.08596 8.14703 2.73256 8.4398 2.4398C8.73256 2.14703 9.08596 2.00043 9.5 2ZM2 11C1.72386 11 1.48816 10.9024 1.29289 10.7071C1.09763 10.5118 1 10.2761 1 10L1 7.88656L3.96375 5.25219C4.26051 4.98899 4.60714 4.86312 5.00364 4.87457C5.40013 4.88602 5.73892 5.03168 6.02 5.31156L8.04969 7.33687L4.38656 11L2 11ZM13 10C13 10.2761 12.9024 10.5118 12.7071 10.7071C12.5118 10.9024 12.2761 11 12 11L5.80094 11L9.59531 7.20563C9.87268 6.96975 10.1934 6.85133 10.5575 6.85039C10.9216 6.84944 11.243 6.96619 11.5216 7.20063L13 8.4325L13 10Z" fill-rule="nonzero" transform="matrix(1 0 0 1 1 2)" fill="rgb(55, 65, 81)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

12
src/icon/plink/desc.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<clipPath id="clipPath9650034066">
<path d="M0 0L16 0L16 16L0 16L0 0Z" fill-rule="nonzero" transform="matrix(1 0 0 1 0 0)"/>
</clipPath>
</defs>
<g clip-path="url(#clipPath9650034066)">
<path d="M10.875 6L6.5 6C6.08579 6 5.73223 5.85355 5.43934 5.56066C5.14645 5.26777 5 4.91421 5 4.5L5 0.125C5 0.0904822 4.9878 0.0610193 4.96339 0.0366116C4.93898 0.0122039 4.90952 0 4.875 0L2 0C1.44772 0 0.976311 0.195262 0.585786 0.585786C0.195262 0.976311 0 1.44772 0 2L0 12C0 12.5523 0.195262 13.0237 0.585786 13.4142C0.976311 13.8047 1.44772 14 2 14L9 14C9.55228 14 10.0237 13.8047 10.4142 13.4142C10.8047 13.0237 11 12.5523 11 12L11 6.125C11 6.09048 10.9878 6.06102 10.9634 6.03661C10.939 6.0122 10.9095 6 10.875 6ZM8 11L3 11C2.86193 11 2.74408 10.9512 2.64645 10.8536C2.54882 10.7559 2.5 10.6381 2.5 10.5C2.5 10.3619 2.54882 10.2441 2.64645 10.1464C2.74408 10.0488 2.86193 10 3 10L8 10C8.13807 10 8.25592 10.0488 8.35355 10.1464C8.45118 10.2441 8.5 10.3619 8.5 10.5C8.5 10.6381 8.45118 10.7559 8.35355 10.8536C8.25592 10.9512 8.13807 11 8 11ZM8 8.5L3 8.5C2.86193 8.5 2.74408 8.45118 2.64645 8.35355C2.54882 8.25592 2.5 8.13807 2.5 8C2.5 7.86193 2.54882 7.74408 2.64645 7.64645C2.74408 7.54882 2.86193 7.5 3 7.5L8 7.5C8.13807 7.5 8.25592 7.54882 8.35355 7.64645C8.45118 7.74408 8.5 7.86193 8.5 8C8.5 8.13807 8.45118 8.25592 8.35355 8.35355C8.25592 8.45118 8.13807 8.5 8 8.5Z" fill-rule="nonzero" transform="matrix(1 0 0 1 2.5 1)" fill="rgb(55, 65, 81)"/>
<path d="M4.60063 4.51228L0.106563 0.0182223C0.097628 0.00934013 0.0869808 0.00367733 0.0746212 0.00123394C0.0622616 -0.00120946 0.0502605 -2.40803e-05 0.0386181 0.00479007C0.0269756 0.00960422 0.017642 0.0172406 0.0106173 0.0276991C0.00359249 0.0381577 5.34058e-05 0.0496862 0 0.0622848L0 4.11885C0 4.25692 0.0488154 4.37477 0.146446 4.4724C0.244077 4.57003 0.361928 4.61885 0.5 4.61885L4.55656 4.61885C4.56916 4.61879 4.58069 4.61526 4.59115 4.60823C4.60161 4.60121 4.60924 4.59187 4.61406 4.58023C4.61887 4.56859 4.62006 4.55659 4.61761 4.54423C4.61517 4.53187 4.60951 4.52122 4.60063 4.51228Z" fill-rule="nonzero" transform="matrix(1 0 0 1 8.5 1.38115)" fill="rgb(55, 65, 81)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

12
src/icon/plink/site.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<clipPath id="clipPath4437362061">
<path d="M0 0L16 0L16 16L0 16L0 0Z" fill-rule="nonzero" transform="matrix(1 0 0 1 0 0)"/>
</clipPath>
</defs>
<g clip-path="url(#clipPath4437362061)">
<path d="M10.875 6L6.5 6C6.08579 6 5.73223 5.85355 5.43934 5.56066C5.14645 5.26777 5 4.91421 5 4.5L5 0.125C5 0.0904822 4.9878 0.0610193 4.96339 0.0366116C4.93898 0.0122039 4.90952 0 4.875 0L2 0C1.44772 0 0.976311 0.195262 0.585786 0.585786C0.195262 0.976311 0 1.44772 0 2L0 12C0 12.5523 0.195262 13.0237 0.585786 13.4142C0.976311 13.8047 1.44772 14 2 14L9 14C9.55228 14 10.0237 13.8047 10.4142 13.4142C10.8047 13.0237 11 12.5523 11 12L11 6.125C11 6.09048 10.9878 6.06102 10.9634 6.03661C10.939 6.0122 10.9095 6 10.875 6ZM8 11L3 11C2.86193 11 2.74408 10.9512 2.64645 10.8536C2.54882 10.7559 2.5 10.6381 2.5 10.5C2.5 10.3619 2.54882 10.2441 2.64645 10.1464C2.74408 10.0488 2.86193 10 3 10L8 10C8.13807 10 8.25592 10.0488 8.35355 10.1464C8.45118 10.2441 8.5 10.3619 8.5 10.5C8.5 10.6381 8.45118 10.7559 8.35355 10.8536C8.25592 10.9512 8.13807 11 8 11ZM8 8.5L3 8.5C2.86193 8.5 2.74408 8.45118 2.64645 8.35355C2.54882 8.25592 2.5 8.13807 2.5 8C2.5 7.86193 2.54882 7.74408 2.64645 7.64645C2.74408 7.54882 2.86193 7.5 3 7.5L8 7.5C8.13807 7.5 8.25592 7.54882 8.35355 7.64645C8.45118 7.74408 8.5 7.86193 8.5 8C8.5 8.13807 8.45118 8.25592 8.35355 8.35355C8.25592 8.45118 8.13807 8.5 8 8.5Z" fill-rule="nonzero" transform="matrix(1 0 0 1 2.5 1)" fill="rgb(55, 65, 81)"/>
<path d="M4.60063 4.51228L0.106563 0.0182223C0.097628 0.00934013 0.0869808 0.00367733 0.0746212 0.00123394C0.0622616 -0.00120946 0.0502605 -2.40803e-05 0.0386181 0.00479007C0.0269756 0.00960422 0.017642 0.0172406 0.0106173 0.0276991C0.00359249 0.0381577 5.34058e-05 0.0496862 0 0.0622848L0 4.11885C0 4.25692 0.0488154 4.37477 0.146446 4.4724C0.244077 4.57003 0.361928 4.61885 0.5 4.61885L4.55656 4.61885C4.56916 4.61879 4.58069 4.61526 4.59115 4.60823C4.60161 4.60121 4.60924 4.59187 4.61406 4.58023C4.61887 4.56859 4.62006 4.55659 4.61761 4.54423C4.61517 4.53187 4.60951 4.52122 4.60063 4.51228Z" fill-rule="nonzero" transform="matrix(1 0 0 1 8.5 1.38115)" fill="rgb(55, 65, 81)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

11
src/icon/plink/tag.svg Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<clipPath id="clipPath5101496573">
<path d="M0 0L16 0L16 16L0 16L0 0Z" fill-rule="nonzero" transform="matrix(1 0 0 1 0 0)"/>
</clipPath>
</defs>
<g clip-path="url(#clipPath5101496573)">
<path d="M13.5939 0.412504C13.4631 0.280143 13.3117 0.178199 13.1399 0.106672C12.9681 0.0351444 12.7891 -0.00041159 12.603 3.57628e-06L8.76141 3.57628e-06C8.63541 0.000238498 8.51418 0.0244096 8.39773 0.0725168C8.28128 0.120624 8.17834 0.189057 8.08891 0.277816L0.409219 7.95594C0.136406 8.22931 0 8.55911 0 8.94532C0 9.33153 0.136406 9.66132 0.409219 9.93469L4.06547 13.5909C4.33891 13.8639 4.66881 14.0003 5.05516 14.0003C5.4415 14.0003 5.7714 13.8639 6.04484 13.5909L13.7214 5.91594C13.8103 5.82666 13.8789 5.72381 13.9272 5.6074C13.9755 5.49099 13.9998 5.36977 14.0002 5.24375L14.0002 1.4C14.0013 1.21486 13.9667 1.03667 13.8962 0.865449C13.8258 0.694224 13.725 0.543242 13.5939 0.412504ZM11.0002 4C10.724 4 10.4883 3.90237 10.293 3.70711C10.0978 3.51185 10.0002 3.27615 10.0002 3C10.0002 2.72386 10.0978 2.48816 10.293 2.2929C10.4883 2.09763 10.724 2 11.0002 2C11.2763 2 11.512 2.09763 11.7073 2.2929C11.9025 2.48816 12.0002 2.72386 12.0002 3C12.0002 3.27615 11.9025 3.51185 11.7073 3.70711C11.512 3.90237 11.2763 4 11.0002 4Z" fill-rule="nonzero" transform="matrix(1 0 0 1 0.999844 0.999996)" fill="rgb(55, 65, 81)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

12
src/icon/plink/uri.svg Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<clipPath id="clipPath6925244028">
<path d="M0 0L16 0L16 16L0 16L0 0Z" fill-rule="nonzero" transform="matrix(1 0 0 1 0 0)"/>
</clipPath>
</defs>
<g clip-path="url(#clipPath6925244028)">
<path d="M5.64616 8.58154C5.83085 7.93467 5.95303 7.1831 5.9996 6.46154C6.00177 6.42172 5.98905 6.38742 5.96145 6.35864C5.93384 6.32985 5.9001 6.31572 5.86022 6.31623L3.6396 6.31623C3.60137 6.31588 3.56861 6.3291 3.54134 6.35589C3.51406 6.38268 3.50025 6.41519 3.49991 6.45342L3.49991 8.18154C3.50031 8.21774 3.51295 8.24899 3.53781 8.27529C3.56268 8.3016 3.59317 8.31598 3.62928 8.31842C3.94218 8.3437 4.25274 8.3861 4.56096 8.4456C4.86919 8.5051 5.17321 8.58136 5.47303 8.67436C5.50975 8.6854 5.545 8.68186 5.57879 8.66374C5.61258 8.64563 5.63503 8.61823 5.64616 8.58154ZM5.10959 9.60717C4.87173 9.53772 4.63118 9.47927 4.38797 9.43183C4.14475 9.38439 3.89988 9.34815 3.65334 9.3231C3.61305 9.3186 3.57764 9.32969 3.54711 9.35636C3.51659 9.38303 3.50085 9.41664 3.49991 9.45717L3.49991 11.4959C3.50155 11.5492 3.52573 11.5886 3.57246 11.6142C3.61919 11.6397 3.66542 11.6389 3.71116 11.6115C4.20803 11.3219 4.61741 10.8353 4.98741 10.1919C5.04803 10.0869 5.14022 9.90592 5.19585 9.79529C5.21426 9.75735 5.21468 9.71922 5.1971 9.68088C5.17953 9.64255 5.15036 9.61797 5.10959 9.60717ZM2.35053 9.32873C2.1043 9.35215 1.85968 9.38688 1.61666 9.43292C1.37364 9.47895 1.13327 9.5361 0.895532 9.60435C0.813032 9.62811 0.778344 9.74185 0.815844 9.81654C0.871781 9.92779 0.940844 10.07 0.999907 10.1762C1.40616 10.895 1.83866 11.3481 2.28866 11.6115C2.33439 11.6389 2.38062 11.6397 2.42735 11.6142C2.47408 11.5886 2.49827 11.5492 2.49991 11.4959L2.49991 9.45623C2.49959 9.41669 2.48439 9.38408 2.45431 9.3584C2.42423 9.33273 2.38964 9.32283 2.35053 9.32873ZM2.36022 6.31623L0.139594 6.31623C0.0997814 6.31572 0.0660791 6.32982 0.0384874 6.35853C0.0108951 6.38723 -0.0018611 6.42147 0.000218868 6.46123C0.0464687 7.18186 0.168031 7.93498 0.352094 8.58123C0.363079 8.61845 0.385668 8.64623 0.419861 8.66458C0.454054 8.68293 0.489694 8.6864 0.526781 8.67498C0.826582 8.58198 1.1306 8.5058 1.43883 8.44645C1.74706 8.3871 2.05763 8.34495 2.37053 8.31998C2.40671 8.31761 2.43726 8.30327 2.4622 8.27696C2.48713 8.25064 2.49981 8.21936 2.50022 8.18311L2.50022 6.45373C2.49996 6.41538 2.48615 6.38277 2.45879 6.35589C2.43143 6.32902 2.39857 6.3158 2.36022 6.31623ZM3.65397 2.30935C3.90034 2.28627 4.14492 2.25086 4.38773 2.20314C4.63053 2.15542 4.87032 2.09561 5.1071 2.02373C5.14758 2.01279 5.17658 1.98826 5.19411 1.95017C5.21163 1.91207 5.21138 1.87409 5.19335 1.83623C5.13741 1.72467 5.05991 1.56498 5.00053 1.45842C4.61647 0.770917 4.18366 0.29123 3.71178 0.0209172C3.66604 -0.00616765 3.61986 -0.00694919 3.57323 0.0185726C3.5266 0.0440942 3.50237 0.0834173 3.50053 0.136542L3.50053 2.17498C3.50148 2.21553 3.51721 2.24916 3.54773 2.27588C3.57824 2.3026 3.61365 2.31376 3.65397 2.30935ZM3.6396 5.31623L5.86022 5.31623C5.89972 5.3169 5.93326 5.30308 5.96082 5.27477C5.98839 5.24647 6.00131 5.21258 5.9996 5.1731C5.95334 4.46154 5.83491 3.67935 5.65085 3.04123C5.63952 3.00433 5.61679 2.97687 5.58265 2.95885C5.54851 2.94083 5.51301 2.93756 5.47616 2.94904C4.88241 3.12842 4.26397 3.27498 3.6296 3.32404C3.59364 3.32608 3.56318 3.34009 3.53823 3.36606C3.51328 3.39204 3.5005 3.42303 3.49991 3.45904L3.49991 5.18123C3.50059 5.21914 3.51457 5.25127 3.54183 5.27762C3.56909 5.30397 3.60168 5.31684 3.6396 5.31623ZM2.28866 0.0212295C1.80678 0.289042 1.37709 0.784667 0.99272 1.47029C0.933032 1.57685 0.861782 1.72373 0.80522 1.83498C0.787316 1.87284 0.787115 1.91079 0.804617 1.94884C0.82212 1.98689 0.85107 2.01144 0.89147 2.02248C1.12815 2.09568 1.36803 2.15623 1.6111 2.20412C1.85417 2.25202 2.09908 2.28699 2.34584 2.30904C2.3861 2.31344 2.42148 2.30232 2.45198 2.27568C2.48247 2.24903 2.49824 2.21546 2.49928 2.17498L2.49928 0.136855C2.49712 0.0840894 2.4729 0.0450005 2.42661 0.0195878C2.38031 -0.00582512 2.33433 -0.00527803 2.28866 0.0212295ZM2.37022 3.32435C1.73584 3.27592 1.11678 3.12935 0.523657 2.94935C0.486801 2.93788 0.451304 2.94114 0.417164 2.95916C0.383024 2.97718 0.360293 3.00464 0.34897 3.04154C0.164907 3.67967 0.0464697 4.46185 0.000219822 5.17342C-0.00149584 5.21289 0.0114285 5.24678 0.0389929 5.27508C0.0665569 5.30339 0.100091 5.31721 0.139595 5.31654L2.36022 5.31654C2.39825 5.31715 2.43091 5.30421 2.4582 5.27771C2.48549 5.25121 2.49939 5.21895 2.49991 5.18092L2.49991 3.45935C2.49931 3.42334 2.48654 3.39235 2.46159 3.36638C2.43663 3.3404 2.40618 3.3264 2.37022 3.32435Z" fill-rule="nonzero" transform="matrix(1 0 0 1 5.00228 2.18377)" fill="rgb(55, 65, 81)"/>
<path d="M11.9739 2.07454C10.6091 0.696278 8.95682 0.00477179 7.01714 2.45571e-05C5.07745 -0.00472267 3.42182 0.678688 2.05026 2.05026C0.678688 3.42182 -0.00472267 5.07745 2.45571e-05 7.01714C0.00477179 8.95682 0.696278 10.6091 2.07454 11.9739C3.43938 13.3522 5.09164 14.0437 7.03133 14.0484C8.97101 14.0532 10.6266 13.3698 11.9982 11.9982C13.3698 10.6266 14.0532 8.97101 14.0484 7.03133C14.0437 5.09164 13.3522 3.43938 11.9739 2.07454ZM4.54298 12.4883C4.47258 12.3894 4.40486 12.2888 4.33981 12.1863C4.27477 12.0838 4.21249 11.9797 4.15298 11.8739C4.09048 11.7586 4.00173 11.5845 3.94267 11.4636C3.91402 11.4029 3.86759 11.3627 3.8034 11.343C3.73921 11.3233 3.67824 11.3306 3.62048 11.3649C3.48611 11.4402 3.30798 11.5436 3.17892 11.6283C3.04359 11.5254 2.91436 11.4154 2.79122 11.2982C2.66809 11.181 2.5518 11.0574 2.44236 10.9274C2.59371 10.8205 2.74867 10.7192 2.90726 10.6235C3.06585 10.5278 3.22765 10.4378 3.39267 10.3536C3.45079 10.3224 3.47923 10.2867 3.46079 10.223C3.33989 9.80453 3.24263 9.38066 3.16902 8.95135C3.0954 8.52205 3.04589 8.08999 3.02048 7.65517C3.01891 7.61907 3.00515 7.58844 2.97921 7.56328C2.95327 7.53813 2.92224 7.52532 2.88611 7.52486L1.11298 7.52486C1.0974 7.52503 1.08367 7.52007 1.0718 7.50998C1.05992 7.49989 1.05282 7.48714 1.05048 7.47173C1.04066 7.39757 1.03356 7.32314 1.02918 7.24846C1.0248 7.17378 1.02315 7.09903 1.02423 7.02423C1.02316 6.94957 1.02487 6.87498 1.02936 6.80045C1.03384 6.72592 1.04109 6.65166 1.05111 6.57767C1.05344 6.56226 1.06055 6.54951 1.07242 6.53942C1.0843 6.52933 1.09802 6.52437 1.11361 6.52454L2.88673 6.52454C2.95829 6.52454 3.01704 6.48329 3.02079 6.41111C3.04587 5.97809 3.09498 5.54782 3.16812 5.12029C3.24127 4.69276 3.33799 4.27064 3.45829 3.85392C3.46754 3.82254 3.46571 3.79182 3.45283 3.76175C3.43994 3.73169 3.41895 3.70918 3.38986 3.69423C3.22838 3.61149 3.06985 3.52346 2.91426 3.43012C2.75867 3.33679 2.60637 3.23837 2.45736 3.13486C2.56622 3.00483 2.68104 2.8803 2.80184 2.76128C2.92264 2.64225 3.04885 2.52928 3.18048 2.42236C3.30798 2.50611 3.47423 2.60079 3.60736 2.67579C3.66932 2.71102 3.73434 2.71805 3.8024 2.69689C3.87047 2.67573 3.92004 2.63307 3.95111 2.56892C4.00986 2.44798 4.07611 2.31329 4.14048 2.19767C4.20025 2.09058 4.26281 1.98515 4.32817 1.88138C4.39353 1.77761 4.46159 1.67564 4.53236 1.57548C4.92195 1.39194 5.327 1.25366 5.7475 1.16064C6.16799 1.06761 6.59357 1.02215 7.02423 1.02423C7.92861 1.02423 8.77111 1.24298 9.54079 1.60111C9.60875 1.69651 9.674 1.79373 9.73655 1.89277C9.7991 1.9918 9.85884 2.0925 9.91579 2.19486C9.99673 2.34048 10.0827 2.51892 10.1552 2.67361C10.1716 2.70837 10.1981 2.73174 10.2346 2.74372C10.2712 2.75569 10.3063 2.75253 10.3402 2.73423C10.5083 2.64329 10.6905 2.53861 10.8508 2.43329C10.9828 2.54127 11.1094 2.65529 11.2305 2.77535C11.3516 2.89542 11.4667 3.02098 11.5758 3.15204C11.4309 3.25256 11.2826 3.34795 11.1311 3.43821C10.9795 3.52847 10.8251 3.6134 10.6677 3.69298C10.6385 3.70795 10.6175 3.73051 10.6047 3.76066C10.5918 3.79082 10.5901 3.82159 10.5995 3.85298C10.7218 4.26687 10.8191 4.68654 10.8915 5.112C10.9639 5.53745 11.0109 5.9657 11.0324 6.39673C11.0341 6.43263 11.0479 6.46301 11.0739 6.48787C11.0998 6.51274 11.1308 6.52527 11.1667 6.52548L12.9361 6.52329C12.9517 6.52312 12.9654 6.52807 12.9773 6.53817C12.9892 6.54826 12.9963 6.56101 12.9986 6.57642C13.0168 6.72492 13.0259 6.87399 13.0259 7.02361C13.0259 7.17323 13.0168 7.32229 12.9986 7.47079C12.9964 7.48632 12.9894 7.49921 12.9775 7.50943C12.9656 7.51966 12.9518 7.5247 12.9361 7.52454L11.1652 7.52454C11.129 7.52493 11.0979 7.53771 11.072 7.56289C11.046 7.58806 11.0323 7.61872 11.0308 7.65486C11.0065 8.08508 10.9581 8.51259 10.8856 8.93737C10.8131 9.36215 10.717 9.78152 10.5974 10.1955C10.588 10.2272 10.5898 10.2583 10.6026 10.2888C10.6155 10.3193 10.6366 10.3422 10.6658 10.3577C10.822 10.4386 10.9961 10.5289 11.1461 10.6208C11.2961 10.7127 11.4445 10.8108 11.5877 10.9127C11.4794 11.0431 11.3652 11.1682 11.2452 11.2879C11.1251 11.4076 10.9997 11.5214 10.8689 11.6292C10.7927 11.5789 10.702 11.5217 10.6239 11.4749C10.5702 11.4436 10.4711 11.3883 10.4161 11.3574C10.2967 11.2905 10.1699 11.3405 10.1102 11.4636C10.0505 11.5867 9.95923 11.7633 9.89548 11.8777C9.83674 11.9828 9.77519 12.0863 9.71082 12.1881C9.64645 12.2899 9.57936 12.3898 9.50954 12.488C8.74017 12.8483 7.92861 13.0242 7.02423 13.0242C6.11986 13.0242 5.31267 12.8483 4.54298 12.4883Z" fill-rule="nonzero" transform="matrix(1 0 0 1 0.975769 0.975769)" fill="rgb(55, 65, 81)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

11
src/icon/plink/user.svg Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<clipPath id="clipPath7820171251">
<path d="M0 0L16 0L16 16L0 16L0 0Z" fill-rule="nonzero" transform="matrix(1 0 0 1 0 0)"/>
</clipPath>
</defs>
<g clip-path="url(#clipPath7820171251)">
<path d="M8.89518 1.01813C8.28705 0.361562 7.43768 0 6.50018 0C5.55768 0 4.70549 0.359375 4.10018 1.01188C3.4883 1.67156 3.19018 2.56813 3.26018 3.53625C3.39893 5.44625 4.85237 7 6.50018 7C8.14799 7 9.59893 5.44656 9.73987 3.53687C9.8108 2.5775 9.5108 1.68281 8.89518 1.01813ZM12.0002 14L1.00018 14C0.854387 14.0019 0.715258 13.9724 0.582793 13.9115C0.450327 13.8505 0.337372 13.7641 0.243929 13.6522C0.0408037 13.4094 -0.0410713 13.0778 0.0195537 12.7425C0.283304 11.2794 1.10643 10.0503 2.40018 9.1875C3.54955 8.42156 5.00549 8 6.50018 8C7.99487 8 9.4508 8.42188 10.6002 9.1875C11.8939 10.05 12.7171 11.2791 12.9808 12.7422C13.0414 13.0775 12.9596 13.4091 12.7564 13.6519C12.663 13.7639 12.5501 13.8503 12.4176 13.9113C12.2851 13.9723 12.146 14.0019 12.0002 14Z" fill-rule="nonzero" transform="matrix(1 0 0 1 1.49982 1)" fill="rgb(55, 65, 81)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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

3
src/icon/star.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M9 4.5a.75.75 0 0 1 .721.544l.813 2.846a3.75 3.75 0 0 0 2.576 2.576l2.846.813a.75.75 0 0 1 0 1.442l-2.846.813a3.75 3.75 0 0 0-2.576 2.576l-.813 2.846a.75.75 0 0 1-1.442 0l-.813-2.846a3.75 3.75 0 0 0-2.576-2.576l-2.846-.813a.75.75 0 0 1 0-1.442l2.846-.813A3.75 3.75 0 0 0 7.466 7.89l.813-2.846A.75.75 0 0 1 9 4.5ZM18 1.5a.75.75 0 0 1 .728.568l.258 1.036c.236.94.97 1.674 1.91 1.91l1.036.258a.75.75 0 0 1 0 1.456l-1.036.258c-.94.236-1.674.97-1.91 1.91l-.258 1.036a.75.75 0 0 1-1.456 0l-.258-1.036a2.625 2.625 0 0 0-1.91-1.91l-1.036-.258a.75.75 0 0 1 0-1.456l1.036-.258a2.625 2.625 0 0 0 1.91-1.91l.258-1.036A.75.75 0 0 1 18 1.5ZM16.5 15a.75.75 0 0 1 .712.513l.394 1.183c.15.447.5.799.948.948l1.183.395a.75.75 0 0 1 0 1.422l-1.183.395c-.447.15-.799.5-.948.948l-.395 1.183a.75.75 0 0 1-1.422 0l-.395-1.183a1.5 1.5 0 0 0-.948-.948l-1.183-.395a.75.75 0 0 1 0-1.422l1.183-.395c.447-.15.799-.5.948-.948l.395-1.183A.75.75 0 0 1 16.5 15Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
src/icon/tag.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="1767094212200" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5845" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M788.48 629.418667c0-17.066667-5.802667-31.744-17.749333-44.714667L421.205333 235.861333c-12.288-12.288-29.354667-22.869333-49.834667-31.744-21.162667-8.874667-39.936-12.970667-57.002667-12.970667L110.933333 191.146667c-17.066667 0-31.744 6.485333-44.032 18.773333-12.288 12.288-18.773333 26.965333-18.773333 44.032l0 202.752c0 17.066667 4.096 36.522667 12.970667 57.002667 8.874667 21.162667 18.773333 37.546667 31.744 49.834667l349.525333 349.525333c12.288 12.288 26.965333 17.749333 44.032 17.749333 17.066667 0 31.744-5.802667 44.714667-17.749333l239.616-239.616C781.994667 661.162667 788.48 646.485333 788.48 629.418667L788.48 629.418667 788.48 629.418667zM248.490667 392.192c-12.288 12.288-26.965333 18.090667-44.032 18.090667-17.066667 0-31.744-5.802667-44.032-18.090667-12.288-12.288-18.090667-26.965333-18.090667-44.032 0-17.066667 5.802667-31.744 18.090667-44.032 12.288-12.288 26.965333-18.090667 44.032-18.090667 17.066667 0 31.744 5.802667 44.032 18.090667C260.778667 316.416 266.24 331.093333 266.24 348.16 267.264 365.568 260.778667 379.904 248.490667 392.192L248.490667 392.192 248.490667 392.192zM958.122667 584.362667 608.597333 235.861333c-12.288-12.288-29.354667-22.869333-49.834667-31.744C537.6 195.242667 518.826667 191.146667 501.76 191.146667l-109.909333 0c17.066667 0 36.522667 4.096 57.002667 12.970667 21.162667 8.874667 37.546667 18.773333 49.834667 31.744l349.525333 348.501333c12.288 12.970667 18.090667 27.648 18.090667 44.714667s-5.802667 31.744-18.090667 44.032l-229.034667 229.717333c9.898667 9.898667 18.773333 17.066667 25.941333 21.845333 7.168 4.778667 17.066667 6.485333 28.672 6.485333 17.066667 0 31.744-5.802667 44.714667-18.090667l239.616-240.298667c12.288-12.288 17.749333-26.965333 17.749333-44.032S970.069333 597.674667 958.122667 584.362667L958.122667 584.362667 958.122667 584.362667zM958.122667 584.362667" fill="#ec66ab" p-id="5846"></path></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

3
src/icon/tag2.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M5.25 2.25a3 3 0 0 0-3 3v4.318a3 3 0 0 0 .879 2.121l9.58 9.581c.92.92 2.39 1.186 3.548.428a18.849 18.849 0 0 0 5.441-5.44c.758-1.16.492-2.629-.428-3.548l-9.58-9.581a3 3 0 0 0-2.122-.879H5.25ZM6.375 7.5a1.125 1.125 0 1 0 0-2.25 1.125 1.125 0 0 0 0 2.25Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 411 B

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,94 @@
<template>
<div class="image-container flex w-full">
<n-image ref="lazyRef" class="lazy__img" :src="url" @load="handleLoad" @error="handleError">
<template #placeholder>
<icon-loading class="zhuan text-primary w-12 h-12" />
</template>
<template #error>
<img :src="errorImg" alt="error" />
</template>
</n-image>
</div>
</template>
<script setup lang="ts">
import { inject, onMounted, ref, toRefs } from "vue";
import loadError from "../assets/loadError.png";
import loadingImg from "../assets/loading.gif";
const props = defineProps({
previewIcon: {
type: String,
default: "",
},
url: {
type: String,
default: "",
},
loading: {
type: String,
default: loadingImg,
},
errorImg: {
type: String,
default: loadError,
}
});
const { url, loading, errorImg } = toRefs(props);
const imgLoaded = inject("imgLoaded") as () => void;
const lazyRef = ref<any>(null);
const handleLoad = () => {
imgLoaded();
};
const handleError = () => {
// 可以在这里添加错误处理逻辑
};
onMounted(() => {
if (lazyRef.value) {
imgLoaded();
}
});
</script>
<style scoped>
:deep(.n-image img) {
width: 100%;
height: auto;
/* object-fit: cover; */
}
.lazy__img {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.lazy__img img[alt="loading"],
.lazy__img img[alt="error"] {
width: 100%;
height: auto;
padding: 1em;
margin: 0 auto;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.zhuan {
animation: spin 1.5s linear infinite;
}
</style>

View File

@ -0,0 +1,200 @@
<template>
<div ref="waterfallWrapper" class="waterfall-list" :style="{ height: `${wrapperHeight}px` }">
<div v-for="(item, index) in list" :key="getKey(item, index)" :style="{height:`${colWidth * item.height / item.width}px`}" class="waterfall-item">
<div class="waterfall-card h-full">
<slot name="item" :item="item" :index="index" :url="getRenderURL(item)" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useDebounceFn } from "@vueuse/core";
import type { PropType } from "vue";
import { provide, ref, watch } from "vue";
import type { ViewCard } from "../types/waterfall";
import { useCalculateCols, useLayout } from "../use";
import Lazy from "../utils/Lazy";
import { getValue } from "../utils/util";
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,31 +3,26 @@ 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 backTop from '@/components/backTop.vue';
import 'md-editor-v3/lib/style.css';
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);
// app.use(VMdPreview)
useConfig();
for (const key in icon) {
// console.log(key, icon[key]);
app.component("icon-" + key, icon[key] as any);
}
app.use(PerfectScrollbarPlugin);
app.component("back-top", backTop);
app.mount("#app");

View File

@ -1,6 +1,6 @@
import NProgress from 'nprogress';
import { createRouter, createWebHistory } from "vue-router";
import { routes } from 'vue-router/auto-routes';
import { routes } from "vue-router/auto-routes";
routes.unshift({
path: "/",
redirect: "/home",
@ -8,7 +8,17 @@ routes.unshift({
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes:routes as any
routes: routes as any,
});
// 前置守卫
router.beforeEach((to, from, next) => {
NProgress.start()
next();
});
// 后置守卫
router.afterEach(() => {
NProgress.done()
});
export default router;

View File

@ -98,7 +98,7 @@ export function deepclone<T>(obj: T): T {
* @example
* formatTimeBydate(new Date(), 'yyyy-MM-dd HH:mm:ss');
*/
export function formatTimeBydate(this: Date, f: string): string | undefined {
export function formatTimeBydate(this: string, f: string): string | undefined {
return formatTime(this, f);
}

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

@ -7,7 +7,7 @@
<script setup lang="ts">
definePage({
name:'appshare',
name:'apps',
meta: {
title: '软件分享',
}

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>

View File

@ -1,126 +1,74 @@
<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>
<n-scrollbar ref="virtualListInst" class="py-5" :style="boxStyle" @scroll="handleScroll">
<n-input class="my-4 !w-[72%] ml-[14%]" round size="large" v-model:value="kw" @keyup.enter="onSearch"
placeholder="请输入关键字">
<template #suffix>
<n-icon size="large">
<icon-search />
</n-icon>
</template>
</n-input>
<Waterfall class="ml-[10%] !w-[80%]" ref="waterfall" :list="fileList" :gutter="gutter" :columns="column"
img-selector="url" animation-effect="fadeIn" :animation-duration="1000" :animation-delay="300"
backgroundColor="transparent"> >
<template #item="{ item }">
<div
class="card h-full flex items-center justify-center rounded-md shadow-lg overflow-hidden group transition-transform duration-300 box-border hover:-translate-y-1.5">
<!-- <div class="image-wrapper"> -->
<LazyImg class="rounded-md overflow-hidden" :Pwidth="item.width" :Pheight="item.height" :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 px-2 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>
<div class="text-sm cursor-pointer text-[#f6cbe7] flex items-center" @click="downloadFile(item.filepath)">
<icon-download class="w-5 h-5" />
下载
</div>
</div>
</div>
</template>
</Waterfall>
</n-scrollbar>
</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: '画廊',
}
})
const waterfall = ref<any>(null);
const nav: any = $store.nav.useNavStore()
const boxStyle: any = ref({})
// 画廊页逻辑
const fileList = ref<any[]>([]);
const pn = ref(1);
const ps = ref(40);
const ps = ref(20);
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>(240);
const column = ref<number>(5);
const gutter = ref<number>(18);
// const uploadOptions = ref({
// uri: 'https://www.hxyouzi.com/api/files/upload',
// method: 'POST',
// maximumSize: 5 * 1024 * 1024,
// headers: {
// 'Authorization': 'Bearer ' + $cookies.get('token'),
// },
// });
const isLoadAll = ref<boolean>(false)
// 计算列数 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 + gutter.value) / (cwidth.value + gutter.value));
column.value = col > 0 ? col : 1;
waterfall.value?.renderer()
}
// 获取文件列表
@ -133,66 +81,44 @@ async function getFileList() {
page_num: pn.value,
page_size: ps.value,
});
// console.log('>>> --> getFileList --> res:', res);
// console.log('>>> --> getFileList --> res:', res);
if (res.data.length < ps.value) {
isLoadAll.value = true;
}
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;
if (isLoadAll.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;
// 当滚动到距离底部20%时加载更多
// console.log('>>> --> clientHeight --> clientHeight:', clientHeight)
if (scrollTop + clientHeight >= scrollHeight * 0.8) {
if (scrollTop + clientHeight >= scrollHeight - 100) {
console.log('>>> --> handleScroll --> 加载更多')
pn.value++;
getFileList();
@ -200,6 +126,8 @@ const handleScroll: any = throttle((e: any) => {
}, 1000)
function onSearch() {
pn.value = 1;
kw.value = kw.value.trim()
isLoadAll.value = false
getFileList();
}
@ -224,12 +152,14 @@ function downloadFile(url: string) {
}
onMounted(() => {
calculateColumns()
getFileList();
// 计算初始列数
calculateColumnCount();
// 添加窗口大小变化监听
window.addEventListener('resize', calculateColumnCount);
console.log('>>> --> nav.NavH:', nav.navH)
boxStyle.value = {
maxHeight: `calc(100vh - 5px - ${nav.navH}px)`,
}
// 添加滚动监听
window.addEventListener('resize', calculateColumns);
window.addEventListener('scroll', handleScroll);
});
@ -237,46 +167,14 @@ onUnmounted(() => {
pn.value = 1;
kw.value = '';
fileList.value = [];
resetWaterfall();
isLoadAll.value = false
// 移除监听
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%;
}
:deep(.n-image img) {
border-radius: 0.375rem !important;
}
</style>

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" placeholder="请输入">
<template #prepend>
<d-select class="w-48" size="lg" v-model="broswer" :options="options"></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" autofocus 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" trigger="none">
<div v-if="navlist.length" class="flex gap-6 px-2 mt-6 mb-3 flex-wrap lg:px-12">
<d-tag class="cursor-pointer truncate" hideBeyondTags v-for="tag in tagList" :checked="tag.checked"
:color="tag.color" @click="handdleTagClick(tag)">{{ tag.name }}</d-tag>
</div>
<!-- </PerfectScrollbar> -->
<!-- 图片网格展示区域 -->
<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,15 +239,22 @@ 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() {
setTimeout(() => {
searchBox.value = false
searchItems.value = []
selecedIdx.value = 0
}, 200)
}
function handdleInput() {
function handleInput() {
if (!searchWord.value) {
searchItems.value = []
searchBox.value = false
@ -227,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 {
@ -252,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 {
@ -260,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")
@ -274,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({
@ -295,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()
}
@ -308,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 = ''
@ -343,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)
}
@ -446,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
@ -456,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() {
@ -474,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) {
@ -496,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()
@ -504,17 +553,96 @@ 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 = [
{
@ -529,7 +657,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,20 +1,243 @@
<template>
<div class="plink-page">
<h1>友链页</h1>
<!-- 友链内容 -->
<div class="plink-page pt-4 flex justify-between" :style="boxStyle">
<div class="left ml-4 ">
<n-card class="card w-100 shadow">
<template #header>
<div class="flex items-center gap-4 text-primary text-xl font-bold">
<icon-loading class="zhuan text-primary w-6 h-6" />
申请友链
</div>
</template>
<div>
<n-form :model="plinkData" ref="formRef" :rules="rules" label-width="80" :inline="false"
feedback-style="font-size: 12px">
<n-form-item path="name">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<user-icon></user-icon>
昵称
</div>
</template>
<n-input v-model:value="plinkData.name" placeholder="请输入你的昵称" />
</n-form-item>
<n-form-item path="title">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<site-icon></site-icon>
网站名称
</div>
</template>
<n-input v-model:value="plinkData.title" placeholder="请输入你的网站名称" />
</n-form-item>
<n-form-item path="url">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<url-icon></url-icon>
站点地址
</div>
</template>
<n-input v-model:value="plinkData.url" placeholder="请输入你的网站地址" />
</n-form-item>
<n-form-item path="avater">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<ava-icon></ava-icon>
头像链接
</div>
</template>
<n-input v-model:value="plinkData.avater" placeholder="请输入你的头像链接" />
</n-form-item>
<n-form-item path="desc">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<desc-icon></desc-icon>
站点介绍
</div>
</template>
<n-input type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" maxlength="50"
v-model:value="plinkData.desc" placeholder="请用一句话简短的描述你的站点" />
</n-form-item>
<n-form-item path="tagname">
<template #label>
<div class="flex items-center gap-1 text-sm text-gray-500">
<tag-icon></tag-icon>
标签
</div>
</template>
<n-input type="textarea" :autosize="{ minRows: 2, maxRows: 3 }" maxlength="50"
v-model:value="plinkData.tagname" placeholder="输入你的标签用逗号隔开注意每个标签最多3个字符且只显示前3个标签哦~" />
</n-form-item>
<n-form-item>
<n-button class="w-full" type="primary" @click="submitPut">申请友链</n-button>
</n-form-item>
</n-form>
</div>
</n-card>
<div class="mt-4 text-right text-primary text-sm font-italic">申请完不显示是因为需要审核奥~~</div>
</div>
<n-scrollbar class="h-full">
<div class="right h-full grid grid-cols-3 gap-5 flex-1 px-12">
<div
class="rounded-lg bg-white shadow flex flex-col items-center justify-center p-4 cursor-pointer transition-transform duration-300 box-border hover:-translate-y-1.5"
v-for="p in pList" :key="p.pid" @click="gotoWebsite(p.url)">
<div class="ava">
<n-avatar round size="large" :src="p.avater"></n-avatar>
</div>
<em class="bg-[#fbf2e3] px-4 rounded-full text-sm text-primary mb-4">{{ p.name }}</em>
<div class="font-bold text-xl text-gray-700 mb-1">
{{ p.title }}
</div>
<div class="flex gap-1 truncate mb-1 text-sm">
<div v-for="i in getTags(p.tagname)" :key="i.tid"
:style="{ backgroundColor: getDictValue(tagColorList, 'tag', i, 'color') }"
class="flex items-center gap-1 cursor-pointer text-sm text-white rounded-full px-3">
<icon-tag2 class="w-3 h-3 " />{{ i }}
</div>
</div>
<div class="truncate w-full text-gray-400 text-sm text-center">
{{ p.desc }}
</div>
</div>
</div>
</n-scrollbar>
</div>
</template>
<script setup lang="ts">
import avaIcon from '@/icon/plink/ava.svg';
import descIcon from '@/icon/plink/desc.svg';
import siteIcon from '@/icon/plink/site.svg';
import tagIcon from '@/icon/plink/tag.svg';
import urlIcon from '@/icon/plink/uri.svg';
import userIcon from '@/icon/plink/user.svg';
import { getDictValue } from '@/util';
definePage({
name:'plink',
name: 'plink',
meta: {
title: '友链',
}
})
// 友链页逻辑
const boxStyle: any = ref({})
const nav: any = $store.nav.useNavStore()
const plinkData = reactive<any>({
name: '',
title: '',
avater: '',
url: '',
desc: '',
tagname: '',
});
const rules: any = {
name: [{ required: true, message: '昵称不能为空~~', trigger: ['blur'] }],
title: [{ required: true, message: '网站名称不能为空~~', trigger: ['blur'] }],
url: [{ required: true, message: '站点地址不能为空~~', trigger: ['blur'] }],
avater: [{ required: true, message: '头像链接不能为空~~', trigger: ['blur'] }],
desc: [{ required: true, message: '站点介绍不能为空~~', trigger: ['blur'] }],
tagname: [{ required: true, message: '标签不能为空~~', trigger: ['blur'] }],
}
const pList = ref<any>([])
const formRef = ref<any>(null)
const tagColorList: any = ref([])
async function getPlinkList() {
const res = await $http.plink.getPlinkList()
const list: Array<string> = []
// 对res.data.tags进行去重
res.data.forEach((i: any) => {
const t = getTags(i.tagname)
t.forEach((ii: string) => {
if (!list.includes(ii)) list.push(ii)
})
});
tagColorList.value = list.map((i: string) => {
return {
tag: i,
color: getRandomFromPalette()
}
})
pList.value = res.data
}
function getTags(str: any) {
str = str.replaceAll('', ',')
let tags = str.split(',').slice(0, 3)
tags.forEach((tag: any, idx: number) => {
tags[idx] = tag.trim().slice(0, 4)
});
return tags
}
function gotoWebsite(url: string) {
window.open(url, '_blank')
}
function submitPut() {
formRef.value?.validate(async (valid: boolean) => {
if (valid) {
$msg.error('请完善表单~~')
} else {
const res = await $http.plink.addPlink(plinkData)
if (res?.code !== 200) {
$msg.error(res.msg)
return
}
$msg.success(res.msg)
plinkData.name = ''
plinkData.title = ''
plinkData.avater = ''
plinkData.url = ''
plinkData.desc = ''
plinkData.tagname = ''
getPlinkList()
}
})
}
// 生成一个随机的鲜艳颜色,和白色能对比
function getRandomFromPalette(): string {
const hue = Math.floor(Math.random() * 360);
return hslToHex(hue, 80, 50);
}
function hslToHex(h: number, s: number, l: number): any {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
}
return `#${f(0)}${f(8)}${f(4)}`;
}
onMounted(() => {
boxStyle.value = {
height: window.innerHeight - nav.navH - 1 + 'px',
}
getPlinkList()
})
</script>
<style scoped>
/* 友链页样式 */
/* 无限旋转动画 */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.zhuan {
animation: spin 1.5s linear infinite;
}
:deep(.n-form-item-feedback__line) {
text-align: right;
font-size: 12px !important;
}
</style>

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>

221
src/views/blog/[bid].vue Normal file
View File

@ -0,0 +1,221 @@
<template>
<div class="flex justify-between">
<div class="left w-[20%] shadow">
<div class="fixed top-20 left-8">
<div class="text-center text-3xl text-bold mb-2">目录</div>
<MdCatalog :editorId="blogData.aid" class="my-cata" :scrollElement="scrollElement" />
</div>
</div>
<div class="flex-1">
<n-breadcrumb class="m-2" separator="|">
<n-breadcrumb-item href="/blog/blog">
文章
</n-breadcrumb-item>
<n-breadcrumb-item>
{{ blogData.title }}
</n-breadcrumb-item>
</n-breadcrumb>
<div class="w-full px-1/20">
<!-- <div class="text-center text-[40px] font-bold text-black` mt-10">{{ blogData.title }}</div> -->
<div class="my-4 bg-white shadow-lg p-4 rounded-md">
<n-collapse @item-header-click="AISum" arrow-placement="right">
<n-collapse-item>
<template #arrow>
<div class=" text-primary ml-2">
<!-- <icon-star class="w-6" /> -->
</div>
</template>
<template #header>
<div class="text-primary flex">
<icon-star class="w-6 mx-2" />
<div class="text-center text-[20px] font-bold">AI 摘要</div>
</div>
</template>
<template #header-extra>
<div class="text-primary flex">
<icon-star class="w-6 mx-2" />
</div>
</template>
<div class="indent-lg text-gray-500">
{{ msg.replaceAll('\\n', '<br>') }}
</div>
</n-collapse-item>
</n-collapse>
</div>
<div class="flex gap-2 justify-center my-4">
<em class="text-primary">
{{ blogData.nickname }}</em>
<em class="time">{{ formatTime(blogData.updated_at, "YYYY年MM月DD日hh时") }}</em>
</div>
<div class="flex flex-wrap gap-4 justify-center my-4">
<div v-for="item in tags" :key="item.tid" class="flex items-center gap-1 text-primary cursor-pointer">
<n-icon><icon-tag /></n-icon>
{{ item }}
</div>
</div>
<MdPreview ref="mdp" theme="light" class="relative " :editorId="blogData.aid" previewTheme="my"
:modelValue="markdown" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePage({
name: 'blog/:bid',
path: '/blog/:bid',
meta: {
title: '文章详情',
}
})
import { formatTime } from '@/util';
import type { ExposeParam } from 'md-editor-v3';
import { MdCatalog, MdPreview } from 'md-editor-v3';
const editorRef = ref<ExposeParam>();
const scrollElement: HTMLElement = document.querySelector('.n-scrollbar .n-scrollbar-container') as HTMLElement;
const tags = ref<any[]>([])
const route: any = useRoute()
const msg = ref<any>('...')
const aimask = ref(false)
const markdown = ref('');
const blogData = ref<any>({
aid: 'preview-only',
updated_at: Date.now()
})
async function getBlogDetail() {
const bid: any = route.params?.bid || 0
// console.log('>>> --> getBlogDetail --> bid:', route, bid)
const res = await $http.blog.getBlogDetail(bid)
console.log('>>> --> getBlogDetail --> res:', res.data)
blogData.value = res.data
let ts = res.data.tags
ts = ts.replaceAll('', ',')
tags.value = ts.split(',')
editorRef.value?.toggleCatalog(true)
markdown.value = res.data.cont
// AISum()
}
// ai总结
async function AISum() {
console.log('>>> --> AISum --> AISum:', AISum)
if (aimask.value) return
const response = await fetch('https://api.siliconflow.cn/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer sk-jwwmhmxsjtseyekknqmamlvzmrkmwfvuacnssbwfufogrkdg'
},
body: JSON.stringify({
model: "Qwen/Qwen3-8B",
messages: [{ role: 'user', content: "用一段话对以下内容做一个摘要(语气可爱带表情):" + blogData.value.cont }],
stream: true
})
})
console.log('>>> --> AISum --> response:', response)
const reader = response.body?.getReader()
const decoder = new TextDecoder('utf-8')
if (!reader) return
msg.value = ''
let done = false
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
const chunk = decoder.decode(value)
const match = chunk.match(/"content":"(.*?)"/)
if (match && match[1]) {
msg.value += match[1]
aimask.value = true
}
}
// const data = await response.json()
// console.log('>>> --> AISum --> data:', data)
}
onMounted(() => {
getBlogDetail()
})
</script>
<style lang="less">
@import "@/assets/myblog.less";
.my-cata {
* {
cursor: pointer;
}
.md-editor-catalog-indicator {
display: none;
}
.md-editor-catalog-active>span {
padding: 5px 15px !important;
border-radius: 30px;
color: white;
background-color: @primary;
position: relative;
&::after {
content: "✦";
position: static !important;
transform: none !important;
margin-left: 12px !important;
flex-shrink: 0;
display: inline-block !important;
line-height: 1;
font-size: 14px;
// font-family: Arial, sans-serif;
color: rgba(255, 255, 255, 0.9);
z-index: 10;
animation: outline-twinkle 1.5s infinite alternate;
}
}
.md-editor-catalog-link {
padding-block: 1px !important;
}
.md-editor-catalog-wrapper span {
padding: 5px 15px !important;
transition: all 0.6s ease-in-out;
&:hover {
padding: 5px 15px !important;
border-radius: 30px;
color: white;
background-color: @primary;
}
}
}
</style>
<style scoped lang="less">
/* 文章页样式 */
// :deep(.md img) {
// width: auto !important;
// height: 200px !important;
// }
// :deep(.md-editor-catalog-active>span) {
// color: @primary;
// }
// :deep(.md-editor-catalog-indicator) {
// background-color: @primary;
// }
// :deep(.md-editor-catalog-link span):hover {
// color: @primary;
// }</style>

184
src/views/blog/index.vue Normal file
View File

@ -0,0 +1,184 @@
<template>
<div class="article-page w-full flex" :style="boxStyle">
<div class="w-1/4 shadow relative">
<div class="card w-2/3 absolute top-16 left-1/6">
<div @click="clickCate(i, idx)" v-for="(i, idx) in cateList" :key="i.cid"
:class="{ '!bg-primary text-white': idx == currentCateIdx }"
class="relative cate flex my-6 py-6 px-8 justify-between text-xl font-bold bg-white shadow rounded cursor-pointer">
<div class="flex items-center">
<icon-arti class="w-5 h-5 text-primary mr-3" :class="{ 'text-white': idx == currentCateIdx }"></icon-arti>
<span>{{ i.name }}</span>
</div>
<div class="bg-primary px-4 rounded-full text-white ext-sm flex items-center gap-2">
{{ i.total || 0 }}
</div>
</div>
</div>
</div>
<div class="blog-list w-3/4 mt-10">
<div v-show="blogList.length == 0" class="ml-50 text-gray-500 mt-20">
什么都没有呢~ ~
</div>
<div v-for="item in blogList" :key="item.aid" class="rounded-lg p-4 w-[80%] ml-1/10 my-8 shadow cursor-pointer"
@click="$router.push(`/blog/${item.aid}`)">
<div class="flex justify-right gap-4 mb-2 text-gray-600">
<em class="flex items-center gap-1 text-sm">
<n-icon><icon-date /></n-icon>
{{ formatTime(item.updated_at, 'YYYY年MM月DD日') }}
</em>
<em class="flex items-center gap-1 text-sm">
<n-icon><icon-pen /></n-icon>
{{ getSize(item.cont) }}
</em>
<em class="flex items-center gap-1 text-sm ">
<n-icon><icon-author /></n-icon>
{{ item.nickname }}</em>
</div>
<div class="text-xl font-bold text-primary mb-2">{{ item.title }}</div>
<div class="text-sm text-gray-500 mb-4 line-clamp-3">{{ item.pro }}</div>
<div class="flex gap-4">
<div v-for="i in getTags(item.tags)" :key="i.tid"
:style="{ backgroundColor: getDictValue(tagColorList, 'tag', i, 'color') }"
class="flex items-center gap-1 cursor-pointer text-white rounded-full px-3">
<icon-tag2 class="w-4 h-4" />{{ i }}
</div>
</div>
</div>
<div v-show="blogList.length > page_size" class="mt-20 w-[80%] ml-1/10 cursor-pointer flex justify-center">
<n-pagination v-model:page="page_num" :page-count="total" :page-slot="5" @update:page="getBlogList" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { formatTime, getDictValue } from '@/util'
definePage({
name: 'blog',
meta: {
title: '文章',
}
})
const blogList = ref<any[]>([])
const boxStyle = ref({})
const nav: any = $store.nav.useNavStore()
const cateList = ref<any[]>([])
const currentCateIdx = ref(-1)
const page_size = ref(5)
const page_num = ref(1)
const total = ref(0)
const category = ref('')
const tagColorList: any = ref([])
function clickCate(item: any, idx: number) {
if (currentCateIdx.value == idx) {
currentCateIdx.value = -1
category.value = ''
getBlogList()
total.value = cateList.value.reduce((acc: any, cur: any) => acc + cur.total, 0)
return
}
currentCateIdx.value = idx
category.value = item.cid
getBlogList()
total.value = item.total
console.log('>>> --> clickCate --> total.value:', total.value)
}
async function getBlogList() {
const res = await $http.blog.getBlogList({
category: category.value, page_size: page_size.value, page_num: page_num.value
})
const list: Array<string> = []
// 对res.data.tags进行去重
res.data.forEach((i: any) => {
const t = getTags(i.tags)
t.forEach((ii: string) => {
if (!list.includes(ii)) list.push(ii)
})
});
tagColorList.value = list.map((i: string) => {
return {
tag: i,
color: getRandomFromPalette()
}
})
blogList.value = res.data
}
// 计算markdown的文字数量
function getSize(str: string) {
const reg = /[\u0000-\u0009\u000B-\u001F\u007F-\u009F\u00AD\u0600-\u0604\u070F\u17B4\u17B5\u200B-\u200D\u3000]/g;
return str.replace(reg, '').length;
}
// 生成一个随机的鲜艳颜色,和白色能对比
function getRandomFromPalette(): string {
const hue = Math.floor(Math.random() * 360);
return hslToHex(hue, 80, 50);
}
function hslToHex(h: number, s: number, l: number): any {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
}
return `#${f(0)}${f(8)}${f(4)}`;
}
function getTags(str: any) {
str = str.replaceAll('', ',')
let tags = str.split(',').slice(0, 5)
tags.forEach((tag: any, idx: number) => {
tags[idx] = tag.trim().slice(0, 4)
});
return tags
}
async function getCateList() {
const res = await $http.blog.getCateList()
console.log('>>> --> getCateList --> res:', res.data)
cateList.value = res.data
total.value = cateList.value.reduce((acc: any, cur: any) => acc + cur.total, 0)
}
onMounted(() => {
boxStyle.value = {
height: `calc(100vh - ${nav.navH + 1}px)`,
}
getBlogList()
getCateList()
})
</script>
<style scoped>
/* 文章页样式 */
@keyframes fangda {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.cate:hover {
animation: fangda 01s ease-in-out 1;
}
</style>

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>

View File

@ -11,6 +11,7 @@
],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"target": "es2021",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types":["unplugin-vue-router/client"],
"paths": {

View File

@ -11,7 +11,8 @@
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2021",
"lib": ["es2021.string", "dom"],
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]

18
typed-router.d.ts vendored
View File

@ -18,9 +18,9 @@ declare module 'vue-router/auto-routes' {
* Route name map generated by unplugin-vue-router
*/
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>>,
'apps': RouteRecordInfo<'apps', '/Apps', Record<never, never>, Record<never, never>>,
'blog': RouteRecordInfo<'blog', '/blog', Record<never, never>, Record<never, never>>,
'blog/:bid': RouteRecordInfo<'blog/:bid', '/blog/:bid', { bid: ParamValue<true> }, { bid: ParamValue<false> }>,
'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>>,
@ -39,16 +39,16 @@ declare module 'vue-router/auto-routes' {
* @internal
*/
export interface _RouteFileInfoMap {
'src/views/AppShare.vue': {
routes: 'appshare'
'src/views/Apps.vue': {
routes: 'apps'
views: never
}
'src/views/Article.vue': {
routes: 'article'
'src/views/blog/index.vue': {
routes: 'blog'
views: never
}
'src/views/console/home.vue': {
routes: '/console/home'
'src/views/blog/[bid].vue': {
routes: 'blog/:bid'
views: never
}
'src/views/Gallery.vue': {

View File

@ -5,6 +5,7 @@ export default defineConfig({
theme: {
colors: {
primary: "#ec66ab",
deepp: "#a7415d"
},
},
});

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: {

3132
yarn.lock

File diff suppressed because it is too large Load Diff