Compare commits
14 Commits
4c097e4c40
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4704fe81e9 | |||
| ac4f8dac82 | |||
| d00c18d38a | |||
| 8a3cc98330 | |||
| e924fcca2b | |||
| a9d3854a55 | |||
| d36526b368 | |||
| 79192df508 | |||
| 42dcf4195a | |||
| cec3974cf3 | |||
| 64b377b84d | |||
| 589696c0ad | |||
| be088ab15b | |||
| 96edada2ff |
5
auto-imports.d.ts
vendored
@ -68,9 +68,12 @@ declare global {
|
|||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
const useCssModule: typeof import('vue')['useCssModule']
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
const useCssVars: typeof import('vue')['useCssVars']
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useDialog: typeof import('naive-ui')['useDialog']
|
||||||
const useId: typeof import('vue')['useId']
|
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 useModel: typeof import('vue')['useModel']
|
||||||
|
const useNotification: typeof import('naive-ui')['useNotification']
|
||||||
const useRoute: typeof import('vue-router')['useRoute']
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
const useRouter: typeof import('vue-router')['useRouter']
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
|||||||
60
components.d.ts
vendored
@ -9,36 +9,46 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
Aplayer: typeof import('./src/components/aplayer.vue')['default']
|
Aplayer: typeof import('./src/components/aplayer.vue')['default']
|
||||||
|
BackTop: typeof import('./src/components/backTop.vue')['default']
|
||||||
ContextMenu: typeof import('./src/components/contextMenu.vue')['default']
|
ContextMenu: typeof import('./src/components/contextMenu.vue')['default']
|
||||||
DAside: typeof import('vue-devui/layout/index.es.js')['Aside']
|
Gallery: typeof import('./src/components/Gallery.vue')['default']
|
||||||
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']
|
|
||||||
HomeSide: typeof import('./src/components/homeSide.vue')['default']
|
HomeSide: typeof import('./src/components/homeSide.vue')['default']
|
||||||
Login: typeof import('./src/components/Login.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']
|
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']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
export interface GlobalDirectives {
|
|
||||||
vImagePreview: typeof import('vue-devui/image-preview/index.es.js')['ImagePreviewDirective']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
0
dist/assets/AppShare-tn0RQdqM.css
vendored
26
dist/assets/Article-Cll2DzoB.css
vendored
1
dist/assets/Gallery-BWGJDDfX.css
vendored
BIN
dist/assets/codicon-SZR-f31-.ttf
vendored
BIN
dist/assets/devui-icomoon-BHLqMzSu.woff
vendored
BIN
dist/assets/devui-icomoon-BeYkSg23.eot
vendored
BIN
dist/assets/devui-icomoon-C-SlqI79.ttf
vendored
2196
dist/assets/devui-icomoon-NjuehwAK.svg
vendored
|
Before Width: | Height: | Size: 1.8 MiB |
BIN
dist/assets/logo-DPam0bOR.png
vendored
|
Before Width: | Height: | Size: 201 KiB |
BIN
dist/assets/offwork-gFqEdesJ.png
vendored
|
Before Width: | Height: | Size: 586 KiB |
BIN
dist/assets/onwork-DoTs-gez.png
vendored
|
Before Width: | Height: | Size: 741 KiB |
BIN
dist/assets/qweather-icons-Cp7-xKdf.woff
vendored
BIN
dist/assets/qweather-icons-nP_EPzX8.ttf
vendored
BIN
dist/assets/qweather-icons-pUVm4Tbi.woff2
vendored
BIN
dist/favicon.ico
vendored
|
Before Width: | Height: | Size: 3.2 KiB |
75
dist/index.html
vendored
@ -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-CIE_IpA5.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/blog/assets/index-DYCSKHr7.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
@ -1,2 +1,7 @@
|
|||||||
declare module "aplayer";
|
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";
|
||||||
@ -5,7 +5,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="这是柚子的网站,做一些分享类的功能"/>
|
<meta name="description" content="这是柚子的网站,做一些分享类的功能"/>
|
||||||
<title>柚子の网站</title>
|
<title>柚子の网站</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -17,8 +17,6 @@
|
|||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
! function (e, t, a) {
|
! function (e, t, a) {
|
||||||
|
|
||||||
|
|
||||||
function r() {
|
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[
|
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 +
|
e].y--, s[e].scale += .004, s[e].alpha -= .013, s[e].el.style.cssText = "left:" + s[e].x +
|
||||||
|
|||||||
19
package.json
@ -8,47 +8,44 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"b": "run-p type-check \"build-only {@}\" --",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build"
|
"type-check": "vue-tsc --build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@devui-design/icons": "^1.4.0",
|
|
||||||
"@imengyu/vue3-context-menu": "^1.5.2",
|
"@imengyu/vue3-context-menu": "^1.5.2",
|
||||||
"@meting/core": "^1.5.13",
|
"@meting/core": "^1.5.13",
|
||||||
"@unocss/reset": "^66.3.3",
|
"@unocss/reset": "^66.3.3",
|
||||||
"aplayer": "^1.10.1",
|
"aplayer": "^1.10.1",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"devui-theme": "^0.0.7",
|
|
||||||
"es-toolkit": "^1.39.8",
|
"es-toolkit": "^1.39.8",
|
||||||
"less": "^4.4.0",
|
"less": "^4.4.0",
|
||||||
"ng-devui": "^18.0.0",
|
"md-editor-v3": "^6.3.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"qweather-icons": "^1.7.0",
|
"qweather-icons": "^1.7.0",
|
||||||
"unocss": "^66.3.3",
|
"unocss": "^66.3.3",
|
||||||
"unplugin-auto-import": "^19.3.0",
|
"unplugin-auto-import": "^19.3.0",
|
||||||
"unplugin-vue-components": "^28.8.0",
|
"unplugin-vue-components": "^28.8.0",
|
||||||
"unplugin-vue-router": "^0.15.0",
|
"unplugin-vue-router": "^0.15.0",
|
||||||
"vditor": "^3.11.2",
|
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vite-svg-loader": "^5.1.0",
|
||||||
"vue": "^3.6.0-alpha.2",
|
"vue": "^3.6.0-alpha.2",
|
||||||
"vue-devui": "^1.6.33",
|
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vue3-cookies": "^1.0.6",
|
"vue3-cookies": "^1.0.6"
|
||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
|
||||||
"vue3-video-play": "^1.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.7.0",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"naive-ui": "^2.43.2",
|
||||||
"npm-run-all2": "^8.0.4",
|
"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": "^7.0.6",
|
||||||
"vite-plugin-vue-devtools": "^8.0.0",
|
"vite-plugin-vue-devtools": "^8.0.0",
|
||||||
|
"vue-devui": "^1.6.35",
|
||||||
"vue-tsc": "^3.0.4"
|
"vue-tsc": "^3.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
src/App.vue
@ -1,71 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<n-config-provider :theme-overrides="themeOverrides">
|
||||||
<PerfectScrollbar>
|
<n-loading-bar-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<n-modal-provider>
|
||||||
|
<n-scrollbar style="max-height: 100vh">
|
||||||
<lay-index></lay-index>
|
<lay-index></lay-index>
|
||||||
</PerfectScrollbar>
|
</n-scrollbar>
|
||||||
</div>
|
</n-modal-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
<d-modal class="!w-80" v-model="modal.visible" :title="modal.title">
|
</n-message-provider>
|
||||||
{{ modal.content }}
|
</n-loading-bar-provider>
|
||||||
<div class="mt-4 w-full flex justify-between">
|
</n-config-provider>
|
||||||
<!-- <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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 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 () {
|
document.oncontextmenu = function () {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if($cookies.get('userinfo')) logStatus.setIsLogin(true)
|
|
||||||
|
if ($cookies.get('userinfo')) logStatus.setIsLogin(true)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less"></style>
|
||||||
.ps {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
padding-inline-start: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,38 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<d-layout>
|
<n-layout>
|
||||||
<d-header class="dheader-1">
|
<n-layout-header class="dheader-1">
|
||||||
<menu-h />
|
<menu-h />
|
||||||
</d-header>
|
</n-layout-header>
|
||||||
<d-content class="dcontent-1">
|
<n-layout-content class="dcontent-1">
|
||||||
<router-view />
|
<router-view />
|
||||||
</d-content>
|
</n-layout-content>
|
||||||
<d-footer v-show="route.path == '/home'" class="dfooter-1">
|
<n-layout-footer v-show="route.path == '/home'" class="flex justify-center dfooter-1">
|
||||||
<div class="beian" ref="footer">
|
<div class="beian" ref="footer">
|
||||||
<a class="text-primary" href="https://beian.miit.gov.cn" target="_blank">皖ICP备2021017362号-1</a>
|
<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>
|
<a class="swag text-primary" target="_blank" href="https://www.hxyouzi.com/swag">api文档</a>
|
||||||
</div>
|
</div>
|
||||||
</d-footer>
|
</n-layout-footer>
|
||||||
</d-layout>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const footer = ref<HTMLElement | null>(null)
|
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(() => {
|
onMounted(() => {
|
||||||
// 计算footer的高度
|
// 计算footer的高度
|
||||||
|
|
||||||
const footerHeight = footer.value?.clientHeight || 0;
|
const footerHeight = footer.value?.clientHeight || 0;
|
||||||
|
|
||||||
console.log('footerHeight', footerHeight);
|
console.log('footerHeight', footerHeight);
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.dcontent-1 {
|
.dcontent-1 {
|
||||||
|
|
||||||
background-color: #fbfbfb;
|
background-color: #fbfbfb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.beian {
|
.beian {
|
||||||
|
width: 100%;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
24
src/api/blog/index.ts
Normal 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",
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -24,3 +24,4 @@ export function putShare(data: Record<string, string>) {
|
|||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
src/api/plink/index.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -16,3 +16,10 @@ export function register(data: Record<string, string>) {
|
|||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// autologin
|
||||||
|
export function autologin() {
|
||||||
|
return request({
|
||||||
|
url: "/user/autologin",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -7,12 +7,16 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ps__thumb-y {
|
.n-scrollbar-rail__scrollbar {
|
||||||
background-color: #f6cbe7 !important;
|
background-color: #f6cbe770 !important;
|
||||||
}
|
}
|
||||||
.devui-image-preview {
|
|
||||||
background-color: #00000090 !important;
|
/* 添加渐变背景色 */
|
||||||
img {
|
#nprogress .bar {
|
||||||
border-radius: 8px;
|
background: linear-gradient(to right, #ec66ab, #f78c6c, #7ed6df);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 添加平滑过渡效果 */
|
||||||
|
#nprogress .bar {
|
||||||
|
transition: width 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
@import "./base.less";
|
|
||||||
@import "@devui-design/icons/icomoon/devui-icon.css";
|
|
||||||
@import "qweather-icons/font/qweather-icons.css";
|
@import "qweather-icons/font/qweather-icons.css";
|
||||||
@import "@devui-design/icons/icomoon/devui-icon.css";
|
@import 'md-editor-v3/lib/preview.css';
|
||||||
@import "vue-devui/style.css";
|
@import "nprogress/nprogress.css";
|
||||||
@import "vue3-perfect-scrollbar/style.css";
|
@import "./base.less";
|
||||||
2118
src/assets/myblog.less
Normal file
281
src/components/Gallery.vue
Normal 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>
|
||||||
@ -1,44 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<d-card class="mt-10 bg-[#ffffff60] rounded-[10px]" shadow="never">
|
<n-card class="mt-10 rounden-[10px] w-[500px]" shadow="never">
|
||||||
<d-tabs v-model="tid" type="pills">
|
<n-tabs v-model:value="tid" justify-content="space-evenly" animated>
|
||||||
<d-tab id="tab1" title="登录">
|
<n-tab-pane name="tab1" tab="登录">
|
||||||
<d-form ref="formLogin" layout="vertical" :data="loginData" :rules="rules">
|
<n-form ref="formLogin" layout="vertical" :data="loginData" :rules="rules">
|
||||||
<d-form-item field="username">
|
<n-form-item field="username">
|
||||||
<d-input v-model="loginData.username" placeholder="请输入用户名" />
|
<n-input v-model:value="loginData.username" placeholder="请输入用户名" />
|
||||||
</d-form-item>
|
</n-form-item>
|
||||||
<d-form-item field="password">
|
<n-form-item field="password">
|
||||||
<d-input v-model="loginData.password" show-password placeholder="请输入密码" />
|
<n-input v-model:value="loginData.password" type="password" show-password-on="click" placeholder="请输入密码" />
|
||||||
</d-form-item>
|
</n-form-item>
|
||||||
<d-form-item class="form-operation-wrap">
|
<n-form-item class="form-operation-wrap">
|
||||||
<d-button class="w-full" variant="solid" @click="login">登 录</d-button>
|
<n-button class="w-full" type="primary" @click="login">登 录</n-button>
|
||||||
</d-form-item>
|
</n-form-item>
|
||||||
</d-form>
|
</n-form>
|
||||||
</d-tab>
|
</n-tab-pane>
|
||||||
<d-tab id="tab2" title="注册">
|
<n-tab-pane name="tab2" tab="注册">
|
||||||
<d-form ref="formReg" layout="vertical" :data="regData" :rules="rrules">
|
<n-form ref="formReg" layout="vertical" :data="regData" :rules="rrules">
|
||||||
<d-form-item field="username">
|
<n-form-item field="username">
|
||||||
<d-input v-model="regData.username" placeholder="请输入用户名" />
|
<n-input v-model:value="regData.username" placeholder="请输入用户名" />
|
||||||
</d-form-item>
|
</n-form-item>
|
||||||
<d-form-item field="password">
|
<n-form-item field="password">
|
||||||
<d-input v-model="regData.password" show-password placeholder="请输入用密码" />
|
<n-input v-model:value="regData.password" type="password" show-password-on="click" placeholder="请输入用密码" />
|
||||||
</d-form-item>
|
</n-form-item>
|
||||||
<d-form-item field="nickname">
|
<n-form-item field="nickname">
|
||||||
<d-input v-model="regData.nickname" placeholder="请输入昵称" />
|
<n-input v-model:value="regData.nickname" placeholder="请输入昵称" />
|
||||||
</d-form-item>
|
</n-form-item>
|
||||||
<d-form-item class="form-operation-wrap">
|
<n-form-item class="form-operation-wrap">
|
||||||
<d-button class="w-full" variant="solid" @click="register">注册</d-button>
|
<n-button class="w-full" type="primary" @click="register">注册</n-button>
|
||||||
</d-form-item>
|
</n-form-item>
|
||||||
</d-form>
|
</n-form>
|
||||||
</d-tab>
|
</n-tab-pane>
|
||||||
</d-tabs>
|
</n-tabs>
|
||||||
</d-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const emit = defineEmits<{
|
const props = defineProps({
|
||||||
(e: 'update:visible', value: boolean): void
|
setVisible: {
|
||||||
}>()
|
type: Function,
|
||||||
|
default: () => { },
|
||||||
|
}
|
||||||
|
})
|
||||||
// 登录注册逻辑
|
// 登录注册逻辑
|
||||||
const tid = ref("tab1");
|
const tid = ref("tab1");
|
||||||
const formLogin: any = ref(null);
|
const formLogin: any = ref(null);
|
||||||
@ -77,8 +80,8 @@ const rrules: any = reactive({
|
|||||||
})
|
})
|
||||||
function login() {
|
function login() {
|
||||||
console.log('>>> --> login --> formLogin.value:', usrLog.isLogin)
|
console.log('>>> --> login --> formLogin.value:', usrLog.isLogin)
|
||||||
formLogin.value.validate(async (is: boolean, b: any) => {
|
formLogin.value?.validate(async (is: boolean) => {
|
||||||
if (!is) {
|
if (is) {
|
||||||
$msg.error('信息填写不正确,请检查后再提交')
|
$msg.error('信息填写不正确,请检查后再提交')
|
||||||
} else {
|
} else {
|
||||||
const res = await $http.user.login(loginData)
|
const res = await $http.user.login(loginData)
|
||||||
@ -90,15 +93,14 @@ function login() {
|
|||||||
$cookies.set('userinfo', res.data.userinfo, '1d')
|
$cookies.set('userinfo', res.data.userinfo, '1d')
|
||||||
$msg.success(res.msg)
|
$msg.success(res.msg)
|
||||||
usrLog.setIsLogin(true)
|
usrLog.setIsLogin(true)
|
||||||
// router.push({ path: "/" })
|
props.setVisible(false)
|
||||||
emit("update:visible",false)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
function register() {
|
function register() {
|
||||||
formReg.value.validate(async (is: boolean, b: any) => {
|
formReg.value.validate(async (is: boolean) => {
|
||||||
if (!is) {
|
if (is) {
|
||||||
$msg.error('信息填写不正确,请检查后再提交')
|
$msg.error('信息填写不正确,请检查后再提交')
|
||||||
} else {
|
} else {
|
||||||
const res = await $http.user.register(regData)
|
const res = await $http.user.register(regData)
|
||||||
@ -108,8 +110,10 @@ function register() {
|
|||||||
}
|
}
|
||||||
$msg.success(res.msg)
|
$msg.success(res.msg)
|
||||||
tid.value = 'tab1'
|
tid.value = 'tab1'
|
||||||
formReg.value.resetForm()
|
|
||||||
loginData.username = regData.username
|
loginData.username = regData.username
|
||||||
|
regData.username = ''
|
||||||
|
regData.nickname = ''
|
||||||
|
regData.password = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/components/backTop.vue
Normal 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>
|
||||||
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pr-8">
|
<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"> -->
|
<!-- <div class="dt-card mt-10 bg-white"> -->
|
||||||
<template #title>
|
<template #header>
|
||||||
<div class="flex items-center">
|
<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>
|
</div>
|
||||||
</template>
|
</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="w-full pr-20 text-center text-[#ec66ab] font-500 text-4xl font-[yj]">{{ t }}</div>
|
||||||
<div class="mt-3 flex justify-between">
|
<div class="mt-3 flex justify-between">
|
||||||
<span>今年已过了{{ jq.dayOfYear }}天</span>
|
<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="">
|
<img v-else class="absolute top-0 right-4" width="120" src="@/assets/images/onwork.png" alt="">
|
||||||
</template>
|
</template>
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
</d-card>
|
</n-card>
|
||||||
|
|
||||||
|
|
||||||
<d-card shadow="never" class="mt-4 bg-white">
|
<n-card embedded class="mt-4 shadow">
|
||||||
<template #title>
|
<template #header>
|
||||||
<div class="flex items-center">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #default>
|
||||||
<div class="py-1" v-for="i in bdNews" :key="i.id">
|
<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 }}
|
<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>
|
<icon-hot v-show="i.index < 4" class="ml-2 w-4 text-[red] inline-block"></icon-hot>
|
||||||
</span>
|
</span>
|
||||||
@ -39,7 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-2 justify-between flex items-center">
|
<div class="mt-2 justify-between flex items-center">
|
||||||
<div class="w-2/5 h-px bg-[#ec66ab]"></div>
|
<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">
|
target="_blank">
|
||||||
更多
|
更多
|
||||||
<icon-right class="ml-1 w-4 text-primary inline-block"></icon-right>
|
<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 class="w-2/5 h-px bg-[#ec66ab]"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</d-card>
|
</n-card>
|
||||||
|
|
||||||
|
|
||||||
<d-card shadow="never" class="mt-4 bg-white">
|
<n-card embedded class="mt-4 shadow">
|
||||||
<template #title>
|
<template #header>
|
||||||
<div class="flex items-center">
|
<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 class="ml-12 text-[#ec66ab] font-500">{{ jq.yearTips }}年 {{ jq.lunarCalendar }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #default>
|
||||||
<div class="truncate text-sm mx-[5%]">
|
<div class="truncate text-sm mx-[5%]">
|
||||||
宜:{{ jq.suit }}
|
宜:{{ jq.suit }}
|
||||||
</div>
|
</div>
|
||||||
@ -70,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-center">{{ jq.solarTerms }}</div>
|
<div class="mt-2 text-center">{{ jq.solarTerms }}</div>
|
||||||
</template>
|
</template>
|
||||||
</d-card>
|
</n-card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -132,8 +133,14 @@ onUnmounted(() => {
|
|||||||
src: url('@/assets/font/LCDML.woff2');
|
src: url('@/assets/font/LCDML.woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:deep(.n-card > .n-card__content, .n-card > .n-card__footer){
|
||||||
|
padding: 8px 25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
// .dt-card {
|
// .dt-card {
|
||||||
// background-image: url('@/assets/images/中秋节中国风边框34.png');
|
// background-image: url('@/assets/images/中秋节中国风边框34.png');
|
||||||
// background-size: 100% 100%;
|
// background-size: 100% 100%;
|
||||||
// padding: 20px 40px;
|
// padding: 20px 40px;
|
||||||
// }</style>
|
// }
|
||||||
|
</style>
|
||||||
35
src/components/mask.vue
Normal 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>
|
||||||
@ -1,40 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="nav" class="main-nav hidden lg:flex justify-between bg-white">
|
<div ref="nav" class="main-nav hidden lg:flex justify-between bg-white">
|
||||||
<!-- 网站Logo -->
|
<!-- 网站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" />
|
<img :src="logo" alt="柚子的网站" class="h-9 align-middle" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 主导航菜单 -->
|
<!-- 主导航菜单 -->
|
||||||
<d-menu mode="horizontal" router class="ml-5 h-14 text-[16px] " :default-select-keys="[key]">
|
<div class="w-2/5">
|
||||||
<d-menu-item key="home">
|
<n-menu :icon-size="12" v-model:value="activeKey" class="" mode="horizontal" :options="menuOptions" />
|
||||||
<d-icon :component="homeSvg" class="w-5 mr-1"></d-icon>
|
</div>
|
||||||
首页
|
|
||||||
</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="!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">
|
<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 }}
|
{{ locationInfo }}
|
||||||
</span>
|
</span>
|
||||||
<span class="mx-3 text-gray-300">|</span>
|
<span class="mx-3 text-gray-300">|</span>
|
||||||
@ -43,36 +22,23 @@
|
|||||||
<span class="weather-info ml-4">{{ temp }}°C</span>
|
<span class="weather-info ml-4">{{ temp }}°C</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mr-8">
|
<div class="flex items-center justify-end mr-8 w-1/20">
|
||||||
<d-dropdown class="cursor-pointer w-[100px]" v-if="userinfo" trigger="hover">
|
<n-dropdown class="cursor-pointer w-[100px]" v-if="userinfo" :options="oprOp" @select="handleSelect"
|
||||||
|
trigger="hover">
|
||||||
<div class="flex items-center">
|
<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 class="cursor-pointer ml-2 text-gray text-sm">{{ userinfo.nickname }}</div>
|
||||||
</div>
|
</div>
|
||||||
<template #menu>
|
</n-dropdown>
|
||||||
<ul class="list-menu">
|
<div v-else class="flex items-center" @click="toLogin">
|
||||||
<!-- hover为淡粉色 -->
|
<n-button size="small" type="primary" @click="toLogin">登录</n-button>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 登录弹窗 -->
|
<!-- 登录弹窗 -->
|
||||||
<d-modal class="!w-120" v-model="visible">
|
<masked :visible="visible" :setVisible="setVisible">
|
||||||
<login-modal v-model:visible="visible"></login-modal>
|
<loginModal :setVisible="setVisible" />
|
||||||
</d-modal>
|
</masked>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between bg-white lg:hidden">
|
<div class="flex justify-between bg-white lg:hidden">
|
||||||
<div class="pl-2 items-centerflex" slot="brand" @click="">
|
<div class="pl-2 items-centerflex" slot="brand" @click="">
|
||||||
@ -80,37 +46,58 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mr-2">
|
<div class="flex items-center mr-2">
|
||||||
<div v-if="userinfo" class="flex items-center">
|
<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 class="cursor-pointer ml-2 text-gray text-sm">{{ userinfo.nickname }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center">
|
<div v-else class="flex items-center">
|
||||||
<d-avatar class="cursor-pointer" @click="toLogin"></d-avatar>
|
<!-- <n-avatar round class="cursor-pointer" @click="toLogin"></n-avatar>
|
||||||
<div class="cursor-pointer ml-2 text-gray text-sm" @click="toLogin">登录</div>
|
<div 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>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// 从@/icon/menu引入所有的svg文件
|
// 从@/icon/menu引入所有的svg文件
|
||||||
import logo from '@/assets/images/logo.png';
|
import logo from '@/assets/images/logo.png';
|
||||||
import artiSvg from '@/icon/menu/arti.svg';
|
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 homeSvg from '@/icon/menu/home.svg';
|
||||||
import linkSvg from '@/icon/menu/link.svg';
|
import linkSvg from '@/icon/menu/link.svg';
|
||||||
import picSvg from '@/icon/menu/pic.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 loginModal from '@/components/Login.vue'
|
||||||
|
import masked from '@/components/mask.vue'
|
||||||
|
import type { UploadFileInfo } from 'naive-ui'
|
||||||
|
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter, RouterLink } from 'vue-router';
|
||||||
|
|
||||||
|
|
||||||
|
const activeKey = ref('home')
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const key = ref("home");
|
|
||||||
const locationInfo = ref("获取位置中...");
|
const locationInfo = ref("获取位置中...");
|
||||||
const latitude = ref<number | null>(null);
|
const latitude = ref<number | null>(null);
|
||||||
const longitude = 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 nav: any = useTemplateRef('nav')
|
||||||
const navx = $store.nav.useNavStore()
|
const navx = $store.nav.useNavStore()
|
||||||
const usrLog = $store.log.useLogStore()
|
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 = () => {
|
const getLocation = () => {
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
@ -198,7 +298,7 @@ async function handdleJw(jw: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(() => route.name, (newVal) => {
|
watch(() => route.name, (newVal) => {
|
||||||
key.value = newVal as string
|
activeKey.value = newVal as string
|
||||||
})
|
})
|
||||||
|
|
||||||
function goHome() {
|
function goHome() {
|
||||||
@ -220,6 +320,9 @@ function logout() {
|
|||||||
usrLog.setIsLogin(false)
|
usrLog.setIsLogin(false)
|
||||||
userinfo.value = null;
|
userinfo.value = null;
|
||||||
}
|
}
|
||||||
|
function gotoConsole() {
|
||||||
|
window.open("https://www.hxyouzi.com/console/home", "_BLACK")
|
||||||
|
}
|
||||||
|
|
||||||
function gotoHf() {
|
function gotoHf() {
|
||||||
console.log('>>> --> gotoHf --> fxlink:', fxlink)
|
console.log('>>> --> gotoHf --> fxlink:', fxlink)
|
||||||
@ -230,19 +333,22 @@ onMounted(() => {
|
|||||||
|
|
||||||
userinfo.value = $cookies.get('userinfo');
|
userinfo.value = $cookies.get('userinfo');
|
||||||
console.log('>>>>>>>>>>', userinfo.value);
|
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)
|
console.log('>>> --> route.name:', route.name)
|
||||||
getLocation(); // 组件挂载时获取位置
|
getLocation(); // 组件挂载时获取位置
|
||||||
|
|
||||||
const h: number = nav.value.clientHeight
|
const h: number = nav.value.clientHeight
|
||||||
if (h > 0) navx.setNavH(h)
|
if (h > 0) navx.setNavH(h)
|
||||||
|
token.value = $cookies.get('token')
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
onBeforeUpdate(() => {
|
onBeforeUpdate(() => {
|
||||||
userinfo.value = $cookies.get('userinfo');
|
userinfo.value = $cookies.get('userinfo');
|
||||||
key.value = route.name as string;
|
activeKey.value = route.name as string;
|
||||||
const h: number = nav.value.clientHeight
|
const h: number = nav.value.clientHeight
|
||||||
// console.log('******>>> --> nav.value:', nav.value)
|
// console.log('******>>> --> nav.value:', nav.value)
|
||||||
// console.log('()()()()()>>> --> h:', h)
|
// console.log('()()()()()>>> --> h:', h)
|
||||||
@ -257,32 +363,8 @@ onBeforeUpdate(() => {
|
|||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.devui-menu-horizontal .devui-menu-item:hover span .icon) {
|
:deep(.n-menu-item-content__icon) {
|
||||||
color: var(--devui-brand, #5e7ce0) !important;
|
width: 16px !important;
|
||||||
fill: var(--devui-brand, #5e7ce0) !important;
|
height: 16px !important;
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.devui-menu-item-select span svg) {
|
|
||||||
color: var(--devui-brand, #5e7ce0) !important;
|
|
||||||
fill: var(--devui-brand, #5e7ce0) !important;
|
|
||||||
line-height: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.devui-menu-item-select span) {
|
|
||||||
color: var(--devui-brand, #5e7ce0) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.devui-menu-horizontal) {
|
|
||||||
padding: 14px 20px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.devui-menu-item span) {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.devui-icon__container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { Message } from 'vue-devui';
|
|
||||||
|
|
||||||
const msg:any = Message;
|
|
||||||
|
|
||||||
export default msg;
|
|
||||||
4
src/icon/arti.svg
Normal 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
@ -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
@ -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 |
@ -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">
|
<?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>
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 754 B After Width: | Height: | Size: 3.9 KiB |
@ -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 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" />
|
<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>
|
</svg>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 529 B After Width: | Height: | Size: 530 B |
17
src/icon/loading.svg
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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 |
BIN
src/lib/assets/loadError.png
Normal file
|
After Width: | Height: | Size: 680 B |
BIN
src/lib/assets/loading.gif
Normal file
|
After Width: | Height: | Size: 284 KiB |
94
src/lib/components/LazyImg.vue
Normal 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>
|
||||||
200
src/lib/components/Waterfall.vue
Normal 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
@ -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
@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.gif" {
|
||||||
|
const value: string;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
13
src/lib/types/jsx.d.ts
vendored
Normal 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
@ -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
@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.png" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
3
src/lib/types/util.d.ts
vendored
Normal 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
@ -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
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
export { useCalculateCols } from './useCalculateCols'
|
||||||
|
export { useLayout } from './useLayout'
|
||||||
46
src/lib/use/useCalculateCols.ts
Normal 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
@ -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
@ -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
@ -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)
|
||||||
|
}
|
||||||
19
src/lib/utils/itemWidth.ts
Normal 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
@ -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
@ -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
|
||||||
|
}
|
||||||
25
src/main.ts
@ -3,31 +3,26 @@ import { useConfig } from "@/config";
|
|||||||
import icon from "@/icon/index.ts";
|
import icon from "@/icon/index.ts";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import "virtual:uno.css";
|
import "virtual:uno.css";
|
||||||
import { createApp, vaporInteropPlugin } from "vue";
|
import 'vue-devui/tag/style.css';
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
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);
|
const app = createApp(App);
|
||||||
|
app.use(Tag)
|
||||||
app.use(vaporInteropPlugin)
|
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
// app.use(VMdPreview)
|
||||||
useConfig();
|
useConfig();
|
||||||
for (const key in icon) {
|
for (const key in icon) {
|
||||||
// console.log(key, icon[key]);
|
// console.log(key, icon[key]);
|
||||||
app.component("icon-" + key, icon[key] as any);
|
app.component("icon-" + key, icon[key] as any);
|
||||||
}
|
}
|
||||||
app.use(PerfectScrollbarPlugin);
|
app.component("back-top", backTop);
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import NProgress from 'nprogress';
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import { routes } from 'vue-router/auto-routes';
|
import { routes } from "vue-router/auto-routes";
|
||||||
|
|
||||||
routes.unshift({
|
routes.unshift({
|
||||||
path: "/",
|
path: "/",
|
||||||
redirect: "/home",
|
redirect: "/home",
|
||||||
@ -8,7 +8,17 @@ routes.unshift({
|
|||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
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;
|
export default router;
|
||||||
|
|||||||
@ -98,7 +98,7 @@ export function deepclone<T>(obj: T): T {
|
|||||||
* @example
|
* @example
|
||||||
* formatTimeBydate(new Date(), 'yyyy-MM-dd HH:mm:ss');
|
* 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);
|
return formatTime(this, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import router from "@/router";
|
|
||||||
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios";
|
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useCookies } from "vue3-cookies";
|
import { useCookies } from "vue3-cookies";
|
||||||
@ -45,16 +44,16 @@ request.interceptors.response.use(
|
|||||||
console.log("Response error", error);
|
console.log("Response error", error);
|
||||||
|
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
// window.$msg.warning("无效的token");
|
window.$msg.warning("无效的token");
|
||||||
cookies.remove("token");
|
cookies.remove("token");
|
||||||
cookies.remove("userinfo");
|
cookies.remove("userinfo");
|
||||||
window.$modal({
|
// window.$modal({
|
||||||
title: "无效的token",
|
// title: "无效的token",
|
||||||
content: "token已失效,需要登录,请登录 =>",
|
// content: "token已失效,需要登录,请登录 =>",
|
||||||
handdleSubmit: () => {
|
// handdleSubmit: () => {
|
||||||
router.replace("/login");
|
// router.replace("/login");
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
// router.replace("/login");
|
// router.replace("/login");
|
||||||
return "Unauthorized";
|
return "Unauthorized";
|
||||||
|
|||||||
15
src/util/theme.ts
Normal 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;
|
||||||
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
definePage({
|
definePage({
|
||||||
name:'appshare',
|
name:'apps',
|
||||||
meta: {
|
meta: {
|
||||||
title: '软件分享',
|
title: '软件分享',
|
||||||
}
|
}
|
||||||
@ -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>
|
|
||||||
@ -1,126 +1,74 @@
|
|||||||
<template>
|
<template>
|
||||||
<PerfectScrollbar ref="scrollbar" @ps-scroll-y="handleScroll">
|
<n-scrollbar ref="virtualListInst" class="py-5" :style="boxStyle" @scroll="handleScroll">
|
||||||
<div ref="myCon" class="gallery-page py-5 px-[10%]">
|
<n-input class="my-4 !w-[72%] ml-[14%]" round size="large" v-model:value="kw" @keyup.enter="onSearch"
|
||||||
<d-search class="mt-0 mb-8 w-2/3 mx-auto rounded-full" v-model="kw" is-keyup-search :delay="1000"
|
placeholder="请输入关键字">
|
||||||
@search="onSearch"></d-search>
|
<template #suffix>
|
||||||
<div class="gallery-container w-full box-border">
|
<n-icon size="large">
|
||||||
<!-- 瀑布流容器 -->
|
<icon-search />
|
||||||
<div ref="waterfallContainer" v-image-preview
|
</n-icon>
|
||||||
class="waterfall-container flex justify-between flex-nowrap w-full overflow-hidden">
|
</template>
|
||||||
<!-- 动态生成的列 -->
|
</n-input>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { throttle } from 'es-toolkit';
|
import { throttle } from 'es-toolkit';
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { LazyImg, Waterfall } from '../lib';
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
name: 'gallery',
|
name: 'gallery',
|
||||||
meta: {
|
meta: {
|
||||||
title: '画廊',
|
title: '画廊',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const waterfall = ref<any>(null);
|
||||||
|
const nav: any = $store.nav.useNavStore()
|
||||||
|
const boxStyle: any = ref({})
|
||||||
// 画廊页逻辑
|
// 画廊页逻辑
|
||||||
const fileList = ref<any[]>([]);
|
const fileList = ref<any[]>([]);
|
||||||
const pn = ref(1);
|
const pn = ref(1);
|
||||||
const ps = ref(40);
|
const ps = ref(20);
|
||||||
const loading = ref(false);
|
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 kw = ref<string>('');
|
||||||
|
const cwidth = ref<number>(240);
|
||||||
|
const column = ref<number>(5);
|
||||||
|
const gutter = ref<number>(18);
|
||||||
|
|
||||||
// const uploadOptions = ref({
|
const isLoadAll = ref<boolean>(false)
|
||||||
// uri: 'https://www.hxyouzi.com/api/files/upload',
|
|
||||||
// method: 'POST',
|
|
||||||
// maximumSize: 5 * 1024 * 1024,
|
|
||||||
// headers: {
|
|
||||||
// 'Authorization': 'Bearer ' + $cookies.get('token'),
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// 计算列数 based on 屏幕宽度
|
|
||||||
function calculateColumnCount() {
|
|
||||||
if (!waterfallContainer.value) return;
|
|
||||||
const containerWidth = waterfallContainer.value.clientWidth;
|
|
||||||
const newColumnCount = Math.max(1, Math.floor(containerWidth / itemWidth.value));
|
|
||||||
if (newColumnCount !== columnCount.value) {
|
|
||||||
columnCount.value = newColumnCount;
|
|
||||||
resetWaterfall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置瀑布流
|
|
||||||
function resetWaterfall() {
|
|
||||||
columns.value = Array(columnCount.value).fill(0).map(() => []);
|
|
||||||
columnHeights.value = Array(columnCount.value).fill(0);
|
|
||||||
// 重新分配图片
|
|
||||||
fileList.value.forEach(async item =>await addToWaterfall(item));
|
|
||||||
console.log('>>> --> resetWaterfall --> fileList:', fileList.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加图片到瀑布流
|
|
||||||
async function addToWaterfall(item: any) {
|
|
||||||
if (columns.value.length === 0) return;
|
|
||||||
const { height, width } =await getImageSizeByCheck(item.filepath)
|
|
||||||
|
|
||||||
// 找到高度最小的列
|
// 计算列数
|
||||||
let minHeight = Math.min(...columnHeights.value);
|
function calculateColumns() {
|
||||||
let minIndex = columnHeights.value.indexOf(minHeight);
|
const totalWidth = window.innerWidth * 0.8; // 画廊宽度为视口宽度的80%
|
||||||
console.log('>>> --> addToWaterfall --> item:', item)
|
const col = Math.floor((totalWidth + gutter.value) / (cwidth.value + gutter.value));
|
||||||
console.log('>>> --> addToWaterfall --> minIndex:', minIndex)
|
column.value = col > 0 ? col : 1;
|
||||||
// 添加到该列
|
waterfall.value?.renderer()
|
||||||
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)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取文件列表
|
// 获取文件列表
|
||||||
@ -133,66 +81,44 @@ async function getFileList() {
|
|||||||
page_num: pn.value,
|
page_num: pn.value,
|
||||||
page_size: ps.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) {
|
if (pn.value === 1) {
|
||||||
fileList.value = res.data;
|
fileList.value = res.data;
|
||||||
resetWaterfall();
|
|
||||||
} else {
|
} else {
|
||||||
// 追加新数据
|
// 追加新数据
|
||||||
res.data.forEach((item: any) => {
|
res.data.forEach((item: any) => {
|
||||||
fileList.value.push(item);
|
fileList.value.push(item);
|
||||||
addToWaterfall(item);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取文件列表失败:', error);
|
console.error('获取文件列表失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
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) => {
|
const handleScroll: any = throttle((e: any) => {
|
||||||
console.log('>>> --> handleScroll --> loading:', e)
|
// console.log('>>> --> handleScroll --> loading:', e)
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
|
if (isLoadAll.value) return;
|
||||||
const scrollTop = e.target.scrollTop
|
const scrollTop = e.target.scrollTop
|
||||||
console.log('>>> --> handleScroll --> scrollTop:', scrollTop)
|
// console.log('>>> --> handleScroll --> scrollTop:', scrollTop)
|
||||||
const scrollHeight = e.target.scrollHeight
|
const scrollHeight = e.target.scrollHeight
|
||||||
// console.log('>>> --> handleScroll --> scrollHeight:', scrollHeight)
|
// console.log('>>> --> handleScroll --> scrollHeight:', scrollHeight)
|
||||||
const clientHeight = e.target.offsetHeight;
|
const clientHeight = e.target.offsetHeight;
|
||||||
|
|
||||||
// 当滚动到距离底部20%时加载更多
|
// 当滚动到距离底部20%时加载更多
|
||||||
// console.log('>>> --> clientHeight --> clientHeight:', clientHeight)
|
// console.log('>>> --> clientHeight --> clientHeight:', clientHeight)
|
||||||
if (scrollTop + clientHeight >= scrollHeight * 0.8) {
|
if (scrollTop + clientHeight >= scrollHeight - 100) {
|
||||||
console.log('>>> --> handleScroll --> 加载更多')
|
console.log('>>> --> handleScroll --> 加载更多')
|
||||||
pn.value++;
|
pn.value++;
|
||||||
getFileList();
|
getFileList();
|
||||||
@ -200,6 +126,8 @@ const handleScroll: any = throttle((e: any) => {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
function onSearch() {
|
function onSearch() {
|
||||||
pn.value = 1;
|
pn.value = 1;
|
||||||
|
kw.value = kw.value.trim()
|
||||||
|
isLoadAll.value = false
|
||||||
getFileList();
|
getFileList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,12 +152,14 @@ function downloadFile(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
calculateColumns()
|
||||||
getFileList();
|
getFileList();
|
||||||
// 计算初始列数
|
console.log('>>> --> nav.NavH:', nav.navH)
|
||||||
calculateColumnCount();
|
boxStyle.value = {
|
||||||
// 添加窗口大小变化监听
|
maxHeight: `calc(100vh - 5px - ${nav.navH}px)`,
|
||||||
window.addEventListener('resize', calculateColumnCount);
|
}
|
||||||
// 添加滚动监听
|
// 添加滚动监听
|
||||||
|
window.addEventListener('resize', calculateColumns);
|
||||||
window.addEventListener('scroll', handleScroll);
|
window.addEventListener('scroll', handleScroll);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -237,46 +167,14 @@ onUnmounted(() => {
|
|||||||
pn.value = 1;
|
pn.value = 1;
|
||||||
kw.value = '';
|
kw.value = '';
|
||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
resetWaterfall();
|
isLoadAll.value = false
|
||||||
|
|
||||||
// 移除监听
|
// 移除监听
|
||||||
window.removeEventListener('resize', calculateColumnCount);
|
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
/* 画廊页样式 */
|
:deep(.n-image img) {
|
||||||
:deep(.devui-tabs__nav) {
|
border-radius: 0.375rem !important;
|
||||||
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>
|
</style>
|
||||||
@ -1,37 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-page" :style="contentStyle">
|
<div class="home-page" :content-style="contentStyle">
|
||||||
<d-layout>
|
<n-layout has-sider>
|
||||||
<d-content class="main-content">
|
<n-layout-content class="main-content">
|
||||||
<div class="pt-8 px-12 relative hidden lg:block">
|
<div class="pt-8 px-12 relative hidden lg:block">
|
||||||
<d-input class="devui-input-demo__mt" size="lg" v-model="searchWord" @blur="cancelSbox" placeholder="请输入">
|
<n-input-group class="shadow ">
|
||||||
<template #prepend>
|
<n-select class="w-24" size="large" v-model:value="broswer" :options="options"
|
||||||
<d-select class="w-48" size="lg" v-model="broswer" :options="options" @click="cancelSbox"></d-select>
|
@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>
|
||||||
<template #append>
|
</n-input>
|
||||||
<d-icon name="search" style="font-size: inherit;" @click="search" />
|
</n-input-group>
|
||||||
</template>
|
<div v-if="searchBox"
|
||||||
</d-input>
|
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 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">
|
|
||||||
<div class="flex p-2 pr-20 truncate rounded-md items-center hover:text-primary cursor-pointer"
|
<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"
|
:class="selecedIdx === idx ? 'text-white bg-primary' : ''" v-for="(i, idx) in searchItems" :key="idx"
|
||||||
:key="idx" @click="goExtra(i.menu_link)" @keyup.enter ="goExtra(i.menu_link)"
|
@click="goExtra(i.menu_link)" @keyup.enter="goExtra(i.menu_link)"><span v-if="idx">导航:</span>
|
||||||
><span v-if="idx">导航:</span> {{i.menu_name }}</div>
|
{{ i.menu_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 标签组 -->
|
<!-- 标签组 -->
|
||||||
<!-- <PerfectScrollbar class="w-full overflow-x-auto"> -->
|
<!-- <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">
|
<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"
|
<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>
|
:color="tag.color" @click="handdleTagClick(tag)">{{ tag.name }}</d-tag>
|
||||||
</div>
|
</div>
|
||||||
<!-- </PerfectScrollbar> -->
|
<!-- </PerfectScrollbar> -->
|
||||||
<!-- 图片网格展示区域 -->
|
<!-- 图片网格展示区域 -->
|
||||||
<PerfectScrollbar class="" :style="navStyle">
|
|
||||||
<div ref="navcards"
|
<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">
|
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"
|
<n-card embedded
|
||||||
@click="goExtra(item.menu_link)" @contextmenu.prevent="handdleContextMenu($event, item)">
|
class="bg-[#ffffff80] h-24 shadow-md transition-transform duration-300 box-border hover:-translate-y-1.5"
|
||||||
<template #content>
|
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 class="mt-1 w-full flex flex-col items-center cursor-pointer hover:!text-primary">
|
||||||
<div :style="{ background: item.color }"
|
<div :style="{ background: item.color }"
|
||||||
class="w-8 h-8 rounded-full text-white flex items-center justify-center" v-if="item.icon_error">
|
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"
|
<em class="absolute rounded-md top-0 left-0 px-2 text-white text-center text-sm"
|
||||||
:style="{ background: getItemColor(item) }">{{ item.tag }}</em>
|
:style="{ background: getItemColor(item) }">{{ item.tag }}</em>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</n-card>
|
||||||
</d-card>
|
<n-card embedded
|
||||||
<d-card class="bg-[#ffffff80] h-25">
|
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 @click="addNav" class="w-full h-full flex flex-col items-center justify-center cursor-pointer">
|
||||||
<div :style="{ background: getRandomDarkColor() }"
|
<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>
|
||||||
|
<div class="mt-2 w-full text-center text-lg truncate text-primary">新增导航</div>
|
||||||
</div>
|
</div>
|
||||||
</d-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
</PerfectScrollbar>
|
</n-scrollbar>
|
||||||
|
</n-layout-content>
|
||||||
</d-content>
|
<n-layout-sider width="30rem">
|
||||||
<d-aside class="daside hidden w-120 lg:block">
|
|
||||||
<homeSide></homeSide>
|
<homeSide></homeSide>
|
||||||
</d-aside>
|
</n-layout-sider>
|
||||||
</d-layout>
|
</n-layout>
|
||||||
|
|
||||||
<!-- 新增导航弹窗 -->
|
<!-- 新增导航弹窗 -->
|
||||||
<d-modal class="!w-120" v-model="visible" title="新增导航">
|
<maskX :visible="visible" :setVisible="navCancel">
|
||||||
<d-form ref="formNav" layout="vertical" :data="navData">
|
<n-form class="w-[500px] bg-white rounded-md p-8 shadow-md" ref="formNav" layout="vertical" :data="navData">
|
||||||
<d-form-item class="h-8" field="username">
|
<div class="text-center text-lg">新增导航</div>
|
||||||
<d-input @blur="getIcon" v-model="navData.menu_link" placeholder="请输入单行链接(必填)" />
|
<n-form-item class="h-8" path="menu_link">
|
||||||
</d-form-item>
|
<n-input @blur="getIcon" v-model:value="navData.menu_link" placeholder="请输入单行链接(必填)" />
|
||||||
<d-form-item class="h-8" field="password">
|
</n-form-item>
|
||||||
<d-input v-model="navData.menu_name" placeholder="请输入导航名称(必填)" />
|
<n-form-item class="h-8 mt-4" path="menu_name">
|
||||||
</d-form-item>
|
<n-input v-model:value="navData.menu_name" placeholder="请输入导航名称(必填)" />
|
||||||
<d-form-item class="h-8" field="tag">
|
</n-form-item>
|
||||||
<d-input v-model="navData.tag" placeholder="请自定义一个标签(必填,只取前四字)" />
|
<n-form-item class="h-8 mt-4" path="tag">
|
||||||
</d-form-item>
|
<n-input v-model:value="navData.tag" placeholder="请自定义一个标签(必填,只取前四字)" />
|
||||||
<d-form-item class="h-8 form-operation-wrap">
|
</n-form-item>
|
||||||
<div class="flex">
|
<n-form-item class="h-8 mt-4 form-operation-wrap">
|
||||||
<d-input v-model="navData.menu_icon" placeholder="请输入图标链接" />
|
<!-- <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="">
|
<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 v-else class="ml-5 w-[30px] h-[30px]"></div>
|
||||||
</div>
|
<!-- </div> -->
|
||||||
</d-form-item>
|
</n-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 class="mt-14 flex justify-between">
|
<div class="mt-14 flex justify-between">
|
||||||
<d-button class="w-[48%]" variant="solid" color="secondary" @click="navCancel">取消</d-button>
|
<n-button class="w-[48%]" secondary variant="solid" @click="navCancel">取消</n-button>
|
||||||
<d-button class="w-[48%]" variant="solid" color="primary" @click="navSubmit">确定</d-button>
|
<n-button class="w-[48%]" type="primary" variant="solid" @click="navSubmit">确定</n-button>
|
||||||
</div>
|
</div>
|
||||||
</d-modal>
|
</n-form>
|
||||||
|
|
||||||
|
|
||||||
|
</maskX>
|
||||||
|
|
||||||
<!-- 音乐插件 -->
|
<!-- 音乐插件 -->
|
||||||
<aplayer></aplayer>
|
<aplayer></aplayer>
|
||||||
@ -116,25 +118,29 @@
|
|||||||
</contextMenu>
|
</contextMenu>
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
<!-- 编辑弹窗 -->
|
||||||
<d-modal class="!w-120" v-model="editModal" title="导航修改">
|
<maskX :visible="editModal" :setVisible="navCancel">
|
||||||
<div class="mb-2">
|
<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 === 1">导航名称:{{ currentItem?.menu_name }}</span>
|
||||||
<span class="text-primary" v-if="currentClickedItem === 2">导航链接:{{ currentItem.menu_link }}</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 === 3">导航标签:{{ currentItem?.tag }}</span>
|
||||||
</div>
|
</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">
|
<div class="mt-8 flex justify-between">
|
||||||
<d-button class="w-[48%]" variant="solid" color="secondary" @click="handdleItemCancel">取消</d-button>
|
<n-button class="w-[48%]" secondary @click="handdleItemCancel">取消</n-button>
|
||||||
<d-button class="w-[48%]" variant="solid" color="primary" @click="handdleItemSubmit">确定</d-button>
|
<n-button class="w-[48%]" type="primary" @click="handdleItemSubmit">确定</n-button>
|
||||||
</div>
|
</div>
|
||||||
</d-modal>
|
</div>
|
||||||
|
</maskX>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import contextMenu from '@/components/contextMenu.vue';
|
import contextMenu from '@/components/contextMenu.vue';
|
||||||
|
import maskX from '@/components/mask.vue';
|
||||||
import { deepclone, getDictValue } from '@/util/index.ts';
|
import { deepclone, getDictValue } from '@/util/index.ts';
|
||||||
definePage({
|
definePage({
|
||||||
name: 'home',
|
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 menuShow = ref(false)
|
||||||
const MenuOptions: any = reactive({});
|
const MenuOptions = reactive({ x: 0, y: 0 })
|
||||||
const currentItem = ref<any>(null);
|
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 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 currentClickedItem = ref<number>(0)
|
||||||
const editModal = ref<boolean>(false);
|
const editModal = ref<boolean>(false)
|
||||||
const editInput = ref<string>('');
|
const editInput = ref<string>('')
|
||||||
// 新增导航弹窗
|
// 新增导航弹窗
|
||||||
const visible: any = ref<boolean>(false)
|
const visible = ref<boolean>(false)
|
||||||
const navData: any = reactive({
|
const navData = reactive<NavItem>({
|
||||||
menu_link: '',
|
menu_link: '',
|
||||||
menu_name: '',
|
menu_name: '',
|
||||||
tag: '',
|
tag: '',
|
||||||
menu_icon: ''
|
menu_icon: ''
|
||||||
})
|
})
|
||||||
const formNav: any = ref(null)
|
const formNav = ref<any>(null)
|
||||||
const navcards: any = ref(null)
|
const navcards = ref<any>(null)
|
||||||
|
|
||||||
// 首页逻辑
|
// 首页逻辑
|
||||||
const nav: any = $store.nav.useNavStore()
|
const nav: any = $store.nav.useNavStore()
|
||||||
@ -174,22 +204,22 @@ const selecedIdx = ref(0)
|
|||||||
const searchBox = ref(false)
|
const searchBox = ref(false)
|
||||||
const options = ref([
|
const options = ref([
|
||||||
{
|
{
|
||||||
name: '必应',
|
label: '必应',
|
||||||
value: 'bing',
|
value: 'bing',
|
||||||
url: 'https://cn.bing.com/search?q='
|
url: 'https://cn.bing.com/search?q='
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '百度',
|
label: '百度',
|
||||||
value: 'baidu',
|
value: 'baidu',
|
||||||
url: 'https://www.baidu.com/s?wd='
|
url: 'https://www.baidu.com/s?wd='
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '谷歌',
|
label: '谷歌',
|
||||||
value: 'google',
|
value: 'google',
|
||||||
url: 'https://www.google.com/search?q='
|
url: 'https://www.google.com/search?q='
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: '翻译',
|
label: '翻译',
|
||||||
value: 'trans',
|
value: 'trans',
|
||||||
url: 'https://translate.volcengine.com?text='
|
url: 'https://translate.volcengine.com?text='
|
||||||
},
|
},
|
||||||
@ -197,9 +227,9 @@ const options = ref([
|
|||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
const navlist: any = ref([])
|
const navlist = ref<NavItem[]>([])
|
||||||
let cloneNavlist: any = []
|
let cloneNavlist: NavItem[] = []
|
||||||
const tagList: any = ref([
|
const tagList = ref<TagItem[]>([
|
||||||
{
|
{
|
||||||
name: '全部',
|
name: '全部',
|
||||||
color: '',
|
color: '',
|
||||||
@ -209,11 +239,11 @@ const tagList: any = ref([
|
|||||||
|
|
||||||
|
|
||||||
const usrLog = $store.log.useLogStore()
|
const usrLog = $store.log.useLogStore()
|
||||||
let timer: any = null;
|
let timer: any = null
|
||||||
|
|
||||||
// 输入搜索内容时监听searchWord在navlist中模糊搜索
|
// 输入搜索内容时监听searchWord在navlist中模糊搜索
|
||||||
watch(searchWord, () => {
|
watch(searchWord, () => {
|
||||||
handdleInput()
|
handleInput()
|
||||||
})
|
})
|
||||||
|
|
||||||
function cancelSbox() {
|
function cancelSbox() {
|
||||||
@ -224,7 +254,7 @@ function cancelSbox() {
|
|||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handdleInput() {
|
function handleInput() {
|
||||||
if (!searchWord.value) {
|
if (!searchWord.value) {
|
||||||
searchItems.value = []
|
searchItems.value = []
|
||||||
searchBox.value = false
|
searchBox.value = false
|
||||||
@ -234,24 +264,23 @@ function handdleInput() {
|
|||||||
searchBox.value = true
|
searchBox.value = true
|
||||||
selecedIdx.value = 0
|
selecedIdx.value = 0
|
||||||
const keyword = searchWord.value.toLowerCase()
|
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_name.toLowerCase().includes(keyword) ||
|
||||||
item.menu_link.toLowerCase().includes(keyword) ||
|
item.menu_link.toLowerCase().includes(keyword) ||
|
||||||
item.tag.toLowerCase().includes(keyword)
|
item.tag.toLowerCase().includes(keyword)
|
||||||
)
|
)
|
||||||
// 在searchItems第一个位置插入一条原本搜索
|
// 在searchItems第一个位置插入一条原本搜索
|
||||||
searchItems.value.unshift({
|
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,
|
menu_link: getDictValue(options.value, "value", broswer.value, "url") + searchWord.value,
|
||||||
tag: ''
|
tag: ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handdleKeyup(e: any) {
|
function handleKeyup(e: KeyboardEvent) {
|
||||||
if (!searchBox.value) return
|
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) {
|
if (selecedIdx.value < searchItems.value.length - 1) {
|
||||||
selecedIdx.value += 1
|
selecedIdx.value += 1
|
||||||
} else {
|
} else {
|
||||||
@ -259,7 +288,7 @@ function handdleKeyup(e: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 向上箭头
|
// 向上箭头
|
||||||
else if (e.keyCode == 38) {
|
else if (e.keyCode === 38) {
|
||||||
if (selecedIdx.value > 0) {
|
if (selecedIdx.value > 0) {
|
||||||
selecedIdx.value -= 1
|
selecedIdx.value -= 1
|
||||||
} else {
|
} else {
|
||||||
@ -267,7 +296,7 @@ function handdleKeyup(e: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 回车键
|
// 回车键
|
||||||
else if (e.keyCode == 13) {
|
else if (e.keyCode === 13) {
|
||||||
if (searchItems.value.length > 0) {
|
if (searchItems.value.length > 0) {
|
||||||
const selectedItem = searchItems.value[selecedIdx.value]
|
const selectedItem = searchItems.value[selecedIdx.value]
|
||||||
window.open(selectedItem.menu_link, "_BLANK")
|
window.open(selectedItem.menu_link, "_BLANK")
|
||||||
@ -281,19 +310,23 @@ function handdleKeyup(e: any) {
|
|||||||
|
|
||||||
// 2秒后自动隐藏菜单
|
// 2秒后自动隐藏菜单
|
||||||
const hideMenu = () => {
|
const hideMenu = () => {
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
menuShow.value = false
|
menuShow.value = false
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeTimer = () => {
|
const removeTimer = () => {
|
||||||
|
if (timer) {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handdleMenuItem(type: number) {
|
function handleMenuItem(type: number) {
|
||||||
menuShow.value = false
|
menuShow.value = false
|
||||||
currentClickedItem.value = type
|
currentClickedItem.value = type
|
||||||
if (type == 4) {
|
if (type === 4) {
|
||||||
// 删除导航
|
// 删除导航
|
||||||
if (!currentItem.value) return
|
if (!currentItem.value) return
|
||||||
$modal({
|
$modal({
|
||||||
@ -302,9 +335,8 @@ function handdleMenuItem(type: number) {
|
|||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
submitText: '删除',
|
submitText: '删除',
|
||||||
handdleSubmit: async () => {
|
handdleSubmit: async () => {
|
||||||
const res = await $http.nav.deleteNav(currentItem.value.nid)
|
const res = await $http.nav.deleteNav(currentItem.value?.nid)
|
||||||
console.log('>>> --> handdleMenuItem --> res:', res)
|
if (res.code === 200) {
|
||||||
if (res.code == 200) {
|
|
||||||
$msg.success('删除成功')
|
$msg.success('删除成功')
|
||||||
getNavList()
|
getNavList()
|
||||||
}
|
}
|
||||||
@ -315,27 +347,26 @@ function handdleMenuItem(type: number) {
|
|||||||
editModal.value = true
|
editModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handdleItemCancel() {
|
function handleItemCancel() {
|
||||||
editModal.value = false
|
editModal.value = false
|
||||||
editInput.value = ''
|
editInput.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handdleItemSubmit() {
|
async function handleItemSubmit() {
|
||||||
if (!editInput.value) {
|
if (!editInput.value) {
|
||||||
$msg.error('请输入修改内容')
|
$msg.error('请输入修改内容')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let updateData: any = {}
|
let updateData: Partial<NavItem> = {}
|
||||||
if (currentClickedItem.value == 1) {
|
if (currentClickedItem.value === 1) {
|
||||||
updateData.menu_name = editInput.value
|
updateData.menu_name = editInput.value
|
||||||
} else if (currentClickedItem.value == 2) {
|
} else if (currentClickedItem.value === 2) {
|
||||||
updateData.menu_link = editInput.value
|
updateData.menu_link = editInput.value
|
||||||
} else if (currentClickedItem.value == 3) {
|
} else if (currentClickedItem.value === 3) {
|
||||||
updateData.tag = editInput.value.slice(0, 4)
|
updateData.tag = editInput.value.slice(0, 4)
|
||||||
}
|
}
|
||||||
const res = await $http.nav.editNav(currentItem.value.nid, updateData)
|
const res = await $http.nav.editNav(currentItem.value!.nid!, updateData)
|
||||||
console.log('>>> --> handdleItemSubmit --> res:', res)
|
if (res.code === 200) {
|
||||||
if (res.code == 200) {
|
|
||||||
$msg.success('修改成功')
|
$msg.success('修改成功')
|
||||||
editModal.value = false
|
editModal.value = false
|
||||||
editInput.value = ''
|
editInput.value = ''
|
||||||
@ -350,23 +381,30 @@ async function getNavList() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res = await $http.nav.getNavList()
|
const res = await $http.nav.getNavList()
|
||||||
res.data?.forEach((i: any) => {
|
if (!res.data) return
|
||||||
i.icon_error = false
|
|
||||||
i.first = i.menu_name.at(0)
|
|
||||||
i.color = getRandomDarkColor()
|
|
||||||
});
|
|
||||||
navlist.value = res.data
|
|
||||||
|
|
||||||
console.log("&&&&&&&&&&&&&&", navlist.value);
|
// 合并两个循环,提高性能
|
||||||
res.data.forEach((i: any) => {
|
res.data.forEach((i: any) => {
|
||||||
if (!tagList.value.find((t: any) => t.name == i.tag))
|
i.icon_error = false
|
||||||
tagList.value.push({ name: i.tag.substring(0, 4), color: getRandomDarkColor(30, 128), checked: 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)
|
cloneNavlist = deepclone(res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemColor(item: any) {
|
function getItemColor(item: NavItem) {
|
||||||
const tag = tagList.value.find((t: any) => t.name == item.tag)
|
const tag = tagList.value.find((t) => t.name === item.tag)
|
||||||
return tag ? tag.color : getRandomDarkColor(30, 128)
|
return tag ? tag.color : getRandomDarkColor(30, 128)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,6 +491,8 @@ function addNav() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getIcon() {
|
async function getIcon() {
|
||||||
|
console.log(11111);
|
||||||
|
|
||||||
if (!navData.menu_link) return
|
if (!navData.menu_link) return
|
||||||
const res = await $http.mix.getIcon({
|
const res = await $http.mix.getIcon({
|
||||||
url: navData.menu_link
|
url: navData.menu_link
|
||||||
@ -463,9 +503,15 @@ async function getIcon() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetNav() {
|
||||||
|
navData.menu_link = ''
|
||||||
|
navData.menu_name = ''
|
||||||
|
navData.tag = ''
|
||||||
|
navData.menu_icon = ''
|
||||||
|
}
|
||||||
function navCancel() {
|
function navCancel() {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
formNav.value.resetFields()
|
resetNav()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navSubmit() {
|
async function navSubmit() {
|
||||||
@ -481,14 +527,10 @@ async function navSubmit() {
|
|||||||
if (res.code == 200) {
|
if (res.code == 200) {
|
||||||
$msg.success('添加成功')
|
$msg.success('添加成功')
|
||||||
visible.value = false
|
visible.value = false
|
||||||
formNav.value.resetFields()
|
|
||||||
getNavList()
|
getNavList()
|
||||||
}
|
}
|
||||||
// 清空navData
|
// 清空navData
|
||||||
navData.menu_link = ''
|
resetNav()
|
||||||
navData.menu_name = ''
|
|
||||||
navData.tag = ''
|
|
||||||
navData.menu_icon = ''
|
|
||||||
}
|
}
|
||||||
// 处理右键菜单
|
// 处理右键菜单
|
||||||
function handdleContextMenu(event: MouseEvent, item: any) {
|
function handdleContextMenu(event: MouseEvent, item: any) {
|
||||||
@ -503,7 +545,7 @@ function handdleContextMenu(event: MouseEvent, item: any) {
|
|||||||
|
|
||||||
|
|
||||||
// 监听store的登录状态
|
// 监听store的登录状态
|
||||||
watch(() => usrLog.isLogin, (newVal) => {
|
watch(() => usrLog.isLogin, (newVal: any) => {
|
||||||
console.log('********** --> watch --> newVal:', newVal)
|
console.log('********** --> watch --> newVal:', newVal)
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
getNavList()
|
getNavList()
|
||||||
@ -511,17 +553,96 @@ watch(() => usrLog.isLogin, (newVal) => {
|
|||||||
navlist.value = []
|
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(() => {
|
onMounted(() => {
|
||||||
console.log("&&&&&&&&&&&&&&", nav.navH);
|
|
||||||
contentStyle.value = {
|
contentStyle.value = {
|
||||||
height: `${window.innerHeight - nav.navH}px)`
|
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 = {
|
navStyle.value = {
|
||||||
// height: `calc(100vh - ${navcards.value.getBoundingClientRect().y}px - ${nav.navH}px)`,
|
// 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 = [
|
tagList.value = [
|
||||||
{
|
{
|
||||||
@ -536,7 +657,7 @@ onMounted(() => {
|
|||||||
height: `calc(100vh - ${nav.navH}px)`
|
height: `calc(100vh - ${nav.navH}px)`
|
||||||
}
|
}
|
||||||
navStyle.value = {
|
navStyle.value = {
|
||||||
height: `${window.innerHeight - navcards.value.getBoundingClientRect().top - 20}px`
|
height: `${navHeight.value}px`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
window.addEventListener('keyup', (e: Event) => {
|
window.addEventListener('keyup', (e: Event) => {
|
||||||
|
|||||||
@ -1,20 +1,243 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="plink-page">
|
<div class="plink-page pt-4 flex justify-between" :style="boxStyle">
|
||||||
<h1>友链页</h1>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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({
|
definePage({
|
||||||
name:'plink',
|
name: 'plink',
|
||||||
meta: {
|
meta: {
|
||||||
title: '友链',
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
@ -1,22 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="widget-page">
|
<div class="widget-page">
|
||||||
<h1>工具页</h1>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
221
src/views/blog/[bid].vue
Normal 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
@ -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>
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
这里是控制台首页
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
//mark import
|
|
||||||
|
|
||||||
//mark data
|
|
||||||
|
|
||||||
//mark method
|
|
||||||
|
|
||||||
//mark 周期、内置函数等
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@ -11,6 +11,7 @@
|
|||||||
],
|
],
|
||||||
"exclude": ["src/**/__tests__/*"],
|
"exclude": ["src/**/__tests__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "es2021",
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"types":["unplugin-vue-router/client"],
|
"types":["unplugin-vue-router/client"],
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
@ -11,7 +11,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "es2021",
|
||||||
|
"lib": ["es2021.string", "dom"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
|
|||||||
18
typed-router.d.ts
vendored
@ -18,9 +18,9 @@ declare module 'vue-router/auto-routes' {
|
|||||||
* Route name map generated by unplugin-vue-router
|
* Route name map generated by unplugin-vue-router
|
||||||
*/
|
*/
|
||||||
export interface RouteNamedMap {
|
export interface RouteNamedMap {
|
||||||
'appshare': RouteRecordInfo<'appshare', '/AppShare', Record<never, never>, Record<never, never>>,
|
'apps': RouteRecordInfo<'apps', '/Apps', Record<never, never>, Record<never, never>>,
|
||||||
'article': RouteRecordInfo<'article', '/Article', Record<never, never>, Record<never, never>>,
|
'blog': RouteRecordInfo<'blog', '/blog', Record<never, never>, Record<never, never>>,
|
||||||
'/console/home': RouteRecordInfo<'/console/home', '/console/home', 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>>,
|
'gallery': RouteRecordInfo<'gallery', '/Gallery', Record<never, never>, Record<never, never>>,
|
||||||
'home': RouteRecordInfo<'home', '/Home', 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>>,
|
'404': RouteRecordInfo<'404', '/NotFound', Record<never, never>, Record<never, never>>,
|
||||||
@ -39,16 +39,16 @@ declare module 'vue-router/auto-routes' {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export interface _RouteFileInfoMap {
|
export interface _RouteFileInfoMap {
|
||||||
'src/views/AppShare.vue': {
|
'src/views/Apps.vue': {
|
||||||
routes: 'appshare'
|
routes: 'apps'
|
||||||
views: never
|
views: never
|
||||||
}
|
}
|
||||||
'src/views/Article.vue': {
|
'src/views/blog/index.vue': {
|
||||||
routes: 'article'
|
routes: 'blog'
|
||||||
views: never
|
views: never
|
||||||
}
|
}
|
||||||
'src/views/console/home.vue': {
|
'src/views/blog/[bid].vue': {
|
||||||
routes: '/console/home'
|
routes: 'blog/:bid'
|
||||||
views: never
|
views: never
|
||||||
}
|
}
|
||||||
'src/views/Gallery.vue': {
|
'src/views/Gallery.vue': {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export default defineConfig({
|
|||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: "#ec66ab",
|
primary: "#ec66ab",
|
||||||
|
deepp: "#a7415d"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { fileURLToPath, URL } from "node:url";
|
|||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import UnoCSS from "unocss/vite";
|
import UnoCSS from "unocss/vite";
|
||||||
import AutoImport from "unplugin-auto-import/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 Components from "unplugin-vue-components/vite";
|
||||||
import { VueRouterAutoImports } from 'unplugin-vue-router';
|
import { VueRouterAutoImports } from "unplugin-vue-router";
|
||||||
import VueRouter from 'unplugin-vue-router/vite';
|
import VueRouter from "unplugin-vue-router/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
// import vueDevTools from "vite-plugin-vue-devtools";
|
// import vueDevTools from "vite-plugin-vue-devtools";
|
||||||
import svgLoader from "vite-svg-loader";
|
import svgLoader from "vite-svg-loader";
|
||||||
@ -20,11 +20,18 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
|
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||||
imports: ["vue", "pinia", VueRouterAutoImports],
|
imports: [
|
||||||
|
"vue",
|
||||||
|
"pinia",
|
||||||
|
VueRouterAutoImports,
|
||||||
|
{
|
||||||
|
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Components({
|
Components({
|
||||||
resolvers: [DevUiResolver()],
|
resolvers: [NaiveUiResolver()],
|
||||||
dirs: ["src/components"],
|
dirs: ["src/components"],
|
||||||
}),
|
}),
|
||||||
UnoCSS(),
|
UnoCSS(),
|
||||||
@ -39,7 +46,7 @@ export default defineConfig({
|
|||||||
base: "/blog/",
|
base: "/blog/",
|
||||||
server: {
|
server: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 8080,
|
port: 8989,
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||