first commit

This commit is contained in:
heixinyouzi 2024-05-22 14:45:45 +08:00
commit f69367d3f4
116 changed files with 8594 additions and 0 deletions

7
.env.development Normal file
View File

@ -0,0 +1,7 @@
# 开发环境配置
ENV = 'development'
VITE_APP_ENV = 'development'
#VITE_APP_BASE_URL = 'http://127.0.0.1:7777'
VITE_APP_BASE_URL = 'https://www.hxyouzi.com'

5
.env.production Normal file
View File

@ -0,0 +1,5 @@
# 生产环境配置
ENV = 'production'
VITE_APP_ENV = 'production'
VITE_APP_BASE_URL = 'https://www.hxyouzi.com'

View File

@ -0,0 +1,46 @@
name: Build and Deploy
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout
uses: https://gitea.cn/actions/checkout@v3
- id: tool-cache
name: 安装node
uses: https://gitea.com/kongxiangyiren/gitea-tool-cache@v4
with:
# 只有node支持版本号别名
node-version: 18
- uses: https://gitea.cn/actions/setup-node@v3
with:
# gitea-tool-cache导出 node 具体版本
node-version: ${{ steps.tool-cache.outputs.node-version }}
registry-url: 'https://registry.npmmirror.com'
- name: 缓存
uses: https://gitea.cn/actions/cache@v3
id: cache
with:
path: node_modules
key: ${{ runner.os }}-4
- name: Install and Build # 下载依赖 打包项目
run: |
yarn install
yarn build
- name: Appleboy
uses: https://gitcode.net/weixin_44697517/scp-action@v0.1.4
with:
host: ${{ secrets.USER_HOST }} # 服务器地址: xxx.xxx.xxx.xxx
username: ${{ secrets.USER_NAME }} # 服务器名字 一般是root
key: ${{ secrets.SERVER_SSH_KEY }} # 服务器连接密钥
source: './dist/' # 拷贝文件目录
target: ${{ secrets.USER_TARGET }} # 服务器目标目录
strip_components: 2

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

14
README.md Normal file
View File

@ -0,0 +1,14 @@
# 个人练手
## 主要功能
### √ 1.登录注册功能
### √ 2.首页展示共享文章列表
### √ 3.画廊页面展示共享图片
### × 4.控制台功能 --目前已完成:画廊上传共享
### √ 5.文章编辑功能
### 其余功能正在考虑中

71
index.html Normal file
View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>黑心柚子</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</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>

11
niva.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "blog",
"uuid": "3056dae6-4b74-4e53-b77a-d8fe1f09c04d",
"debug": {
"entry": "http://localhost:5173",
"resource": "public"
},
"build": {
"resource": "dist"
}
}

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "blog",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 4173"
},
"dependencies": {
"@vant/auto-import-resolver": "^1.1.0",
"@vant/touch-emulator": "^1.4.0",
"@vicons/ionicons5": "^0.12.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"aplayer": "^1.10.1",
"axios": "^1.4.0",
"chinese-workday": "^1.10.0",
"colorofchina": "^1.0.6",
"gsap": "^3.12.2",
"less": "^4.1.3",
"naive-ui": "^2.38.1",
"nprogress": "^0.2.0",
"pinia": "^2.0.17",
"qs": "^6.11.2",
"qweather-icons": "^1.3.3",
"rapidoc": "^9.3.4",
"unplugin-auto-import": "^0.15.2",
"unplugin-vue-components": "^0.24.1",
"vant": "^4.8.7",
"vfonts": "^0.0.3",
"vite-svg-loader": "^4.0.0",
"vue": "^3.2.37",
"vue-cookies": "^1.8.3",
"vue-router": "^4.1.3",
"vue-wechat-title": "^2.0.7"
},
"devDependencies": {
"@vicons/ionicons4": "^0.12.0",
"@vitejs/plugin-vue": "^3.0.1",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.22",
"tailwindcss": "^3.3.1",
"vite": "^3.0.4"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/bg/bg-d.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

BIN
public/bg/bg-l.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

20
src/App.vue Normal file
View File

@ -0,0 +1,20 @@
<script setup>
import themeOverrides from "@/util/theme.js"; //
import { dateZhCN, zhCN } from "naive-ui";
import idx from "./index.vue"; //
</script>
<template>
<n-config-provider :locale="zhCN" :date-locale="dateZhCN" :theme="null" class="config" :theme-overrides="themeOverrides.lightOverrides">
<n-message-provider>
<n-dialog-provider>
<n-notification-provider>
<idx />
</n-notification-provider>
</n-dialog-provider>
</n-message-provider>
</n-config-provider>
</template>
<style scoped></style>

26
src/api/addr/index.js Normal file
View File

@ -0,0 +1,26 @@
import request from "@/util/request";
//getAddressByIP
export function getAddressByIP(params) {
return request({
url: "/addr/ip",
method: "get",
params,
});
}
//SendMail
export function sendMail(data) {
return request({
url: "/addr/mail",
method: "post",
data,
});
}
//SendMailinpass
export function sendMailinpass(data) {
return request({
url: "/addr/mailinpass",
method: "post",
data,
});
}

49
src/api/art/index.js Normal file
View File

@ -0,0 +1,49 @@
import request from "@/util/request";
//queryArt
export function queryArt() {
return request({
url: "/art/q",
method: "get",
});
}
//addArt
export function addArt(data) {
return request({
url: "/art/add",
method: "post",
data,
});
}
// queryArtInfo
export function queryArtInfo(id) {
return request({
url: "/art/getInfoById?id=" + id,
method: "get",
});
}
// myArti
export function myArti() {
return request({
url: "/art/myArticle",
method: "get",
});
}
// deleteArti
export function deleteArti(id) {
return request({
url: "/art/article?id=" + id,
method: "delete",
});
}
// updArt
export function updArt(data) {
return request({
url: "/art/article",
method: "put",
data,
});
}

48
src/api/file/index.js Normal file
View File

@ -0,0 +1,48 @@
import request from "@/util/request";
//MyFile
export function MyFile() {
return request({
url: "/file/myfile",
method: "get",
});
}
//ShareFile
export function ShareFile(params) {
return request({
url: "/f/sharefile",
method: "get",
params,
});
}
export function updateFile(id) {
return request({
url: "/file/" + id,
method: "put",
});
}
export function unUpdateFile(id) {
return request({
url: "/file/un/" + id,
method: "put",
});
}
export function delFile(id) {
return request({
url: "/file/" + id,
method: "delete",
});
}
export function updateName(data) {
return request({
url: "/file/updatename",
method: "post",
data,
});
}
export function listMusic() {
return request({
url: "/f/music",
method: "get",
});
}

14
src/api/index.js Normal file
View File

@ -0,0 +1,14 @@
const files = import.meta.glob('./*/index.js', {
eager: true,
})
const http = {}
// console.log(files);
for (const i in files) {
const t = i.split("/")
const name = t[1]
http[name] = files[i]
}
export default http

9
src/api/mix/index.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/util/request';
//poetry
export function poetry() {
return request({
url: '/mix/poetry',
method: 'get'
});
}

9
src/api/nav/index.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/util/request';
// listNav
export function listNav() {
return request({
url: '/nav/menu',
method: 'get'
});
}

9
src/api/news/index.js Normal file
View File

@ -0,0 +1,9 @@
import request from '@/util/request';
//listBaiduNews
export function listBaiduNews() {
return request({
url: '/news/baidu',
method: 'get'
});
}

24
src/api/plink/index.js Normal file
View File

@ -0,0 +1,24 @@
import request from '@/util/request';
//listPlink
export function listPlink() {
return request({
url: '/plink/all',
method: 'get'
});
}
// tagList
export function tagList() {
return request({
url: '/plink/tag',
method: 'get'
});
}
export function addPlink(){
return request({
url: '/plink/add',
method: 'post'
});
}

50
src/api/user/index.js Normal file
View File

@ -0,0 +1,50 @@
import request from "@/util/request";
//login
export function login(data) {
return request({
url: "/user/login",
method: "post",
data,
});
}
//register
export function regis(data) {
return request({
url: "/user/register",
method: "post",
data,
});
}
//autologin
export function alogin() {
return request({
url: "/info/autologin",
method: "post",
});
}
//rexpass
export function rexpass(data) {
return request({
url: "/user/respass",
method: "post",
data,
});
}
//updatepass
export function updatepass(data) {
return request({
url: "/user/updatepass",
method: "put",
data,
});
}
//updatetel
export function updatetel(data) {
return request({
url: "/info/update/tel",
method: "put",
data,
});
}

32
src/api/wea/index.js Normal file
View File

@ -0,0 +1,32 @@
import request from '@/util/request';
//getWeather
export function getWeather(params) {
return request({
url: '/wea/now',
method: 'get',
params
});
}
//getJq
export function getJq() {
return request({
url: '/wea/getJq',
method: 'get'
});
}
//getNextHoliday
export function getNextHoliday() {
return request({
url: '/wea/getNextHoliday',
method: 'get'
});
}
//iswork
export function iswork() {
return request({
url: '/wea/iswork',
method: 'get'
});
}

BIN
src/assets/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
src/assets/bro/baidu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
src/assets/bro/bing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
src/assets/bro/google.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
src/assets/font/LCDML.woff2 Normal file

Binary file not shown.

BIN
src/assets/offwork.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/onwork.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

View File

@ -0,0 +1,12 @@
@purple: #8a2be2;
@orange: #ffa500;
@blue: #409eff;
@dp: #f2e8fc;
@ps:0 2px 12px 0 #5C169F10;
//#BE8AEF #B172EC #A45BE9 #9744E5 #8A2BE2 #7B1DD3 #6C19B9 #5C169F
:root:root {
--van-primary-color: #8a2be2;
}

65
src/assets/style/main.css Normal file
View File

@ -0,0 +1,65 @@
/* @import "./base.css"; */
/* .n-scrollbar-rail.n-scrollbar-rail--vertical {
right: 0 !important;
}
.n-scrollbar-rail__scrollbar {
background-color: #409eff !important;
} */
.n-button {
background-color: var(--n-color) !important;
}
.n-scrollbar-rail--vertical {
right: 0 !important;
}
body {
overflow: hidden;
}
/* .a1111111111 {
cursor: url("@/cursor/help_26x26.png"), help;
cursor: url("@/cursor/link_26x26.png"), pointer;
cursor: url("@/cursor/working.png"), progress;
cursor: url("@/cursor/busy.png"), wait;
cursor: url("@/cursor/cross.png"), crosshair;
cursor: url("@/cursor/text_26x26.png"), text;
cursor: url("@/cursor/move_26x26.png"), move;
cursor: url("@/cursor/unavailable.png"), not-allowed;
cursor: url("@/cursor/unavailable.png"), no-drop;
cursor: url("@/cursor/cross") all-scroll;
cursor: url("@/cursor/horiz_26x26.png"), col-resize;
cursor: url("@/cursor/horiz_26x26.png"), w-resize;
cursor: url("@/cursor/horiz_26x26.png"), e-resize;
cursor: url("@/cursor/horiz_26x26.png"), ew-resize;
cursor: url("@/cursor/vert.png"), row-resize;
cursor: url("@/cursor/vert.png"), n-resize;
cursor: url("@/cursor/vert.png"), s-resize;
cursor: url("@/cursor/vert.png"), ns-resize;
cursor: url("@/cursor/diag1.png"), nw-resize;
cursor: url("@/cursor/diag1.png"), se-resize;
cursor: url("@/cursor/diag1.png"), nwse-resize;
cursor: url("@/cursor/diag2_26x26.png"), ne-resize;
cursor: url("@/cursor/diag2_26x26.png"), sw-resize;
cursor: url("@/cursor/diag2_26x26.png"), nesw-resize;
cursor: url("@/cursor/handwriting_26x26.png"), copy;
cursor: url("@/cursor/alt.png"), grab;
} */
div {
cursor: url("@/cursor/normal.png"), default !important;
}
a,
button,
li,
.aplayer-list-author,
.aplayer-icon,
.n-back-top,
.n-tabs-tab,
.n-input__eye,
.n-image:not(.n-image--preview-disabled) {
cursor: url("@/cursor/link_26x26.png"), pointer !important;
}
input {
cursor: url("@/cursor/text_26x26.png"), text !important;
}
#plink .v-binder-follower-content {
width: 20vw;
}

View File

@ -0,0 +1,170 @@
<template>
<!-- 准备一个容器用来存放音乐播放器 -->
<div id="aplayer"></div>
</template>
<script setup>
// https://aplayer.js.org/#/zh-Hans/
// import Hls from 'hls.js/dist/hls.min.js';
import APlayer from "aplayer"; //
import "aplayer/dist/APlayer.min.css"; //
import { onMounted, reactive } from "vue";
// window.Hls = Hls;
const data = reactive({
audio: [],
info: {
fixed: true, //
// mini: false, //
// autoplay: true, // ()
// theme: '#000', //
loop: "all", // , : 'all', 'one', 'none'
order: "list", // , : 'list', 'random'
preload: "metadata", //: 'none', 'metadata', 'auto'
volume: 0.5, // : 0-1
mutex: true, // true
lrcType: 3, // : 1, 2, 3
},
});
onMounted(async () => {
// if (import.meta.env.VITE_APP_ENV != "development") {
const res = await $http.file.listMusic();
data.audio = [];
res.data.forEach(i => {
let name = i.split(".")[0];
let t = name.split("-");
let mid = name + "/"+ name
data.audio.push({
title: t[1],
author: t[0],
url: res.baseUrl + mid +'.mp3',
pic: res.baseUrl + mid + ".jpg",
lrc: res.baseUrl + mid + ".lrc",
});
console.log('>>>>>',data.audio);
});
// } else {
// const d = {
// title: "",
// author: "",
// url: "https://www.hxyouzi.com/music/ - .mp3",
// pic: "https://www.hxyouzi.com/img/cover/ - .jpg",
// lrc: "/ - .lrc",
// };
// data.audio.push(d, d, d, d, d, d, d, d, d, d, d);
// }
// DOM
const ap = new APlayer({
container: document.getElementById("aplayer"),
audio: data.audio, //
...data.info, //
});
const d = document.querySelector("#aplayer > div.aplayer-body > div.aplayer-miniswitcher > button");
let flag = true;
const app = document.querySelector("#aplayer > div.aplayer-lrc");
app.style.display = "block";
app.style.transform = "translateY(50px)";
d.addEventListener("click", () => {
if (flag) {
app.style.transform = "translateY(0)";
ap.lrc.show();
flag = false;
} else {
app.style.transform = "translateY(50px)";
ap.lrc.hide();
ap.list.hide();
flag = true;
}
});
// d.click();
});
</script>
<style lang="less" scoped>
#aplayer {
width: 480px; //
color: var(--n-text-color); //
background-color: #f5f5f8; //
:deep(.aplayer-body) {
color: var(--n-text-color); //
background-color: #f5f5f8; //
// bottom: 10px; //
.aplayer-icon-lrc {
display: none;
}
}
:deep(.aplayer-icon):hover svg path {
fill: @blue; //
}
:deep(.aplayer-list) {
width: 418px !important;
background-color: #f5f5f8;
border: none;
}
:deep(.aplayer-list-light) {
background: inherit;
color: @orange;
}
:deep(.aplayer-list) ol {
max-height: 160px !important;
// margin-bottom: 12px;
.aplayer-list-light .aplayer-list-author {
color: @orange !important;
}
}
:deep(.aplayer-list) ol li {
border: none;
}
:deep(.aplayer-list) ol li:hover {
background: inherit;
color: @blue;
border: none;
.aplayer-list-author {
color: @blue !important;
}
}
:deep(.aplayer-info) {
padding: 0 !important;
border-top: none;
}
:deep(.aplayer-icon) svg {
color: var(--n-text-color); //
background-color: var(--v-bgc); //
}
:deep(.aplayer-icon):hover svg {
color: @blue; //
}
:deep(.aplayer-miniswitcher) {
color: var(--n-text-color); //
background-color: var(--v-bgc); //
}
:deep(.aplayer-lrc) {
// text-align: right !important;
width: 368px !important;
transition: all 0.3s ease;
// left: unset;
bottom: 12px;
z-index: 999;
text-shadow: none !important;
}
:deep(.aplayer-lrc-contents) {
transition: all 0.5s ease !important;
p {
font-size: 12px !important;
color: @purple !important;
opacity: 0.8;
}
}
:deep(.aplayer-lrc-contents) {
.aplayer-lrc-current {
opacity: 1;
font-size: 13px !important;
color: @orange !important;
}
}
}
@media screen and (max-width: 800px) {
#aplayer {
width: 100vw !important;
}
}
</style>

80
src/components/arti.vue Normal file
View File

@ -0,0 +1,80 @@
<template>
<TransitionGroup name="list" tag="div" @enter="onEnter" @before-enter="onBeforeEnter">
<n-card v-for="(item, index) in list" size="small" :key="item.id" :bordered="false"
@click="handdleClickArti(item.id)" :data-index="index">
<div class="titel mt-2">
<n-h6 prefix="bar">
<n-text type="primary">
{{ item.title }}
</n-text>
</n-h6>
</div>
<div class="cont">
<div class="pro">{{ item.pro }}</div>
<div class="author mb-2 mr-4">{{ item.nickname }}</div>
</div>
</n-card>
</TransitionGroup>
</template>
<script setup>
//mark import
import gsap from 'gsap';
//mark data
const router = useRouter()
const props = defineProps({
list: {
type: Array,
default: () => []
}
})
//mark method
function handdleClickArti(id) {
router.push('/mini/' + id)
}
function onBeforeEnter(el) {
el.style.opacity = 0
if (el.dataset.index % 2 == 0)
el.style.transform = 'translateX(-500px)'
else
el.style.transform = 'translateX(500px)'
}
function onEnter(el, done) {
gsap.to(el, {
opacity: 1,
x: 0,
ease: 'slow',
delay: el.dataset.index * 0.4,
onComplete: done
})
}
</script>
<style scoped lang="less">
:deep(.n-card) {
width: 90%;
margin: 0 0 20px 5%;
box-shadow: @ps;
background-color: #fafbfc;
}
:deep(.n-card__content) {
padding: 0 !important;
}
.pro {
text-indent: 20px;
}
.author {
text-align: right;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div>
<n-menu v-model:value="activeKey" :collapsed="collapsed" inverted :collapsed-width="64" :icon-size="20" :collapsed-icon-size="26" :options="menuOptions" />
</div>
</template>
<script setup>
import menuInfo from "@/util/consoleMenu.js";
const props = defineProps({
collapsed: Boolean,
});
const activeKey = ref("0");
const route = useRoute()
const { path } = route;
menuInfo.menuList.forEach((i, idx) => {
if (path == i) {
activeKey.value = idx + "";
}
});
let menuOptions = ref([]);
menuOptions.value = menuInfo.options;
</script>
<style scoped lang="less">
:deep(.orange) {
svg {
color: #fff;
}
}
</style>

152
src/components/edition.vue Normal file
View File

@ -0,0 +1,152 @@
<template>
<div class="w-[100%] h-[100%] relative">
<div class=" mb-6">
<n-input v-model:value="info.title" type="text" placeholder="请输入标题..." size="medium" clearable>
<template #prefix>
<n-button type="primary" size="mini" text>标题</n-button>
</template>
</n-input>
</div>
<div class=" mb-6">
<n-input v-model:value="info.pro" type="text" placeholder="请输入简介..." size="medium" clearable>
<template #prefix>
<n-button type="primary" size="mini" text>简介</n-button>
</template>
</n-input>
</div>
<div class="" style="border:1px solid #ccc;border-radius: 4px;overflow: hidden;">
<Toolbar style="border-bottom: 1px solid #ccc;font-size: 12px;" :editor="editorRef" :defaultConfig="toolbarConfig"
:mode="mode" />
<Editor style="height: 500px; overflow-y: hidden;" v-model="info.cont" :defaultConfig="editorConfig" :mode="mode"
@onCreated="handleCreated" />
</div>
<div class="btns absolute bottom-0 w-[100%]">
<n-button type="primary" @click="handleSubmit">提交</n-button>
</div>
</div>
</template>
<script setup>
//mark import
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import { onMounted, watchEffect } from "vue";
import cookie from "vue-cookies";
//mark data
const props = defineProps({
id: {
type: [String, Number],
default: -1
}
})
const info = reactive({
cont: "",
title: "",
pro: ""
})
const mode = 'simple' // 'simple','default'
const toolbarConfig = {}
const editorRef = shallowRef()
const editorConfig = ref({ MENU_CONF: {} })
const emit = defineEmits(['close'])
//mark method
const handleCreated = (editor) => {
editorRef.value = editor
}
//mark
editorConfig.value.placeholder = '请输入内容...'
toolbarConfig.excludeKeys = ['fullScreen', 'header1','header2','header3', 'emoji', 'line-height', 'headings', 'list-ul', 'list-ol', 'line', 'hr', 'link', '|', 'image', 'code', 'code-theme', 'insertVideo']
editorConfig.value.MENU_CONF['uploadImage'] = {
server: "https://www.hxyouzi.com/api/art/upimg",
fieldName: 'file',
headers: {
Authorization: cookie.get("token"),
},
maxNumberOfFiles: 1,
//
customInsert(res, insertFn) {
console.log(res);
if (res.code == 1) {
const url = res.completePath
const alt = ""
const href = res.completePath
insertFn(url, alt, href)
}
},
}
watchEffect(async () => {
console.log(props.id);
if (props.id != -1) {
const res = await $http.art.queryArtInfo(props.id)
console.log(res);
info.cont = res.data[0].cont
info.title = res.data[0].title
info.pro = res.data[0].pro
} else {
info.cont = ''
info.title = ''
info.pro = ''
}
})
async function handleSubmit() {
if (info.title == '') {
$msg.error("标题不能为空");
return
}
if (info.pro == '') {
$msg.error("简介不能为空");
return
}
console.log(info.cont);
if (info.cont == '' || info.cont == '<p><br></p>') {
$msg.error("内容不能为空");
return
}
if (parseInt(props.id) < 0) {
const res = await $http.art.addArt(info)
if (res.code == 1) {
$msg.success(res.msg)
emit('close')
}
else $msg.error(res.msg)
} else {
const res = await $http.art.updArt({
id: props.id,
...info
})
if (res.code == 1) {
$msg.success(res.msg)
emit('close')
}
else $msg.error(res.msg)
}
}
onMounted(() => {
// console.log(editorRef.value);
})
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
</script>
<style scoped lang="less">
:deep(.w-e-bar-item button) {
padding: 0 3px;
font-size: 12px;
}
:deep(.n-button) {
width: 100%;
}
</style>

141
src/components/falls.vue Normal file
View File

@ -0,0 +1,141 @@
<template>
<div class="box flex" ref="curcomp">
<div class="col" v-for="item in fileArr">
<div v-for="i in item">
<slot :file="i"></slot>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, watch } from "vue";
const prop = defineProps({
fileList: {
type: Array,
default: () => [],
},
count: [String, Number],
pathname: String,
bot: {
type: [String, Number],
default: 20
},
imgW: {
type: [String, Number],
default: 200
},
});
const { fileList, count, pathname, bot, imgW } = toRefs(prop);
const list = reactive({})
const lh = ref([])
const fileArr = ref([])
const originList = ref([])
initList(count.value)
watch(() => count.value, init)
watch(() => fileList.value, initPart, { immediate: true })
function initPart() {
console.log("initPart", fileList.value);
if (fileList.value.length < 1) {
// console.log("query");
const c = count.value
initList(c)
}
fileArr.value = []
originList.value = originList.value.concat(fileList.value)
const c = count.value
getlist(fileList.value)
for (let i = 0; i < c; i++) {
fileArr.value.push(list["file" + i])
}
}
function init() {
fileArr.value = []
// console.log("init",fileList.value);
const c = count.value
// console.log("--->", c);
initList(c)
getlist(originList.value)
// console.log("list", list);
for (let i = 0; i < c; i++) {
fileArr.value.push(list["file" + i])
}
}
function initList(c) {
lh.value = []
for (let i = 0; i < c; i++) {
list["file" + i] = []
lh.value.push(0)
}
}
function getlist(fl) {
if (lh.value.length != count.value) return
fl.forEach(async i => {
const { height, width } = await getImageSizeByCheck(i[pathname.value])
const index = getlhmIndex()
const h = imgW.value * height / width
list["file" + index].push(i)
// console.log("^^^",index,h,list["file" + index]);
lh.value[index] += (h + Number(bot.value))
})
}
function getImageSize(url) {
return new Promise(function (resolve, reject) {
let image = new Image();
image.onload = function () {
resolve(image.height);
};
image.onerror = function () {
reject(new Error('error'));
};
image.src = url;
});
}
function getImageSizeByCheck(url) {
return new Promise(function (resolve, reject) {
let image = new Image();
image.src = url;
let height = 0
let width = 0
let timer = setInterval(() => {
if (image.width > 0 && image.height > 0) {
height = image.height
width = image.width
resolve({ height, width })
clearInterval(timer)
}
}, 40)
});
}
function getlhmIndex() {
const min = Math.min(...lh.value)
// console.log(">>>>>>",lh.value,min,lh.value.findIndex(i => i == min));
return lh.value.findIndex(i => i == min)
}
</script>
<style lang="less" scoped>
.col:not(:last-child) {
margin-right: 20px;
}
@media screen and (max-width:1000px) {
.col:not(:last-child) {
margin-right: 10px;
}
}
</style>

36
src/components/foot.vue Normal file
View File

@ -0,0 +1,36 @@
<template>
<div class="beian" ref="footer">
<a href="https://beian.miit.gov.cn" target="_blank">皖ICP备2021017362号-1</a>
<a class="swag" target="_blank" href="https://www.hxyouzi.com/swag">api文档</a>
</div>
</template>
<script setup>
import { usesizeStore } from '@/stores/size.js';
import { onMounted } from 'vue';
const footer = ref(null);
const size = usesizeStore();
onMounted(()=>{
size.setFootH(footer.value.clientHeight)
})
</script>
<style scoped lang="less">
.beian {
padding: 20px 0;
display: flex;
justify-content: center;
background-color: @mozi;
}
.beian a {
font-size: 12px;
text-decoration: none;
color: @blue;
}
.beian .swag {
margin-left: 20px;
}
</style>

189
src/components/menu.vue Normal file
View File

@ -0,0 +1,189 @@
<template>
<div class="box flex justify-between items-center md:py-1" ref="menuH">
<div class="md:hidden">
<n-menu v-model:value="activeKey" :options="menuInfo.options" mode="horizontal" />
</div>
<n-icon size="30" class="menuIcon ml-2 hidden md:block text-pp-500" @click="active = true">
<MdMenu />
</n-icon>
<div class="right flex items-center">
<div class="wea text-white md:hidden">
<wea />
</div>
<div v-if="!userinfo" class="login mx-8">
<n-button color="#8a2be2" size="small" @click="goLogin">登录</n-button>
</div>
<div v-else class="uinfo flex items-center">
<n-dropdown :options="userOp" @select="handleSelect" :show-arrow="true">
<div class="uinfo mx-8 text-white flex items-center">
<n-avatar round size="small" :src="userinfo.avaUrl" />
<div class="ml-2"></div>
<n-button text class="userinfo">
{{ userinfo.nickname }}
</n-button>
</div>
</n-dropdown>
</div>
</div>
</div>
<n-drawer v-model:show="active" width="40%" :auto-focus="false" placement="left" show-mask="transparent">
<n-drawer-content>
<template #header>
<div class="flex items-center" :style="{ height: menuH.clientHeight + 'px' }">
<n-icon size="24" class="orange mx-2">
<orange />
</n-icon>
菜单
</div>
</template>
<n-menu v-model:value="activeKey" mode="vertical" :options="menuInfo.options.slice(1)"
@click="active = false"></n-menu>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import wea from "@/components/wea.vue";
import orange from "@/icon/orange.svg";
import { usesizeStore } from "@/stores/size.js";
import { renderIcon } from "@/util/h.js";
import menuInfo from "@/util/menu.js";
import { IosLogOut, IosSettings, MdHome, MdMenu } from "@vicons/ionicons4";
import cookie from "vue-cookies";
import { useRoute, useRouter } from "vue-router";
let activeKey = ref("1");
const active = ref(false);
const menuH = ref(null);
const sizeStore = usesizeStore();
let { setMenuH } = sizeStore
const route = useRoute()
const { path, fullPath } = route;
const router = useRouter()
const { push } = router;
let userinfo = ref(cookie.get("userinfo"));
let userOp = [
{
label: "控制台",
key: "profile",
icon: renderIcon(MdHome),
},
{
label: "退出登录",
key: "logout",
icon: renderIcon(IosLogOut),
},
{
label: "设置",
key: "2",
icon: renderIcon(IosSettings),
},
];
menuInfo.menuList.forEach((i, idx) => {
if (path == i) {
activeKey.value = idx + 1 + "";
}
});
onMounted(() => {
// console.log(5555, setMenu);
setMenuH(menuH.value.clientHeight);
});
function goLogin() {
push("/login");
}
function handleSelect(k) {
switch (k) {
case "logout":
logout();
break;
case "profile":
goConsole();
break;
}
}
function logout() {
cookie.remove("token");
cookie.remove("userinfo");
userinfo.value = "";
// log.setLoginStatus(false);
$dialog.success({
title: "登出",
content: "已退出登录,是否重新登录?",
positiveText: "登录",
negativeText: "回首页",
onPositiveClick: () => {
push({
path: "/login",
query: {
redirect: fullPath,
},
});
},
onNegativeClick: () => {
push("/");
},
});
}
function goConsole() {
if (document.documentElement.clientWidth < 1000) {
push("/m");
return
}
push("/console");
}
</script>
<style scoped lang="less">
.login {
:deep(.n-button) {
background-color: @purple;
width: 80px;
}
}
:deep(.orange) {
color: #ffa500;
}
:deep(.menu-item) {
color: @orange !important;
}
:deep(.router-link-active) {
color: @purple !important;
}
:deep(.n-drawer-header),
:deep(.n-drawer-body-content-wrapper) {
padding: 0 !important;
}
:deep(.n-menu-item-content--disabled) {
opacity: unset !important;
}
@media screen and (min-width: 1023px) {
:deep(.n-menu-item-content--selected::after) {
content: "";
display: inline-block;
height: 3px;
width: 70%;
background-color: @purple;
position: absolute;
left: 15%;
bottom: 1px;
}
}
</style>

153
src/components/pass.vue Normal file
View File

@ -0,0 +1,153 @@
<template>
<div>
<n-steps :current="current">
<n-step title="邮箱" description=""></n-step>
<n-step title="修改密码"></n-step>
</n-steps>
<div class="step1 mt-5" v-if="current == 1">
<n-form-item-row label="邮箱">
<n-input v-model:value="form1.username" placeholder="请填写邮箱" @keyup.enter="getCode" />
</n-form-item-row>
<n-form-item-row label="验证码">
<n-input-group>
<n-input v-model:value="form1.code" placeholder="请填写验证码" />
<n-button type="info" @click="getCode">获取验证码</n-button>
</n-input-group>
</n-form-item-row>
</div>
<n-form ref="pass" :model="form2" class="step2 mt-5" v-if="current == 2" :rules="rules">
<n-form-item label="密码" path="password">
<n-input v-model:value="form2.password" type="password" show-password-on="mousedown" placeholder="请输入密码"
@keydown.enter.prevent />
</n-form-item>
<n-form-item label="重复密码" path="repassword">
<n-input ref="reInput" type="password" show-password-on="mousedown" v-model:value="form2.repassword"
placeholder="请再次输入密码" @keyup.enter="" />
</n-form-item>
</n-form>
<div class="btns">
<n-button v-if="current == 1" type="primary" secondary @click="back">返回登录</n-button>
<n-button v-if="current == 2" type="primary" secondary @click="current--">上一步</n-button>
<n-button v-if="current == 1" type="primary" @click="next">下一步</n-button>
<n-button v-if="current == 2" type="primary" @click="submit">确定改密</n-button>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(["back"]);
const pass = ref(null)
const repass = ref(null)
const current = ref(1);
const form1 = reactive({
username: "",
code: "",
});
const form2 = reactive({
username: "",
password: "",
repassword: "",
});
const rules = {
password: [
{ required: true, message: "密码不能为空", trigger: "blur" },
{ validator: validatePassword, message: "必须是含有数字字母的6-12位密码", trigger: "blur" },
],
repassword: [
{ required: true, message: "密码不能为空", trigger: "blur" },
{ validator: validatePassword, message: "必须是含有数字字母的6-12位密码", trigger: "blur" },
{ validator: validatePasswordSame, message: "两次密码输入不一致", trigger: ["blur", "password-input"] },
],
};
function validatePasswordSame(rule, value) {
return value === form2.password;
}
function validatePassword(r, v) {
return /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,12}$/.test(v);
}
async function getCode() {
if (!form1.username) {
$msg.error("请输入邮箱");
return;
}
const res = await $http.addr.sendMailinpass({ mail: form1.username });
console.log(888, res);
if (res.code == "1") $msg.success(res.msg);
else if (res.code == "0") $msg.error(res.msg);
}
async function next() {
if (!form1.username) {
$msg.error("请输入邮箱");
return;
}
if (!form1.code) {
$msg.error("请输入验证码");
return;
}
const res = await $http.user.rexpass(form1);
if (res.code == "1") {
$msg.success(res.msg);
current.value++;
form2.username = form1.username;
} else if (res.code == "0") $msg.error(res.msg);
}
function submit() {
pass.value.validate(async errors => {
if (!errors) {
const res = await $http.user.updatepass(form2);
if (res.code == "1") {
$msg.success(res.msg);
emit("back");
} else if (res.code == "0") $msg.error(res.msg);
} else {
$msg.error(errors[0][0].message)
}
});
}
function back() {
emit("back");
}
</script>
<style scoped lang="less">
.btns {
width: 100%;
display: flex;
justify-content: space-between;
:deep(.n-button) {
width: 45%;
}
}
:deep(.n-step--wait-status) {
.n-step-indicator {
border: 1px solid @blue;
.n-step-indicator-slot__index {
color: @blue;
}
}
.n-step-content-header__title {
color: @blue;
}
}
:deep(.n-step--finish-status) {
.n-step-content-header__title {
color: @purple;
}
}
</style>

42
src/components/router.vue Normal file
View File

@ -0,0 +1,42 @@
<!-- 渐变过渡 -->
<template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<div>
<component :is="Component" />
</div>
</transition>
</router-view>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
<!-- 缩放过渡 -->
<!-- <template>
<router-view v-slot="{ Component }">
<transition name="scale" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<style scoped>
.scale-enter-active,
.scale-leave-active {
transition: all 0.5s ease;
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.9);
}
</style> -->

58
src/components/wea.vue Normal file
View File

@ -0,0 +1,58 @@
<template>
<div class="info">
<div class="loc flex items-center">
<n-icon size="18" color="#8a2be2">
<IosPin />
</n-icon>
{{ addr }}
</div>
<div class="wea">
<i :class="'qiIcon qi-' + now.icon + '-fill'"></i>
<div class="tem">{{ now.text }} {{ now.temp }} </div>
</div>
</div>
</template>
<script setup>
import { IosPin } from "@vicons/ionicons4";
const city = ref("合肥市");
const addr = ref("安徽省合肥市");
let now = ref({});
onMounted(async () => {
const res = await $http.addr.getAddressByIP();
console.log("111", res);
city.value = res.city;
addr.value = ['上海','重庆','北京','天津'].includes(res.province) ? res.country + res.city : res.province + res.city;
const r = await $http.wea.getWeather({ city: city.value });
now.value = r.now;
});
</script>
<style lang="less" scoped>
.info {
display: flex;
// justify-content: space-between;
align-items: center;
.loc {
margin-right: 15px;
:deep(n-icon) {
padding-top: 5px;
}
}
.wea {
display: flex;
// justify-content: space-between;
align-items: center;
.qiIcon {
font-size: 26px;
color: @purple;
}
.tem {
margin-left: 15px;
}
}
}</style>

BIN
src/cursor/alt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/cursor/busy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/cursor/cross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

BIN
src/cursor/diag1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/cursor/diag2_26x26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

BIN
src/cursor/help_26x26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

BIN
src/cursor/horiz_26x26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

BIN
src/cursor/link_26x26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

BIN
src/cursor/loc_26x26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

BIN
src/cursor/move_26x26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

BIN
src/cursor/normal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/cursor/text_26x26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

BIN
src/cursor/unavailable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/cursor/vert.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/cursor/working.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32"><path d="M26 30H11a2.002 2.002 0 0 1-2-2v-6h2v6h15V6h-9V4h9a2.002 2.002 0 0 1 2 2v22a2.002 2.002 0 0 1-2 2z" fill="currentColor"></path><path d="M17 10h7v2h-7z" fill="currentColor"></path><path d="M16 15h8v2h-8z" fill="currentColor"></path><path d="M15 20h9v2h-9z" fill="currentColor"></path><path d="M9 19a5.005 5.005 0 0 1-5-5V3h2v11a3 3 0 0 0 6 0V5a1 1 0 0 0-2 0v10H8V5a3 3 0 0 1 6 0v9a5.005 5.005 0 0 1-5 5z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 549 B

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

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694156756216" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4088" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M379.616 515.648L705.44 841.472c20 20 20 52.416 0 72.416s-52.416 20-72.416 0L270.976 551.84c-20-20-20-52.416 0-72.416l362.048-362.048c20-20 52.416-20 72.416 0s20 52.416 0 72.416L379.616 515.616z" p-id="4089"></path></svg>

After

Width:  |  Height:  |  Size: 552 B

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

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="48px" height="48.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#7B1DD3" d="M822.327949 1008.51541h-620.624867a186.404679 186.404679 0 0 1-186.18746-186.187461v-620.624867a186.404679 186.404679 0 0 1 186.18746-186.18746h620.624867a186.404679 186.404679 0 0 1 186.187461 186.18746v620.624867a186.404679 186.404679 0 0 1-186.187461 186.187461zM510.960453 298.520561a84.063638 84.063638 0 0 0-57.842237 22.590745 98.679354 98.679354 0 0 0-27.090276 35.996243 158.259341 158.259341 0 0 0-10.891966 37.237492 107.399133 107.399133 0 0 0-1.365375 17.129246 123.380224 123.380224 0 0 0 13.002091 55.452832 100.944635 100.944635 0 0 0 39.719992 45.119428l-131.386285 60.883299a24.173339 24.173339 0 0 0-13.684778 23.273433v104.016728a27.400588 27.400588 0 0 0 6.206249 17.811933 18.835965 18.835965 0 0 0 15.050153 7.540593h335.94424a19.487621 19.487621 0 0 0 15.732841-7.540593 27.400588 27.400588 0 0 0 6.206248-17.811933v-104.016728a23.831995 23.831995 0 0 0-13.684778-23.273433l-91.014637-43.754053-38.975241-18.618746a100.199885 100.199885 0 0 0 39.657929-46.546865 131.479378 131.479378 0 0 0 10.954028-52.753114 158.259341 158.259341 0 0 0-3.413436-30.814024 116.615413 116.615413 0 0 0-34.134368-58.959363 84.932513 84.932513 0 0 0-58.959362-22.994151z m158.104185 41.085366a62.062487 62.062487 0 0 0-21.22537 3.72375 86.887481 86.887481 0 0 0-19.177309 9.961029 153.449498 153.449498 0 0 1 8.22328 27.928119 161.021122 161.021122 0 0 1 2.730749 30.069275 157.049123 157.049123 0 0 1-6.516561 45.491802 149.167187 149.167187 0 0 1-18.122246 39.347617 72.706203 72.706203 0 0 0 18.618746 15.763872l67.710173 32.179399a53.001364 53.001364 0 0 1 22.901058 20.852996 58.680081 58.680081 0 0 1 8.595654 31.031243v88.252856h72.520016a15.298403 15.298403 0 0 0 12.412497-5.802842 22.46662 22.46662 0 0 0 4.778812-14.739841v-83.474045a19.735871 19.735871 0 0 0-10.954029-19.177308l-104.699415-49.929271a89.152762 89.152762 0 0 0 29.44865-33.172399 99.299979 99.299979 0 0 0 11.636716-47.570896 101.81351 101.81351 0 0 0-6.206249-35.220461 93.404043 93.404043 0 0 0-16.818934-29.076275 79.408952 79.408952 0 0 0-24.824994-19.549684 68.920392 68.920392 0 0 0-31.000212-6.919967z m-314.780932 0a66.468923 66.468923 0 0 0-30.503713 7.168218 80.898451 80.898451 0 0 0-24.576744 19.549683 94.428074 94.428074 0 0 0-16.756872 29.076275 105.164884 105.164884 0 0 0 5.430468 83.474045 85.335919 85.335919 0 0 0 30.814024 33.172399l-106.002727 49.246583a17.998121 17.998121 0 0 0-10.954029 19.177308v83.474045a22.435589 22.435589 0 0 0 4.716749 14.739841 15.515622 15.515622 0 0 0 12.412497 5.802842h70.534017v-88.252856a58.959362 58.959362 0 0 1 8.533592-31.031243 52.753114 52.753114 0 0 1 23.583745-20.852996l70.471953-33.544774a68.610079 68.610079 0 0 0 15.763872-13.002091 143.240219 143.240219 0 0 1-19.115246-40.030304 154.970029 154.970029 0 0 1-6.919967-46.17449 157.576654 157.576654 0 0 1 11.698778-59.517925 101.223916 101.223916 0 0 0-18.618746-8.905967 60.572987 60.572987 0 0 0-20.48062-3.599624z" /></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

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

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1711591952359" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3959" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M414.72 981.333s-416.427-91.52-230.4-545.92c0 0 42.667 50.56 36.48 74.88 0 0 33.067-114.773 104.533-183.253 61.44-59.093 123.734-224.64 66.347-284.373 0 0 284.8 59.733 316.587 359.04 0 0 36.48-95.36 111.146-104.747a204.587 204.587 0 0 0 0 130.987s235.947 403.84-170.666 540.373c0 0 121.813-138.453-136.534-375.893 0 0-61.013 128-97.28 171.946-0.213 0-101.973 114.134-0.213 216.96z" p-id="3960" fill="#d81e06"></path></svg>

After

Width:  |  Height:  |  Size: 755 B

12
src/icon/index.js Normal file
View File

@ -0,0 +1,12 @@
const icons = import.meta.glob('./*.svg', {
eager: true,
})
const icon = {}
console.log(icons);
for (const i in icons) {
const t = i.split("/")
const name = t[1].split('.')[0]
icon[name] = icons[i]
}
export default icon

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M14.768 15.712l1.513-1.461l-1.513-1.461a.75.75 0 1 1 1.042-1.08l1.886 1.821a1 1 0 0 1 0 1.44l-1.886 1.82a.75.75 0 0 1-1.042-1.079zm-9.534 0L3.72 14.251l1.513-1.461a.75.75 0 0 0-1.042-1.08l-1.886 1.821a1 1 0 0 0 0 1.44l1.886 1.82a.75.75 0 0 0 1.042-1.079zM8 14.252a.75.75 0 0 1-.75.75h-.5a.75.75 0 1 1 0-1.5h.5a.75.75 0 0 1 .75.75zm1.75.75a.75.75 0 1 1 0-1.5h.5a.75.75 0 0 1 0 1.5h-.5zm2.25-.75c0 .415.336.75.75.75h.5a.75.75 0 1 0 0-1.5h-.5a.75.75 0 0 0-.75.75zm5-9.252a2 2 0 0 0-2-2h-10a2 2 0 0 0-2 2v4.25a.75.75 0 0 0 1.5 0V5a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 .5.5v4.25a.75.75 0 1 0 1.5 0V5z" fill="currentColor"></path></g></svg>

After

Width:  |  Height:  |  Size: 755 B

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

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="48px" height="48.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#7B1DD3" d="M873.472 0H150.528C67.584 0 0 67.584 0 150.528v722.944C0 956.416 67.584 1024 150.528 1024h722.944c82.944 0 150.528-67.584 150.528-150.528V150.528C1024 67.584 956.416 0 873.472 0z m-61.952 656.896c0 41.472-33.792 74.752-74.752 74.752H287.232c-41.472 0-74.752-33.28-74.752-74.752V367.104c0-41.472 33.28-74.752 74.752-74.752h449.024c41.472 0 74.752 33.28 74.752 74.752v289.792z" /><path fill="#7B1DD3" d="M712.192 427.008L512 556.032 311.808 427.008c-9.216-6.144-21.504-3.072-27.648 6.144-6.144 9.216-3.072 21.504 6.144 27.648l210.944 135.68c3.072 2.048 7.168 3.072 10.752 3.072 3.584 0 7.68-1.024 10.752-3.072L733.696 460.8a20.48 20.48 0 0 0 6.144-27.648 20.48 20.48 0 0 0-27.648-6.144z" /></svg>

After

Width:  |  Height:  |  Size: 976 B

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

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1711588832708" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6877" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M713.8 523.5c-8.9-8.9-23.2-8.6-32.1 0.3L512.1 693.6 342.7 523.7c-8.9-8.9-23.2-9.1-32.1-0.3v0.5c-8.9 8.9-8.9 23 0 31.9l185.3 185.5c0.5 0.5 1.1 1 1.7 1.5l0.1 0.1c0.5 0.4 1.1 0.8 1.7 1.2 0.1 0.1 0.2 0.1 0.2 0.2 0.5 0.4 1.1 0.7 1.7 1 0.1 0 0.2 0.1 0.3 0.1 0.6 0.3 1.2 0.6 1.8 0.8 0.1 0 0.1 0.1 0.2 0.1 0.6 0.2 1.2 0.5 1.9 0.7h0.2c0.6 0.2 1.3 0.4 2 0.5h0.1c0.7 0.1 1.4 0.2 2 0.3h0.1c0.7 0.1 1.4 0.1 2.1 0.1 0.7 0 1.4 0 2.1-0.1h0.1c0.7-0.1 1.4-0.2 2-0.3h0.1c0.7-0.1 1.3-0.3 2-0.5h0.2c0.6-0.2 1.3-0.4 1.9-0.7 0.1 0 0.1-0.1 0.2-0.1 0.6-0.2 1.2-0.5 1.8-0.8 0.1 0 0.2-0.1 0.3-0.1 0.6-0.3 1.1-0.6 1.7-1 0.1-0.1 0.2-0.1 0.2-0.2 0.6-0.4 1.1-0.8 1.7-1.2l0.1-0.1c0.6-0.5 1.1-1 1.7-1.5l185.8-185.5c8.8-8.8 8.8-23.4-0.1-32.3z m-218.3-23.1c0.5 0.5 1.1 1 1.7 1.5l0.1 0.1c0.5 0.4 1.1 0.8 1.7 1.2 0.1 0.1 0.2 0.1 0.2 0.2 0.5 0.4 1.1 0.7 1.7 1 0.1 0 0.2 0.1 0.3 0.1 0.6 0.3 1.2 0.6 1.8 0.8 0.1 0 0.1 0.1 0.2 0.1 0.6 0.2 1.2 0.5 1.9 0.7h0.2c0.6 0.2 1.3 0.4 2 0.5h0.1c0.7 0.1 1.4 0.2 2 0.3h0.1c0.7 0.1 1.4 0.1 2.1 0.1 0.7 0 1.4 0 2.1-0.1h0.1c0.7-0.1 1.4-0.2 2-0.3h0.1c0.7-0.1 1.3-0.3 2-0.5h0.2c0.6-0.2 1.3-0.4 1.9-0.7 0.1 0 0.1-0.1 0.2-0.1 0.6-0.2 1.2-0.5 1.8-0.8 0.1 0 0.2-0.1 0.3-0.1 0.6-0.3 1.1-0.6 1.7-1 0.1-0.1 0.2-0.1 0.2-0.2 0.6-0.4 1.1-0.8 1.7-1.2l0.1-0.1c0.6-0.5 1.1-1 1.7-1.5l185.8-185.5c8.9-8.9 8.9-23.5 0-32.4-8.9-8.9-23.2-8.6-32.1 0.3L511.7 452.6 342.3 282.7c-8.9-8.9-23.2-9.1-32.1-0.3v0.5c-8.9 8.9-8.9 23 0 31.9l185.3 185.6z" p-id="6878" fill="#8a2be2"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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

@ -0,0 +1,17 @@
<svg
t="1652688633372"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1737"
width="200"
height="200"
>
<path
d="M544.768 904.192c40.96-3.412992 80.212992-12.971008 117.76-28.672s72.020992-36.864 103.424-63.488L578.56 624.64c-10.24 6.144-21.504 10.923008-33.792 14.336v265.216z m267.264-138.24c26.624-31.403008 47.787008-65.876992 63.488-103.424 15.700992-37.547008 25.259008-76.8 28.672-117.76H638.976c-3.412992 12.288-8.192 23.552-14.336 33.792l187.392 187.392z m92.16-286.72c-3.412992-40.96-12.971008-80.212992-28.672-117.76s-36.864-72.020992-63.488-103.424L624.64 445.44c6.144 10.24 10.923008 21.504 14.336 33.792h265.216zM765.952 211.968c-31.403008-26.624-65.876992-47.787008-103.424-63.488-37.547008-15.700992-76.8-25.259008-117.76-28.672v265.216c12.288 3.412992 23.552 8.192 33.792 14.336l187.392-187.392z m-286.72-92.16c-40.96 3.412992-80.212992 12.971008-117.76 28.672s-72.020992 36.864-103.424 63.488l187.392 187.392c10.24-6.144 21.504-10.923008 33.792-14.336V119.808zM211.968 258.048c-26.624 31.403008-47.787008 65.876992-63.488 103.424-15.700992 37.547008-25.259008 76.8-28.672 117.76h265.216c3.412992-12.288 8.192-23.552 14.336-33.792L211.968 258.048z m-92.16 286.72c3.412992 40.96 12.971008 80.212992 28.672 117.76s36.864 72.020992 63.488 103.424l187.392-187.392c-6.144-10.923008-10.923008-22.187008-14.336-33.792H119.808z m138.24 267.264c31.403008 26.624 65.876992 47.787008 103.424 63.488 37.547008 15.700992 76.8 25.259008 117.76 28.672V638.976c-11.604992-3.412992-22.868992-8.192-33.792-14.336L258.048 812.032z m253.952 158.72c-129.707008-3.412992-237.739008-48.299008-324.096-134.656S56.660992 641.707008 53.248 512c3.412992-129.707008 48.299008-237.739008 134.656-324.096S382.292992 56.660992 512 53.248c129.707008 3.412992 237.739008 48.299008 324.096 134.656S967.339008 382.292992 970.752 512c-3.412992 129.707008-48.299008 237.739008-134.656 324.096S641.707008 967.339008 512 970.752z m0-393.216c18.432-0.683008 33.792-7.168 46.08-19.456s18.432-27.648 18.432-46.08-6.144-33.792-18.432-46.08-27.648-18.432-46.08-18.432-33.792 6.144-46.08 18.432-18.432 27.648-18.432 46.08 6.144 33.792 18.432 46.08 27.648 18.772992 46.08 19.456z"
p-id="1738"
></path>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024"><path d="M928 160H96c-17.7 0-32 14.3-32 32v640c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V192c0-17.7-14.3-32-32-32zm-40 632H136v-39.9l138.5-164.3l150.1 178L658.1 489L888 761.6V792zm0-129.8L664.2 396.8c-3.2-3.8-9-3.8-12.2 0L424.6 666.4l-144-170.7c-3.2-3.8-9-3.8-12.2 0L136 652.7V232h752v430.2zM304 456a88 88 0 1 0 0-176a88 88 0 0 0 0 176zm0-116c15.5 0 28 12.5 28 28s-12.5 28-28 28s-28-12.5-28-28s12.5-28 28-28z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 554 B

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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="7" r="4"></circle><path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0-3-3.85"></path></g></svg>

After

Width:  |  Height:  |  Size: 396 B

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

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="48px" height="48.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#7B1DD3" d="M824.8 613.2c-16-51.4-34.4-94.6-62.7-165.3C766.5 262.2 689.3 112 511.5 112 331.7 112 256.2 265.2 261 447.9c-28.4 70.8-46.7 113.7-62.7 165.3-34 109.5-23 154.8-14.6 155.8 18 2.2 70.1-82.4 70.1-82.4 0 49 25.2 112.9 79.8 159-26.4 8.1-85.7 29.9-71.6 53.8 11.4 19.3 196.2 12.3 249.5 6.3 53.3 6 238.1 13 249.5-6.3 14.1-23.8-45.3-45.7-71.6-53.8 54.6-46.2 79.8-110.1 79.8-159 0 0 52.1 84.6 70.1 82.4 8.5-1.1 19.5-46.4-14.5-155.8z" /></svg>

After

Width:  |  Height:  |  Size: 710 B

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

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694158711559" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5049" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M724 511.4L361 947.6c-12.2 14.6-33.9 16.6-48.5 4.4-14.6-12.2-16.6-33.9-4.4-48.5l326.2-392-326.3-391c-12.2-14.6-10.2-36.3 4.4-48.5 14.6-12.2 36.3-10.2 48.5 4.4l363.1 435z" p-id="5050"></path></svg>

After

Width:  |  Height:  |  Size: 527 B

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

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="48px" height="48.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path fill="#7B1DD3" d="M490.7 100.8c115.8 0 209.6 92.3 209.6 206.2 0 113.9-93.9 206.2-209.6 206.2S281.1 420.9 281.1 307c0-113.9 93.9-206.2 209.6-206.2z m0 0c115.8 0 209.6 92.3 209.6 206.2 0 113.9-93.9 206.2-209.6 206.2S281.1 420.9 281.1 307c0-113.9 93.9-206.2 209.6-206.2zM412.1 582h174.7c149.6 0 270.8 119.2 270.8 266.3v17.2c0 58-121.2 60.2-270.8 60.2H412.1c-149.6 0-270.8-0.1-270.8-60.2v-17.2c0-147.1 121.2-266.3 270.8-266.3z m0 0" /></svg>

After

Width:  |  Height:  |  Size: 699 B

13
src/index.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<n-global-style />
<RouterView />
</template>
<script setup>
import { useDialog, useMessage, useNotification } from "naive-ui";
window.$msg = useMessage();
window.$dialog = useDialog();
window.$note = useNotification();
</script>
<style scoped></style>

36
src/main.js Normal file
View File

@ -0,0 +1,36 @@
import { createPinia } from "pinia";
import { createApp } from "vue";
import http from '@/api/index.js';
import icon from "@/icon/index.js";
import "@vant/touch-emulator";
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import "qweather-icons/font/qweather-icons.css";
import "tailwindcss/tailwind.css";
import "vfonts/FiraCode.css";
import VueWechatTitle from "vue-wechat-title";
import App from "./App.vue";
import "./assets/style/main.css";
import router from "./router/guard.js";
import { formatTimeBydate } from './util/index.js';
Date.prototype.format = formatTimeBydate
const app = createApp(App);
document.oncontextmenu = function () {
return false;
};
app.use(createPinia());
app.use(router).use(VueWechatTitle);
window.$http = http
// window.$icon = icon
for (const key in icon) {
// console.log(key, icon[key]);
app.component('icon-'+key, icon[key]);
}
app.mount("#app");

38
src/router/guard.js Normal file
View File

@ -0,0 +1,38 @@
import NProgress from "nprogress";
import cookie from "vue-cookies";
import router from "./index";
// import 'nprogress/nprogress.css'; // nprogress样式文件
import "@/router/nprogress.less";
router.beforeEach((to, from, next) => {
const token = cookie.get("token");
const userinfo = cookie.get("userinfo");
// 路由发生变化修改页面title
if (to.meta.title) {
document.title = to.meta.title;
}
// 开启进度条
NProgress.start();
if (to.meta?.auth) {
if (!!token && !!userinfo) {
next();
} else {
next({
path: "/login",
query: {
redirect: to.fullPath,
},
});
}
} else {
next();
}
});
//当路由跳转结束后
router.afterEach(() => {
// 关闭进度条
NProgress.done();
});
export default router;

143
src/router/index.js Normal file
View File

@ -0,0 +1,143 @@
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
redirect: "/home",
children: [
{
path: "/",
component: HomeView,
redirect: "/home",
children: [
{
path: "/home",
component: () => import("@/views/home/index.vue"),
},
{
path: "/gallery",
component: () => import("@/views/gallery/index.vue"),
},
{
path: "/article",
component: () => import("@/views/article/index.vue"),
},
{
path: "/tour",
component: () => import("@/views/tour/index.vue"),
},
{
path: "/plink",
component: () => import("@/views/plink/index.vue"),
},
],
},
{
path: "/login",
component: () => import("@/views/login.vue"),
},
{
path: "/swag",
component: () => import("@/views/swag/index.vue"),
},
{
path: "/mini/:id",
component: () => import("@/views/article/mini/index.vue"),
},
{
path: "/console",
component: () => import("@/views/console/index.vue"),
redirect: "/console/home",
children: [
{
path: "/console/home",
component: () => import("@/views/console/home/index.vue"),
meta: {
title: "控制台",
name: "首页",
auth: true,
},
},
{
path: "/console/profile",
component: () => import("@/views/console/profile/index.vue"),
meta: {
title: "控制台",
name: "个人信息",
auth: true,
},
},
{
path: "/console/gallery",
component: () => import("@/views/console/gallery/index.vue"),
meta: {
title: "控制台",
name: "画廊管理",
auth: true,
},
},
{
path: "/console/article",
component: () => import("@/views/console/article/index.vue"),
meta: {
title: "控制台",
name: "文章管理",
auth: true,
},
},
],
},
{
path: "/m",
component: () => import("@/views/mobile/index.vue"),
redirect: "/m/home",
children: [
{
path: "/m/home",
component: () => import("@/views/mobile/home/index.vue"),
meta: {
title: "我的",
name: "控制台",
auth: true,
},
},
{
path: "/m/profile",
component: () => import("@/views/mobile/info/index.vue"),
meta: {
title: "个人信息",
name: "控制台",
auth: true,
},
},
{
path: "/m/gallery",
component: () => import("@/views/mobile/gallery/index.vue"),
meta: {
title: "画廊管理",
name: "控制台",
auth: true,
}
},
{
path: "/m/article",
component: () => import("@/views/mobile/arti/index.vue"),
meta: {
title: "文章管理",
name: "控制台",
auth: true,
}
},
]
}
],
},
],
});
export default router;

82
src/router/nprogress.less Normal file
View File

@ -0,0 +1,82 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: @purple;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
/* display: block; */
display: none;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #29d;
border-left-color: #29d;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

14
src/stores/login.js Normal file
View File

@ -0,0 +1,14 @@
import { defineStore } from "pinia";
export const loginStore = defineStore("login", {
state: () => {
return {
loginStatus: true,
};
},
getters: {},
actions: {
setLoginStatus(v) {
this.loginStatus = v;
},
},
});

21
src/stores/size.js Normal file
View File

@ -0,0 +1,21 @@
import { defineStore } from "pinia";
export const usesizeStore = defineStore("size", {
state: () => ({
menuH: 0,
navH: 0,
conH: 0,
footH:0,
}),
actions: {
setMenuH(v) {
this.menuH = v;
},
setConH(v) {
this.conH = v;
},
setFootH(v){
this.footH = v;
}
},
});

34
src/util/consoleMenu.js Normal file
View File

@ -0,0 +1,34 @@
import article from "@/icon/article.svg";
import orange from "@/icon/orange.svg";
import pic from "@/icon/pic.svg";
import profile from "@/icon/profile.svg";
import { NIcon } from "naive-ui";
import { RouterLink } from "vue-router";
const options = [
{
label: () => h(RouterLink, { to: "/console/home", class: "menu-item" }, { default: () => "黑心柚子" }),
key: "0",
icon: () => h(NIcon, { class: "orange" }, { default: () => h(orange) }),
},
{
label: () => h(RouterLink, { to: "/console/profile", class: "menu-item" }, { default: () => "个人信息" }),
key: "1",
icon: () => h(NIcon, { class: "orange" }, { default: () => h(profile) }),
},
{
label: () => h(RouterLink, { to: "/console/gallery", class: "menu-item" }, { default: () => "画廊管理" }),
key: "2",
icon: () => h(NIcon, { class: "orange" }, { default: () => h(pic) }),
},
{
label: () => h(RouterLink, { to: "/console/article", class: "menu-item" }, { default: () => "文章管理" }),
key: "3",
icon: () => h(NIcon, { class: "orange" }, { default: () => h(article) }),
},
];
const consoleMenuInfo = {
options,
menuList: ["/console/home", "/console/profile", "/console/gallery", "/console/article"],
};
export default consoleMenuInfo;

9
src/util/h.js Normal file
View File

@ -0,0 +1,9 @@
import { NIcon } from 'naive-ui';
export function renderIcon(icon) {
return () => {
return h(NIcon, null, {
default: () => h(icon)
});
};
}

136
src/util/index.js Normal file
View File

@ -0,0 +1,136 @@
// 将res的key对应的值复制给dzdata的同名key
export function ObjectCopy(res, dzdata) {
Object.keys(dzdata).map(key => {
dzdata[key] = res[key];
});
}
/**
* @description: 时间格式化
* @param {*} time 传入时间参数支持字符串和时间戳
* @param {*} f 对应格式 "YYYY-MM-DD hh:mm:ss" "YYYY年MM月DD日hh时mm分ss秒"
* 格式可以自定义对应的字符串对应的时间会更新输入单个字符表示可以不补零
* @return {*} 返回格式化的日期
*/
export function formatTime(time, f) {
const date = new Date(time);
if (!(date instanceof Date && !isNaN(date.getTime()))) {
console.error("不合法的日期!");
return;
}
const year = date.getFullYear();
const month = date.getMonth();
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
const monthAdd0 = timeAdd0(month + 1);
const dayAdd0 = timeAdd0(day);
const hourAdd0 = timeAdd0(hour);
const minuteAdd0 = timeAdd0(minute);
const secondAdd0 = timeAdd0(second);
let str = f.toString();
str = str.replace("YYYY", year);
if (f.includes("M")) {
if (f.includes("MM")) str = str.replace("MM", monthAdd0);
else str = str.replace("M", month + 1);
}
if (f.includes("D")) {
if (f.includes("DD")) str = str.replace("DD", dayAdd0);
else str = str.replace("D", day);
}
if (f.includes("h")) {
if (f.includes("h")) str = str.replace("hh", hourAdd0);
else str = str.replace("h", hour);
}
if (f.includes("m")) {
if (f.includes("mm")) str = str.replace("mm", minuteAdd0);
else str = str.replace("m", minute);
}
if (f.includes("s")) {
if (f.includes("ss")) str = str.replace("ss", secondAdd0);
else str = str.replace("s", second);
}
return str;
}
/**
* @description: 补零函数 为时间服务 不足两位的自定在数字前面加上0
* @param {*} n 待补零数字
* @return {*} 补零后数字
*/
function timeAdd0(n) {
const s = n.toString();
return s.padStart(2, "0");
}
export function deepclone(obj) {
let str, newobj = obj.constructor === Array ? [] : {};
if (typeof obj !== "object") {
return;
}
else if (window.JSON) {
str = JSON.stringify(obj), //系列化对象
newobj = JSON.parse(str); //还原
}
else {
for (var i in obj) {
newobj[i] = typeof obj[i] === "object" ? deepclone(obj[i]) : obj[i];
}
}
return newobj;
}
export function formatTimeBydate(f) {
return formatTime(this, f)
}
export function debounce(fn, delay) {
let timer = null;
return function () {
let _this = this;
let args = arguments;
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(function () {
fn.apply(_this, args);
}, delay);
}
}
export function throttle(fn, delay) {
let timer = null;
return function () {
let _this = this;
let args = arguments;
if (!timer) {
timer = setTimeout(function () {
fn.apply(_this, args);
timer = null;
}, delay);
}
}
}
// 字典查询 通个一个字段返回另一个字段的值
/**
* @description 字典查询
* @param {*} dict 字典数组
* @param {*} ckey 查询字段
* @param {*} cvalue 查询值
* @param {*} rkey 返回字段
* @return {*} 返回值
*/
export function getDictValue(dict, ckey, cvalue,rkey) {
let result = "";
dict.map(item => {
if (item[ckey] == cvalue) {
result = item[rkey];
}
})
return result;
}

32
src/util/menu.js Normal file
View File

@ -0,0 +1,32 @@
import orange from "@/icon/orange.svg";
import { NIcon } from "naive-ui";
import { RouterLink } from "vue-router";
const menuInfo = {
options: [
{
label: () => "",
key: "0",
disabled: true,
icon: () => h(NIcon, { class: "orange" }, { default: () => h(orange) }),
},
{
key: "1",
label: () => h(RouterLink, { to: "/home", class: "menu-item" }, { default: () => "首页" }),
},
{
key: "2",
label: () => h(RouterLink, { to: "/gallery", class: "menu-item" }, { default: () => "画廊" }),
},
{
key: "3",
label: () => h(RouterLink, { to: "/article", class: "menu-item" }, { default: () => "文章" }),
},
{
key: "4",
label: () => h(RouterLink, { to: "/plink", class: "menu-item" }, { default: () => "友链" }),
},
],
menuList: ["/home", "/gallery", "/article","/plink"],
};
export default menuInfo;

58
src/util/request.js Normal file
View File

@ -0,0 +1,58 @@
import axios from 'axios';
import qs from 'qs';
import cookie from 'vue-cookies';
import router from '../router/index.js';
let baseURL = import.meta.env.VITE_APP_BASE_URL + '/api';
let headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
const request = axios.create({
baseURL,
headers
});
// 添加请求拦截器
request.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
if (config.method == 'post' || config.method == 'POST') {
config.data = qs.stringify(config.data);
}
if (cookie.get('token') && cookie.get('token') != '') {
config.headers['Authorization'] = cookie.get('token');
}
return config;
},
async function (error) {
// 对请求错误做些什么
return error.message;
}
);
// 添加响应拦截器
request.interceptors.response.use(
function (response) {
// const msg = useMessage()
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
if (response.data.code == 9) {
window.$msg.error('登录过期,请重新登录');
router.push('/login');
return false;
}
if (response.data.code == 8) {
window.$msg.error(response.data.msg);
return false;
}
return response.data;
},
async function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return error.message;
}
);
export default request;

45
src/util/theme.js Normal file
View File

@ -0,0 +1,45 @@
const themeOverrides = {
lightOverrides: {
common: {
primaryColor: "#8a2be2", //默认颜色
primaryColorHover: "#8a2be2", //hover颜色
primaryColorPressed: "#8a2be2", //点击颜色
successColor: "#409EFF",
successColorHover: "#409EFF",
successColorPressed: "#409EFF",
infoColor: "#8a2be2",
infoColorHover: "#8a2be2",
infoColorPressed: "#8a2be2",
},
Radio: {
buttonColorActive: "#FFA50040",
},
},
darkOverrides: {
common: {
primaryColor: "#409EFF", //默认颜色
primaryColorHover: "#409EFF", //hover颜色
primaryColorPressed: "#409EFF", //点击颜色
successColor: "#409EFF",
successColorHover: "#409EFF",
successColorPressed: "#409EFF",
},
Menu: {
itemColorHover: "rgb(38, 52, 69)", //hover背景颜色
itemTextColor: "#777", //默认文字颜色
itemTextColorHover: "rgb(191, 203, 217)", ////hover文字颜色
arrowColor: "rgb(191, 203, 217)", //默认标签颜色
arrowColorHover: "rgb(191, 203, 217)", // hover 标签颜色
itemHeight: "56px", //item的高度
itemIconColor: "rgb(191, 203, 217)", //默认icon颜色
itemIconColorHover: "rgb(191, 203, 217)", //hover icon颜色
itemIconColorCollapsed: "rgb(191, 203, 217)", //收缩 icon颜色
itemColorActiveCollapsed: "rgb(38, 52, 69)",
},
Radio: {
buttonColorActive: "#409EFF",
},
},
};
export default themeOverrides;

39
src/views/HomeView.vue Normal file
View File

@ -0,0 +1,39 @@
<template>
<n-layout style="">
<n-layout-header ref="nav">
<navH />
</n-layout-header>
<n-scrollbar :style="cH">
<router>
<router-view />
</router>
</n-scrollbar>
</n-layout>
</template>
<script setup>
import navH from "@/components/menu.vue";
import router from "@/components/router.vue";
import { onMounted } from "vue";
const nav = ref(null);
let contH = ref(0);
let cH = ref({});
onMounted(() => {
getH();
});
function getH() {
contH.value = nav.value.$el.clientHeight;
cH.value = {
height: document.documentElement.clientHeight - contH.value + "px",
backgroundColor:"#f1f2f3"
};
// console.log(4, contH.value);
}
</script>
<style scoped lang="less">
:deep(.n-layout-header) {
background-color: @dp;
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<div>
<div class="box xxx" :style="{ height: `calc(100vh - ${size.menuH}px)` }">
<n-scrollbar class="pt-6" trigger="hover" :style="{ height: `calc(100vh - ${size.menuH}px)` }">
<arti :list="artList" />
</n-scrollbar>
</div>
</div>
</template>
<script setup>
import arti from '@/components/arti.vue';
import { usesizeStore } from "@/stores/size.js";
const size = usesizeStore();
const artList = ref([])
//mark
onMounted(async () => {
const res = await $http.art.queryArt()
artList.value = res.data
})
</script>
<style scoped>
:deep(.n-layout-sider) {
width: unset !important;
max-width: unset !important;
}
:deep(.n-scrollbar-rail) {
display: none !important;
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<n-scrollbar style="max-height: 100vh">
<div class="title">
{{ info.title }}
</div>
<div class="author">
{{ info.nickname }}
</div>
<div class="cont">
<Editor v-model="info.cont" :defaultConfig="editorConfig" />
</div>
<div class="time">--发表于 {{ formatTime(info.updated_at, "YYYY年MM月DD日hh时") }}</div>
</n-scrollbar>
</template>
<script setup>
import { formatTime } from "@/util/index.js";
import { Editor } from "@wangeditor/editor-for-vue";
import { useRoute } from "vue-router";
const editorConfig = { readOnly: true };
const info = ref({updated_at:new Date()});
const route = useRoute();
// console.log(route.params.id);
getInfo(route.params.id);
async function getInfo(id) {
const res = await $http.art.queryArtInfo(id);
// console.log("**********", res);
info.value = res.data[0];
}
</script>
<style scoped lang="less">
.title {
font-style: italic;
font-size: 20px;
text-align: center;
padding: 10px 0;
}
.author {
padding: 5px 20px;
text-align: right;
color: @purple;
}
.cont {
width: 96%;
margin: 0px auto;
min-height: 80vh;
}
.time {
font-style: italic;
text-align: right;
padding: 5px 30px;
}
:deep(#w-e-element-3) {
margin: unset !important;
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<div>
<div class="mb-8 mt-8 relative">
<h1 class="text-center text-lg">我的文章</h1>
<div class="absolute top-0 right-[5%]">
<n-button type="primary" @click="add">写文章</n-button>
</div>
</div>
<div class="table w-[95%] mx-auto">
<n-data-table :columns="columns" striped :single-line="false" :paginate-single-page="false" :pagination="pagination"
:data="artiList">
<template #empty>
<div>
<span>您还未发表过文章</span><span class="text-pp-200 hover:underline" @click="add">去写一篇 ></span>
</div>
</template>
</n-data-table>
</div>
<n-drawer v-model:show="dshow" :width="702" :mask-closable="false">
<n-drawer-content :title="dtitle" closable>
<artiView v-if="show == 'view'" :id="opId" />
<edition v-if="show == 'add'" :id="-1" @close="handdleCloseEditor" />
<edition v-if="show == 'edit'" :id="curId" @close="handdleCloseEditor" />
</n-drawer-content>
</n-drawer>
<n-modal v-model:show="showModal" preset="dialog" title="删除" content="是否永久删除该文章?" positive-text="确认"
negative-text="取消" @positive-click="submitCallback" @negative-click="cancelCallback" />
</div>
</template>
<script setup>
import edition from '@/components/edition.vue';
import { onMounted, ref } from 'vue';
import artiView from './view.vue';
const dshow = ref(false)
const showModal = ref(false)
const artiList = ref([])
const pagination = ref({
pageSize: 10,
})
const dtitle = ref('');
const opId = ref(-1);
const curId = ref(-1);
const delId = ref(-1);
const show = ref('')
const columns = createColumns({
view: (row) => {
dshow.value = true
dtitle.value = '文章预览'
opId.value = row.id
show.value = 'view'
},
edit: (row) => {
dshow.value = true
dtitle.value = '编辑文章'
curId.value = row.id
show.value = 'edit'
},
remove: (row) => {
showModal.value = true
delId.value = row.id
}
})
function createColumns({ edit, remove, view }) {
return [
{
title: '序号',
key: 'key',
align: 'center',
width: "80",
render: (_, index) => {
return `${index + 1}`
}
},
{
title: "标题",
align: 'center',
width: "300",
key: "title"
},
{
title: "简介",
align: 'center',
key: "pro",
ellipsis: {
tooltip: true
}
},
{
title: "操作",
align: 'center',
key: "actions",
width: "200",
render(row) {
return h(
'div',
[
h(NButton,
{
type: "primary",
text: true,
size: "small",
onClick: () => view(row)
},
{ default: () => "预览" }
),
h(NButton,
{
type: "primary",
text: true,
size: "small",
style: "margin-left: 10px",
onClick: () => edit(row)
},
{ default: () => "编辑" }
),
h(NButton,
{
type: "primary",
text: true,
style: "margin-left: 10px",
size: "small",
onClick: () => remove(row)
},
{ default: () => "删除" }
),
]
);
}
}
];
};
function add() {
dshow.value = true
dtitle.value = '新建文章'
show.value = 'add'
}
onMounted(() => {
getList()
})
async function getList() {
//
const res = await $http.art.myArti()
artiList.value = res.data
}
function handdleCloseEditor() {
dshow.value = false
show.value = ''
dtitle.value = ''
curId.value = -1
getList()
}
async function submitCallback() {
const res = await $http.art.deleteArti(delId.value)
if (res.code == 1) $msg.success(res.msg)
else $msg.error(res.msg)
showModal.value = false
delId.value = -1
getList()
}
function cancelCallback() {
showModal.value = false
delId.value = -1
}
</script>
<style scoped></style>

View File

@ -0,0 +1,67 @@
<template>
<n-scrollbar style="max-height: 100vh">
<div class="title">
{{ info.title }}
</div>
<div class="author">
{{ info.nickname }}
</div>
<div class="cont">
<Editor v-model="info.cont" :defaultConfig="editorConfig" />
</div>
<div class="time">--发表于 {{ formatTime(info.updated_at, "YYYY年MM月DD日hh时") }}</div>
</n-scrollbar>
</template>
<script setup>
import { formatTime } from "@/util/index.js";
import { Editor } from "@wangeditor/editor-for-vue";
const editorConfig = { readOnly: true };
const info = ref({updated_at:new Date()});
const props = defineProps({
id: {
type: [String,Number],
default: "",
},
});
getInfo(props.id);
async function getInfo(id) {
const res = await $http.art.queryArtInfo(id);
// console.log("**********", res);
info.value = res.data[0];
}
</script>
<style scoped lang="less">
.title {
font-style: italic;
font-size: 20px;
text-align: center;
padding: 10px 0;
}
.author {
padding: 5px 20px;
text-align: right;
color: @purple;
}
.cont {
width: 96%;
margin: 0px auto;
min-height: 500px;
}
.time {
font-style: italic;
text-align: right;
padding: 5px 30px;
}
:deep(#w-e-element-3) {
margin: unset !important;
}
</style>

View File

@ -0,0 +1,317 @@
<template>
<div class="div w-[100%]" ref="gw">
<div class="list px-3">
<n-upload style="height: 200px; width: 50%" multiple directory-dnd action="https://www.hxyouzi.com/api/file/upload"
accept="image/*" :headers="{ Authorization: token }" :show-file-list="false" :on-finish="fileFinish">
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" class="up">
<MdCloudUpload />
</n-icon>
</div>
<n-text style="font-size: 16px"> 点击或者拖动文件到该区域来上传 </n-text>
<n-p depth="3" style="margin: 8px 0 0 0"> 请不要上传大于5m的图片</n-p>
</n-upload-dragger>
</n-upload>
<falls :fileList="list" :count="acount" :bot="bottomLen" pathname="filepath" v-slot="{ file }">
<div class="img mb-1" @contextmenu="handdlemenu($event, file)" @longpress="handdlemenu($event, file)">
<n-image height="200px" :src="file.filepath" object-fit="fill" :img-props="{ loading: 'lazy' }" />
</div>
</falls>
<div class="munur" v-if="menuShow" :style="{ position: 'fixed', top: y + 'px', left: x + 'px' }">
<template v-for="i in menuList">
<n-button v-if="i.show" type="primary" :tertiary="!i.active" @click="handdleclickmenu(i.id)"
@mouseenter="handdleentermenu(i.id)" @mouseleave="handdleleavemenu(i.id)">{{ i.name
}}</n-button>
</template>
</div>
<n-modal v-model:show="showModal" preset="dialog" title="Dialog">
<template #header>
<div>提示</div>
</template>
<div>
<div style="text-align: center">{{ currentImg.filename }}</div>
<n-form-item label="">
<n-input v-model:value="fileName" placeholder="请输入新的文件名" @keyup.enter="" />
</n-form-item>
</div>
<template #action>
<n-button @click="handdleClose">取消</n-button>
<n-button type="primary" @click="updatename">确认</n-button>
</template>
</n-modal>
</div>
</div>
</template>
<script setup>
import falls from '@/components/falls.vue';
import { MdCloudUpload } from "@vicons/ionicons4";
import { onMounted } from 'vue';
import cookie from "vue-cookies";
const acount = ref(2)
const bottomLen = ref(20)
// console.log(4444, size.conH);
const list = ref([]);
const token = cookie.get("token");
function fileFinish(file) {
const res = JSON.parse(file.event.currentTarget.response);
// console.log(res);
if (res.code == 1) {
$msg.success(res.msg);
getMyfile();
} else {
$msg.error(res.msg);
}
}
async function getMyfile() {
list.value = [];
const res = await $http.file.MyFile();
list.value = res.data;
}
onMounted(() => {
getMyfile();
});
const menuShow = ref(false);
const x = ref(0);
const y = ref(0);
let imgTimer
const menuList = ref([
{
id: 1,
name: "修改文件名",
active: false,
show: true,
},
{
id: 2,
name: "分享",
active: false,
show: true,
},
{
id: 3,
name: "取消分享",
active: false,
show: true,
},
{
id: 4,
name: "下载",
active: false,
show: true,
},
{
id: 5,
name: "删除",
active: false,
show: true,
},
])
const fileName = ref("");
const showModal = ref(false);
const currentImg = ref({});
function handdlemenu(e, img) {
menuShow.value = false
const { clientX, clientY } = e;
x.value = clientX;
y.value = clientY;
menuShow.value = true;
menuList.value[1].show = !img.share;
menuList.value[2].show = !!img.share;
currentImg.value = img;
console.log(img);
imgTimer && clearTimeout(imgTimer)
imgTimer = setTimeout(() => {
menuShow.value = false
resetMenuList()
}, 2000);
}
function handdleclickmenu(id) {
switch (id) {
case 1:
showModal.value = true;
menuShow.value = false;
break;
case 2:
share(currentImg.value.id)
break;
case 3:
unshare(currentImg.value.id)
break;
case 4:
downloadImg(currentImg.value)
break;
case 5:
remove(currentImg.value.id)
break;
}
resetMenuList()
}
function handdleentermenu(id) {
menuList.value[id - 1].active = true;
imgTimer && clearTimeout(imgTimer)
}
function handdleleavemenu(id) {
menuList.value[id - 1].active = false;
imgTimer = setTimeout(() => {
menuShow.value = false
resetMenuList()
}, 2000);
}
function resetMenuList() {
menuList.value = ([
{
id: 1,
name: "修改文件名",
active: false,
show: true,
},
{
id: 2,
name: "分享",
active: false,
show: true,
},
{
id: 3,
name: "取消分享",
active: false,
show: true,
},
{
id: 4,
name: "下载",
active: false,
show: true,
},
{
id: 5,
name: "删除",
active: false,
show: true,
},
])
}
async function share(id) {
menuShow.value = false;
const res = await $http.file.updateFile(id);
res.code ? $msg.success(res.msg) : $msg.error(res.msg);
getMyfile();
}
async function unshare(id) {
menuShow.value = false;
const res = await $http.file.unUpdateFile(id);
res.code ? $msg.success(res.msg) : $msg.error(res.msg);
getMyfile()
}
async function remove(id) {
menuShow.value = false;
const res = await $http.file.delFile(id);
res.code ? $msg.success(res.msg) : $msg.error(res.msg);
getMyfile()
}
async function downloadImg(img) {
let a = document.createElement("a");
a.download = img.filename;
a.href = img.filepath;
a.target = "_BLANK";
a.click();
}
function handdleClose() {
fileName.value = "";
showModal.value = false;
}
async function updatename() {
if (fileName.value != "") {
const res = await $http.file.updateName({ id: currentImg.value.id, name: fileName.value });
res.code ? $msg.success(res.msg) : $msg.error(res.msg);
handdleClose();
getMyfile()
} else {
$msg.error("文件名不能为空");
}
}
const pw = 200
const gw = ref(null)
function arrCount() {
const cw = gw.value.clientWidth
if (cw < 750) {
acount.value = 2
// pivW.value = 180
bottomLen.value = 10
return
}
// pivW.value = 200
bottomLen.value = 20
acount.value = Math.floor(cw * 0.95 / (pw + 20))
}
onMounted(() => {
arrCount()
})
</script>
<style scoped lang="less">
.div {
display: flex;
flex-direction: column;
align-items: center;
}
:deep(.up.n-icon) {
color: @purple;
}
:deep(.n-upload-dragger) {
display: flex;
flex-direction: column;
justify-content: center;
}
.list {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 10px;
margin-bottom: 10px;
}
.munur {
border-radius: 4px;
overflow: hidden;
z-index: 9999;
display: flex;
flex-direction: column;
background-color: #fff;
}
:deep(.n-image) {
display: flex;
img {
width: 200px;
height: auto;
}
}
.img {
border-radius: 4px;
overflow: hidden;
box-shadow: @ps;
border: 1px solid #dddddd;
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<div>控制台首页</div>
</template>
<script setup></script>
<style scoped></style>

View File

@ -0,0 +1,98 @@
<template>
<n-layout has-sider>
<n-layout-sider ref="sider" bordered collapse-mode="width" inverted :collapsed-width="64" show-trigger :width="240" :collapsed="collapsed" :show-trigger="false" @collapse="collapsed = true" @expand="collapsed = false">
<div ref="consoleSiderHeader" class="" @click="collapsed = false">
<n-menu :collapsed="collapsed" inverted :collapsed-width="64" :icon-size="20" :collapsed-icon-size="26" :options="menuOptions" />
</div>
<div class="menu">
<console-menu :collapsed="collapsed" />
</div>
</n-layout-sider>
<n-layout>
<n-layout-header :style="{ height: navH + 'px' }">
<div class="header bg-[#f2e8fc] flex items-center justify-between">
<div class="index ml-4">{{ route.meta.name }}</div>
<div class="info flex items-center">
<div class="wea">
<wea></wea>
</div>
<div class="my">
<div class="uinfo mx-8 text-white flex items-center">
<n-avatar round size="small" :src="userinfo.avaUrl" />
<div class="ml-2"></div>
<n-button text class="userinfo">
{{ userinfo.nickname }}
</n-button>
</div>
</div>
<n-button color="#8a2be2" size="small" class="mr-4" @click="goHome"> 回首页 </n-button>
</div>
</div>
</n-layout-header>
<n-layout-content :content-style="cst" :native-scrollbar="false">
<router-view></router-view>
</n-layout-content>
</n-layout>
</n-layout>
</template>
<script setup>
import consoleMenu from "@/components/consoleMenu.vue";
import wea from "@/components/wea.vue";
import { usesizeStore } from "@/stores/size.js";
import { MdMenu } from "@vicons/ionicons4";
import cookie from "vue-cookies";
import { useRoute, useRouter } from "vue-router";
const size = usesizeStore();
const userinfo = cookie.get("userinfo");
const collapsed = ref(false);
let consoleSiderHeader = ref(null);
let navH = ref(0);
const cst = ref({});
const menuOptions = [
{
label: "控制台",
key: "0",
disabled: true,
icon: () => h(NIcon, { class: "console-sider-header" }, { default: () => h(MdMenu) }),
},
];
const route = useRoute();
const router = useRouter();
onMounted(() => {
navH.value = consoleSiderHeader.value.clientHeight;
size.setConH(document.documentElement.clientHeight - navH.value);
cst.value = {
height: document.documentElement.clientHeight - navH.value + "px",
};
});
function goHome() {
router.replace("/");
}
</script>
<style scoped>
.header {
height: 100%;
}
:deep(.n-layout-sider) {
height: 100vh;
}
.col {
margin: 0 auto;
}
.nocol {
margin-left: 10px;
}
:deep(.n-menu-item-content--disabled) {
opacity: unset !important;
}
:deep(.console-sider-header) {
color: #fff;
}
</style>

View File

@ -0,0 +1,98 @@
<template>
<div class="list">
<div class="row flex items-center">
<div class="label">头像</div>
<n-avatar round :src="userinfo.avaUrl" />
<div class="edit" @click="showModal = true">更换</div>
</div>
<div class="row flex items-center mt-4">
<div class="label">手机号</div>
<div>{{ userinfo.tel }} <span v-if="!userinfo.tel" class="text-gray-500 text-[12px] underline italic">尚未绑定手机号</span>
</div>
<div class="edit" @click="showTelModal = true">更换</div>
</div>
</div>
<n-modal :closable="false" :show="showModal" title="更改头像" @mask-click="showModal = false" @esc="showModal = false"
@negative-click="showModal = false" preset="dialog" negative-text="取消">
<div style="margin: 10px 0; width: 100%; display: flex; justify-content: center">
<n-avatar round :size="80" :src="userinfo.avaUrl" fallback-src="https://www.hxyouzi.com/gin/assets/ava/默认.png" />
</div>
<n-upload action="https://www.hxyouzi.com/api/info/update/avater" :headers="{ Authorization: token }"
:show-file-list="false" :on-finish="fileFinish" accept=".jpg,.jpeg,.png,.gif,.jfif"
style="width: 100%; display: flex; justify-content: center">
<n-button type="primary">上传头像</n-button>
</n-upload>
</n-modal>
<n-modal :closable="false" :show="showTelModal" title="更改手机号" @mask-click="showTelModal = false"
@esc="showTelModal = false" @negative-click="showTelModal = false" preset="dialog" negative-text="取消"
positive-text="确认" @positive-click="submitCallback">
<n-input v-model:value="tel" type="text" :placeholder="userinfo.tel || '请输入手机号'" clearable></n-input>
</n-modal>
</template>
<script setup>
import cookie from "vue-cookies";
let userinfo = ref(cookie.get("userinfo"));
let token = ref(cookie.get("token"));
const showModal = ref(false);
const showTelModal = ref(false);
const tel = ref('')
function fileFinish(file) {
const res = JSON.parse(file.event.currentTarget.response);
// console.log(res);
if (res.code == 1) {
$msg.success(res.msg);
autologin();
} else {
$msg.error(res.msg);
}
}
async function autologin() {
const res = await $http.user.alogin();
if (res.code == 1) {
cookie.set("token", res.token, "1d");
cookie.set("userinfo", res.data, "1d");
userinfo.value = cookie.get("userinfo");
location.reload();
}
}
async function submitCallback() {
if (!tel.value) return
if (!/^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/.test(tel.value)) {
$msg.error('请输入正确的手机号!')
return
}
const res = await $http.user.updatetel({ tel: tel.value })
if (res.code == 1) {
$msg.success(res.msg)
showTelModal.value = false
}
else $msg.error(res.msg)
}
</script>
<style scoped lang="less">
.label {
padding: 0 10px;
margin-right: 20px;
}
.edit {
// padding: 0 10px;
margin-left: 20px;
font-size: 12px;
text-decoration: underline;
color: @blue;
cursor: url("@/cursor/link_26x26.png"), pointer !important;
}
.edit:hover {
color: @purple;
}</style>

101
src/views/gallery/index.vue Normal file
View File

@ -0,0 +1,101 @@
<template>
<n-scrollbar class="bg-[#f8f9fa]" :on-scroll="onScroll" :style="{ maxHeight: contH + 'px' }">
<div class="w-[40vw] mx-auto my-6 md:w-[80vw]">
<n-input-group>
<n-input v-model:value="kw" placeholder="图片名 | 分享人" @keyup.enter.native="search" :style="{ width: '80%' }" />
<n-button :style="{ width: '20%' }" type="info" @click="search"> 搜索 </n-button>
</n-input-group>
</div>
<div class="list">
<xPic :fileList="sharelist"></xPic>
</div>
<n-back-top :right="20" />
</n-scrollbar>
</template>
<script setup>
import { usesizeStore } from '@/stores/size.js';
import { onMounted } from "vue";
import xPic from './xPic.vue';
const kw = ref("");
const sharelist = ref([]);
const ps = ref(20), pn = ref(1), total = ref(0)
const size = usesizeStore()
const contH = ref(0)
let reqLock = false //
onMounted(() => {
getSharefile();
contH.value = document.documentElement.clientHeight - size.menuH - 10
})
function search() {
sharelist.value = [];
pn.value = 1
getSharefile()
}
async function getSharefile() {
if (reqLock) return
reqLock = true
const res = await $http.file.ShareFile({
keyword: kw.value,
page_size: ps.value,
page_num: pn.value,
});
total.value = res.total
res.data.forEach(i => i.ishow = false)
sharelist.value = res.data;
setTimeout(() => {
reqLock = false
}, 1000)
}
//
let lastTop = 0 // 便
function onScroll(e) {
if (reqLock) return
if (ps.value * pn.value >= total.value) return
let st = e.target.scrollTop
let ch = e.target.clientHeight
let sh = e.target.scrollHeight
if (st > lastTop) {
if (st + ch + 200 >= sh) {
// reqLock = false
pn.value++
getSharefile()
}
}
lastTop = st
}
</script>
<style scoped>
.search {
padding: 0 30%;
}
.empty {
margin-top: 15%;
}
.space {
padding: 20px 10px;
}
.space::after {
height: 0;
flex: auto;
content: "";
}
</style>

184
src/views/gallery/xPic.vue Normal file
View File

@ -0,0 +1,184 @@
<template>
<div class="box flex justify-center" ref="curcomp">
<falls :fileList="fileList" :count="acount" :bot="bottomLen" pathname="filepath" v-slot="{ file }">
<div class="img" @mouseenter="movein(file)" @mouseleave="moveout(file)">
<n-image width="200px" :imgW="pivW" :src="file.filepath" object-fit="cover" :img-props="{ loading: 'lazy' }" />
<div class="title w-[120px]" v-if="!ismobile && file.ishow">
<n-ellipsis style="max-width: 120px" :line-clamp="1">{{ file.filename }}</n-ellipsis>
</div>
<div class="tooltip" v-if="ismobile || file.ishow">
<div class="block">
<div class="uploadBy flex items-center">
<n-ellipsis :line-clamp="1">{{ file.nickname }}分享</n-ellipsis>
</div>
</div>
<n-tooltip trigger="hover">
<template #trigger>
<n-button text size="small" @click="downloadImg(file)">
<n-icon size="20">
<MdDownload />
</n-icon>
</n-button>
</template>
下载
</n-tooltip>
</div>
</div>
</falls>
</div>
</template>
<script setup>
import falls from '@/components/falls.vue';
import { MdDownload } from "@vicons/ionicons4";
import { ref } from 'vue';
const pivW = ref(200);
const prop = defineProps({
fileList: {
type: Array,
default: () => [],
},
});
const { fileList } = toRefs(prop);
const pw = 200
const acount = ref(2)
const bottomLen = ref(20)
const ismobile = ref(false)
arrCount()
window.addEventListener("resize", arrCount)
function movein(file) {
if (ismobile.value) return
file.ishow = true
}
function moveout(file) {
if (ismobile.value) return
file.ishow = false
}
async function downloadImg(img) {
let a = document.createElement("a");
a.download = img.filename;
a.href = img.filepath;
a.target = "_BLANK";
a.click();
}
function arrCount() {
// console.log('w=>', document.documentElement.clientWidth);
const cw = document.documentElement.clientWidth
if (cw < 1000) {
acount.value = 2
pivW.value = 180
bottomLen.value = 10
ismobile.value = true
return
}
pivW.value = 200
bottomLen.value = 20
ismobile.value = false
acount.value = Math.floor(cw * 0.95 / (pw + 20))
}
</script>
<style lang="less" scoped>
.img {
// width: 100%;
// height: 200px;
position: relative;
margin-bottom: 20px;
box-shadow: @ps;
.title {
box-sizing: border-box;
padding: 0 8px;
height: 28px;
line-height: 28px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #00000080;
position: absolute;
color: #ddd;
top: 0px;
display: flex;
font-size: 12px;
justify-content: center;
cursor: url("@/cursor/link_26x26.png"), pointer !important;
}
.tooltip {
box-sizing: border-box;
padding: 0 8px;
height: 28px;
line-height: 28px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #00000080;
position: absolute;
border-radius: 0 0 10px 10px;
bottom: 0px;
cursor: url("@/cursor/link_26x26.png"), pointer !important;
.n-icon {
color: #fff !important;
}
.n-icon:hover {
color: @purple !important;
}
}
.uploadBy {
font-size: 12px;
color: @orange;
}
}
:deep(.n-space)>div,
.block {
display: flex;
justify-content: center;
}
:deep(.n-image) {
border-radius: 10px;
box-shadow: @ps;
display: flex;
img {
width: 200px;
height: auto;
}
}
.img {
// margin-right: 20px;
border-radius: 10px;
overflow: hidden;
background-color: transparent;
}
@media screen and (max-width:1000px) {
:deep(.n-image) {
img {
width: 180px;
height: auto;
}
}
.img {
// margin-right: 10px;
margin-bottom: 10px;
}
}
</style>

54
src/views/home/index.vue Normal file
View File

@ -0,0 +1,54 @@
<template>
<div>
<div class="box xxx" :style="{ height: `calc(100vh - ${size.menuH}px)` }">
<n-layout>
<n-layout has-sider>
<n-layout-content class="sm:w-[90vh] w-[70%]" :content-style="{ height: `calc(100vh - ${size.menuH}px)`,backgroundColor:'#f1f2f3' }">
<n-scrollbar class="pt-6" trigger="hover" :style="{ height: `calc(100vh - ${size.menuH}px)` }">
<search></search>
<NavMenu />
</n-scrollbar>
</n-layout-content>
<n-layout-sider class="sm:hidden" :content-style="{width: '30vw',height: `calc(100vh - ${size.menuH}px)`,overflow:'hidden',backgroundColor:'#f1f2f3' }">
<Suspense>
<side />
</Suspense>
</n-layout-sider>
</n-layout>
</n-layout>
</div>
<aplayerView></aplayerView>
<foot />
</div>
</template>
<script setup>
import aplayerView from "@/components/aplayerView.vue";
import foot from "@/components/foot.vue";
import { usesizeStore } from "@/stores/size.js";
import NavMenu from "./navMenu.vue";
import search from './search.vue';
import side from './side.vue';
const size = usesizeStore();
const artList = ref([])
//mark
onMounted(async () => {
const res = await $http.art.queryArt()
artList.value = res.data
})
</script>
<style scoped>
:deep(.n-layout-sider) {
width: unset !important;
max-width: unset !important;
}
:deep(.n-scrollbar-rail) {
display: none !important;
}
</style>

159
src/views/home/navMenu.vue Normal file
View File

@ -0,0 +1,159 @@
<template>
<div class="box flex flex-wrap pl-[2%] mt-8">
<div class="nav-card w-[300px] mx-[2%] mb-6 p-2 bg-[#fafbfc]" v-for="(item, index) in navList" :key="index">
<div class="t" @click="expand(index)">
<n-divider title-placement="center">
<div class="flex items-center font-bold">
{{ item.menuClass }}
<n-icon size="24" class="tiao mx-2">
<icon-more />
</n-icon>
</div>
</n-divider>
</div>
<div class="flex flex-wrap pt-3" v-show="item.exp">
<div class="w-[45%] ml-[3%] mb-2" v-for="(it, idx) in item.children" :key="index">
<!-- <n-popover trigger="hover"> -->
<!-- <template #trigger> -->
<n-button class="w-[100%] text-[12px] text-ellipsis hover:bg-gradient-to-r hover:from-pp-100 hover:to-pp-400 font-bold" ghost color="#B172EC" @click="link(it.menuLink)">
<!-- <n-button class="w-[100%] text-[12px] text-ellipsis" ghost color="#ff69b4" @click="link(it.menuLink)"> -->
{{ it.menuName }}
</n-button>
<!-- </template> -->
<!-- <span class="text-[12px]">{{ it.menuName }}</span> -->
<!-- </n-popover> -->
</div>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
//mark import
//mark data
const navList = ref([])
const active = ref(-1)
//mark method
function formatNav(list) {
let menu = []
let result = []
list.forEach(i => {
if (!menu.includes(i.menuClass)) {
menu.push(i.menuClass)
}
});
menu.forEach(i => {
result.push({
menuClass: i,
children: list.filter(it => it.menuClass == i),
exp: false
})
})
console.log(result);
return result
}
function expand(index) {
console.log(index, active.value);
if (active.value == index) {
navList.value[index].exp = !navList.value[index].exp
} else {
navList.value.forEach(i => {
i.exp = false
})
navList.value[index].exp = true
}
active.value = index
}
function link(url) {
window.open(url, "_blank")
}
//mark
onMounted(async () => {
const res = await $http.nav.listNav()
navList.value = formatNav(res.data)
})
</script>
<style scoped lang="less">
.nav-card {
box-shadow: @ps;
border-radius: 4px;
flex-grow: 1;
}
.box {
align-items: start;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-3px);
}
60% {
transform: translateY(-1px);
}
}
.tiao {
animation: bounce 1s infinite;
/* 播放名为bounce的动画循环无限次每次动画时长1秒 */
}
:deep(.n-divider) {
margin: 0px;
}
:deep(.n-button__content) {
display: block;
width: 100%;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
}
:deep(.n-button):hover {
// background-color: @purple !important;
border: none !important;
color: white !important;
animation: swing .8s ease-in-out 3;
}
@keyframes swing {
0%,
20%,
50%,
80%,
100% {
transform: translateX(0);
}
40% {
transform: translateX(-3px);
}
60% {
transform: translateX(3px);
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More