123
This commit is contained in:
commit
5e43f3a78f
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.bat
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CYBER-2215</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
1260
package-lock.json
generated
Normal file
1260
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
package.json
Normal file
25
package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "mva-cyberv2",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"js-cookie": "^3.0.5",
|
||||
"marked": "^15.0.7",
|
||||
"sweetalert2": "^11.16.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.1.0"
|
||||
}
|
||||
}
|
BIN
public/fonts/Netron.ttf
Normal file
BIN
public/fonts/Netron.ttf
Normal file
Binary file not shown.
25
public/log.md
Normal file
25
public/log.md
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
## 网站更新日志
|
||||
|
||||
---
|
||||
### 2025/2/13 16:30 - [v0.0.2] 测试效果
|
||||
- 优化布局
|
||||
- 优化双端切换逻辑
|
||||
- 新增项目展示页面 - 测试(未完成)
|
||||
- 新增在线工具罗列页面 - 测试(未完成)
|
||||
- 新增管理员上传日志 - 测试(未完成)
|
||||
|
||||
### 2025/2/12 22:30 - [v0.0.1] 测试功能
|
||||
- 制作导航栏手机电脑双端模式
|
||||
- 新增黑白双色模式
|
||||
- 新增账户管理页面
|
||||
- 账号设置 - 基本完成
|
||||
- 头像系统
|
||||
- 基本信息显示
|
||||
- 双色模式切换
|
||||
- 个人主页设置 - 测试(未完成)
|
||||
- 稿件管理 - 测试(未完成)
|
||||
- 草稿箱 - 测试(未完成)
|
||||
- 博客界面
|
||||
- 关键词标签
|
||||
- 关键词搜索
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
87
services/auth.js
Normal file
87
services/auth.js
Normal file
@ -0,0 +1,87 @@
|
||||
import axios from 'axios';
|
||||
import api from "../src/utils/axios.js";
|
||||
import store from "../src/store/index.js";
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export default class AuthService {
|
||||
static async login(username, password) {
|
||||
let result = {}
|
||||
let isEmail = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(username);
|
||||
try {
|
||||
const data = {
|
||||
LOGIN_METHOD: isEmail ? "email" : "username",
|
||||
LOGIN_NAME: username,
|
||||
PASSWORD: password
|
||||
};
|
||||
await api.post('/login', data).then(res => {
|
||||
result = res;
|
||||
});
|
||||
Cookies.set('Token', result.token, {path: '/', expires: 1})
|
||||
} catch (e) {
|
||||
result.code = 3;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static async sendMessage(username, email, password){
|
||||
let result = {}
|
||||
const data = {
|
||||
USER_NAME: username,
|
||||
EMAIL: email,
|
||||
PASSWORD: password
|
||||
};
|
||||
await api.post('/postmessage', data).then(res=>{
|
||||
result = res;
|
||||
})
|
||||
return result;
|
||||
}
|
||||
static logout() {
|
||||
Cookies.remove('Token');
|
||||
store.commit('setToken', null)
|
||||
store.commit('setUserInfo', null)
|
||||
}
|
||||
|
||||
static getToken() {
|
||||
return Cookies.get('Token');
|
||||
}
|
||||
|
||||
static async setSelfInfo() {
|
||||
let info = {};
|
||||
if (!store.state.token) {
|
||||
store.commit('setToken', await this.getToken());
|
||||
}
|
||||
try {
|
||||
await api.post('/selfinfo', {
|
||||
TOKEN: store.state.token
|
||||
}).then((res) => {
|
||||
info = res;
|
||||
})
|
||||
if (info.code !== 0) {
|
||||
throw new Error('error')
|
||||
}
|
||||
if (store.state.token) {
|
||||
store.commit('setUserInfo', info.info)
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
// this.logout();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static isTokenExpired() {
|
||||
const token = this.getToken();
|
||||
if (!token) return true;
|
||||
|
||||
// const payload = JSON.parse(atob(token.split('.')[1])); // 解码JWT的payload部分
|
||||
// const exp = payload.exp * 1000; // 转换为毫秒
|
||||
// return Date.now() >= exp;
|
||||
}
|
||||
|
||||
// static async refreshToken() {
|
||||
// const refreshToken = localStorage.getItem('refreshToken');
|
||||
// const response = await axios.post('/api/refresh-token', {refreshToken});
|
||||
// localStorage.setItem('token', response.data.token);
|
||||
// return response.data.token;
|
||||
// }
|
||||
}
|
47
src/App.vue
Normal file
47
src/App.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<NavBar/>
|
||||
<div style="margin-top: 60px"><router-view /></div>
|
||||
|
||||
<LoadingSpinner v-if="store.state.loading.isLoading"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, watch} from 'vue'
|
||||
import {useStore} from 'vuex'
|
||||
import NavBar from "./components/NavBar.vue";
|
||||
import LoadingSpinner from "./components/LoadingSpinner.vue";
|
||||
import AuthService from "../services/auth.js";
|
||||
|
||||
const store = useStore()
|
||||
|
||||
// 根据主题更新 body 上的类
|
||||
const updateGlobalTheme = (theme) => {
|
||||
if (!(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.body.classList.add('theme-light')
|
||||
return;
|
||||
}
|
||||
if (theme === 'light') {
|
||||
document.body.classList.add('theme-light')
|
||||
} else {
|
||||
document.body.classList.remove('theme-light')
|
||||
}
|
||||
}
|
||||
|
||||
// 挂载时根据 store 中的主题状态更新全局样式
|
||||
onMounted(() => {
|
||||
if (AuthService.getToken()) {
|
||||
AuthService.setSelfInfo();
|
||||
}
|
||||
updateGlobalTheme(store.state.theme)
|
||||
})
|
||||
|
||||
// 监听 Vuex 主题状态的变化,更新全局样式
|
||||
watch(
|
||||
() => store.state.theme,
|
||||
(newTheme) => {
|
||||
updateGlobalTheme(newTheme)
|
||||
}
|
||||
)
|
||||
</script>
|
6
src/assets/styles/fonts.css
Normal file
6
src/assets/styles/fonts.css
Normal file
@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'Netron';
|
||||
src: url('/fonts/Netron.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
128
src/components/AccountWorkPiece.vue
Normal file
128
src/components/AccountWorkPiece.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<script setup>
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
cover: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
createdTime: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
lastModifiedTime: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isDraft: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// 测试函数,可以根据需求替换
|
||||
function onEdit() {
|
||||
console.log('编辑')
|
||||
}
|
||||
|
||||
function onPermission() {
|
||||
console.log('权限')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="work-piece">
|
||||
<!-- 左侧:博客封面 + 标题 -->
|
||||
<div class="piece-left">
|
||||
<img :src="cover" alt="封面" class="cover-image" />
|
||||
<div class="title-text">{{ title }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:时间和操作按钮 -->
|
||||
<div class="piece-right">
|
||||
<!-- 上方时间信息 -->
|
||||
<div class="times">
|
||||
<div>创建时间:{{ createdTime }}</div>
|
||||
<div>最后修改:{{ lastModifiedTime }}</div>
|
||||
</div>
|
||||
<!-- 下方操作 -->
|
||||
<div class="actions">
|
||||
<span class="action-btn" @click="onEdit">编辑</span>
|
||||
<!-- 草稿时不显示“权限” -->
|
||||
<span
|
||||
v-if="!isDraft"
|
||||
class="action-btn"
|
||||
@click="onPermission"
|
||||
>
|
||||
权限
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 容器:一条“横向卡片” */
|
||||
.work-piece {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #24303f; /* 暗色稍微提亮 */
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.theme-light .work-piece {
|
||||
background-color: #f2f2f2;
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 左侧 */
|
||||
.piece-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.cover-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.title-text {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 右侧 */
|
||||
.piece-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.times {
|
||||
opacity: 0.5;
|
||||
font-size: small;
|
||||
text-align: right;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.action-btn {
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-btn:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
91
src/components/Blog_box.vue
Normal file
91
src/components/Blog_box.vue
Normal file
@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
blog: Object,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link :to="'/blog/'+Math.floor(Math.random()*1e+5)">
|
||||
<div class="blog-box">
|
||||
<img :src="blog.image" alt="Blog image" class="blog-image" />
|
||||
<div class="blog-details">
|
||||
<h3>{{ blog.title }}</h3>
|
||||
<p class="author">By {{ blog.author }}</p>
|
||||
<p class="time"> {{ blog.creatTime }}</p>
|
||||
<div class="tags">
|
||||
<span v-for="tag in blog.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.blog-box {
|
||||
width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.blog-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.blog-details {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.blog-details h3 {
|
||||
margin: 5px 0;
|
||||
font-size: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.author {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 3px 8px;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.theme-light .blog-box {
|
||||
border-color: #ffb74d;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.theme-light .tag {
|
||||
border-color: #ffb74d;
|
||||
color: #ffb74d;
|
||||
}
|
||||
</style>
|
43
src/components/HelloWorld.vue
Normal file
43
src/components/HelloWorld.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
71
src/components/LoadingSpinner.vue
Normal file
71
src/components/LoadingSpinner.vue
Normal file
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="loading-overlay">
|
||||
<div class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<p class="loading-text">{{ store.state.loading.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from "../store/index.js";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
store() {
|
||||
return store;
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column; /* 让内容竖直排列 */
|
||||
justify-content: center;
|
||||
align-items: center; /* 垂直和水平都居中 */
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
border: 8px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 8px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 10px; /* 给文字留出空间 */
|
||||
}
|
||||
|
||||
/* 动画:旋转 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
margin-top: 15px; /* 控制文字和圆形动画的间距 */
|
||||
text-align: center; /* 文字水平居中 */
|
||||
}
|
||||
</style>
|
310
src/components/NavBar.vue
Normal file
310
src/components/NavBar.vue
Normal file
@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<nav :class="[themeClass, 'navbar']">
|
||||
<div class="nav-left">
|
||||
<button class="logo" @click="store.commit('toggleTheme')">
|
||||
CYBER
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-center">
|
||||
<ul class="nav-items" v-if="!isMobile">
|
||||
<li v-for="(item, index) in navItems" :key="index">
|
||||
<router-link :to="item.link">{{ item.name }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mobile-menu" v-else>
|
||||
<button @click="toggleMobileMenu" class="hamburger-btn">
|
||||
<span class="hamburger-icon"></span>
|
||||
</button>
|
||||
<transition name="fade">
|
||||
<ul v-if="showMobileMenu" class="mobile-nav-items">
|
||||
<li v-for="(item, index) in navItems" :key="index" @click="toggleMobileMenu">
|
||||
<router-link :to="item.link">{{ item.name.replace(/[☆]/g, "") }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<router-link v-if="!store.getters.hasUserInfo" to="/login">
|
||||
<button class="login-btn">登录/注册</button>
|
||||
</router-link>
|
||||
<router-link v-else to="/account/setting"><div class="user-info">
|
||||
<img :src="store.getters.profileImage" alt="User Avatar" class="avatar" />
|
||||
<span class="username">{{ store.state.userInfo.username }}</span>
|
||||
</div></router-link>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import '../assets/styles/fonts.css';
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const theme = computed(() => store.getters.currentTheme)
|
||||
const themeClass = computed(() => theme.value === 'dark' ? 'theme-dark' : 'theme-light')
|
||||
|
||||
const navItems = [
|
||||
{ name: '工具', link: '/tools' },
|
||||
{ name: '博客', link: '/blog' },
|
||||
{ name: '☆主页☆', link: '/' },
|
||||
{ name: '项目', link: '/projects' },
|
||||
{ name: '关于', link: '/about' },
|
||||
]
|
||||
|
||||
// 移动端菜单
|
||||
const showMobileMenu = ref(false)
|
||||
const toggleMobileMenu = () => {
|
||||
showMobileMenu.value = !showMobileMenu.value
|
||||
}
|
||||
|
||||
// 判定
|
||||
const isMobile = ref(window.innerWidth < 768)
|
||||
const handleResize = () => {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
if (!isMobile.value) {
|
||||
showMobileMenu.value = false // 桌面端时关闭移动菜单
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
const toggleTheme = () => {
|
||||
store.commit('toggleTheme')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.theme-dark {
|
||||
/* 全局文字颜色、背景等由全局样式统一设置,此处仅对组件部分做局部覆盖 */
|
||||
background-color: rgba(40, 40, 40);
|
||||
color: #0ff;
|
||||
|
||||
}
|
||||
.theme-dark .navbar {
|
||||
background-color: rgba(40, 40, 40);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.theme-dark .nav-left .logo {
|
||||
color: #0ff;
|
||||
}
|
||||
.theme-dark .nav-items li a {
|
||||
color: #0ff;
|
||||
}
|
||||
.theme-dark .nav-items li a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.theme-dark .nav-items li a::after {
|
||||
background-color: #0ff;
|
||||
}
|
||||
.theme-dark .login-btn {
|
||||
border: 1px solid #0ff;
|
||||
color: #0ff;
|
||||
}
|
||||
.theme-dark .login-btn:hover {
|
||||
background: #0ff;
|
||||
color: #000;
|
||||
}
|
||||
.theme-dark .hamburger-icon,
|
||||
.theme-dark .hamburger-icon::before,
|
||||
.theme-dark .hamburger-icon::after {
|
||||
background: #0ff;
|
||||
}
|
||||
|
||||
|
||||
.theme-light {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
.theme-light .navbar {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.theme-light .nav-left .logo {
|
||||
color: #2f2f2f;
|
||||
}
|
||||
.theme-light .nav-items li a {
|
||||
color: #2f2f2f;
|
||||
}
|
||||
.theme-light .nav-items li a:hover {
|
||||
color: #000000;
|
||||
}
|
||||
.theme-light .nav-items li a::after {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
.theme-light .login-btn {
|
||||
border: 1px solid #2f2f2f;
|
||||
color: #2f2f2f;
|
||||
}
|
||||
.theme-light .login-btn:hover {
|
||||
background: #2f2f2f;
|
||||
color: #fff;
|
||||
}
|
||||
.theme-light .hamburger-icon,
|
||||
.theme-light .hamburger-icon::before,
|
||||
.theme-light .hamburger-icon::after {
|
||||
background: #2f2f2f;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 2rem;
|
||||
box-sizing: border-box;
|
||||
height: 60px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-left .logo {
|
||||
font-family: 'Netron', sans-serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.nav-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.nav-items {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.nav-items li a {
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
font-size: 1rem;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.nav-items li a::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -4px;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
transform: scaleX(0);
|
||||
transform-origin: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.nav-items li a:hover::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.login-btn {
|
||||
background: transparent;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.user-info .avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* 主题切换按钮 */
|
||||
.theme-toggle-btn {
|
||||
margin-left: 1rem;
|
||||
background: transparent;
|
||||
border: 1px solid currentColor;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 移动端:汉堡菜单样式 */
|
||||
.mobile-menu {
|
||||
position: relative;
|
||||
}
|
||||
.hamburger-btn {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
.hamburger-icon {
|
||||
width: 25px;
|
||||
height: 3px;
|
||||
display: block;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.hamburger-icon::before,
|
||||
.hamburger-icon::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25px;
|
||||
height: 3px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.hamburger-icon::before {
|
||||
left: 0;
|
||||
top: -8px;
|
||||
}
|
||||
.hamburger-icon::after {
|
||||
left: 0;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
/* 移动端下拉菜单 */
|
||||
.mobile-nav-items {
|
||||
position: absolute;
|
||||
backdrop-filter: blur(10px);
|
||||
top: 45px;
|
||||
width: 200%;
|
||||
right: -125%;
|
||||
list-style: none;
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.mobile-nav-items li {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.mobile-nav-items li a {
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 下拉菜单渐隐渐现动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
120
src/components/Projects_projectBox.vue
Normal file
120
src/components/Projects_projectBox.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
project: Object,
|
||||
});
|
||||
|
||||
// 根据项目状态决定背景颜色
|
||||
const statusColors = {
|
||||
"进行中": "#ff9800", // 橙色
|
||||
"已完成": "#4caf50", // 绿色
|
||||
"暂停": "#f44336", // 红色
|
||||
"待定": "#9e9e9e" // 灰色
|
||||
};
|
||||
|
||||
// 获取项目状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
return statusColors[status] || "#9e9e9e"; // 默认灰色
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link :to="'/projects/' + project.id">
|
||||
<div class="project-box" :style="{ borderColor: getStatusColor(project.status) }">
|
||||
<img :src="project.image" alt="Project image" class="project-image" />
|
||||
<div class="project-details">
|
||||
<h3>{{ project.name }}</h3>
|
||||
<p class="author">{{ project.author.join("、") }}</p>
|
||||
<p class="status" :style="{ backgroundColor: getStatusColor(project.status) }">{{ project.status }}</p>
|
||||
<div class="tags">
|
||||
<span v-for="tag in project.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.project-box {
|
||||
width: 350px; /* 增大盒子宽度 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
color: white;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2); /* 更强的阴影效果 */
|
||||
transition: transform 0.1s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.project-box:hover {
|
||||
transform: scale(1.01); /* 鼠标悬停时放大盒子 */
|
||||
}
|
||||
|
||||
.project-image {
|
||||
width: 100%;
|
||||
height: 150px; /* 增加图片高度 */
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.project-details {
|
||||
padding: 10px 15px 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.theme-light .project-details {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.project-details h3 {
|
||||
font-size: 20px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.author {
|
||||
font-size: 16px;
|
||||
color: #7c7c7c;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 4px 10px;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
color: #007bff;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 亮色模式下 */
|
||||
.theme-light .project-box {
|
||||
color: black;
|
||||
border-color: #ffb74d;
|
||||
}
|
||||
|
||||
.theme-light .tag {
|
||||
border-color: #ffb74d;
|
||||
color: #ffb74d;
|
||||
}
|
||||
</style>
|
115
src/components/Tools_box.vue
Normal file
115
src/components/Tools_box.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
tool: Object
|
||||
});
|
||||
|
||||
const imageURL = (r1, r2) => {
|
||||
return `https://picsum.photos/${220 + Math.floor(r1 * 500)}/${220 + Math.floor(r2 * 500)}`;
|
||||
};
|
||||
|
||||
const hover = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tool-box"
|
||||
@mouseover="hover = true"
|
||||
@mouseleave="hover = false"
|
||||
>
|
||||
<div class="image-container">
|
||||
<img :src="tool.image" :alt="tool.title" />
|
||||
</div>
|
||||
<div class="text"><h3>{{ tool.title }}</h3>
|
||||
<p>{{ tool.description }}</p></div>
|
||||
<div class="text-background">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tool-box {
|
||||
position: relative;
|
||||
width: 170px;
|
||||
height: 225px;
|
||||
border: cyan solid 1px;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theme-light .tool-box {
|
||||
border: #ffb74d solid 1px;
|
||||
}
|
||||
|
||||
.tool-box:hover {
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
.image-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.image-container img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.text-background {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background: linear-gradient(to top, black, transparent);
|
||||
color: white;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background: linear-gradient(to top, black, transparent);
|
||||
color: white;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.text h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
white-space: nowrap;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.text p {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.tool-box:hover .text-background {
|
||||
background: linear-gradient(to top, black 60%, transparent);
|
||||
transform: scaleY(5);
|
||||
}
|
||||
|
||||
.tool-box:hover h3 {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.tool-box:hover p {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
</style>
|
67
src/components/mdRenderer.vue
Normal file
67
src/components/mdRenderer.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div v-html="convertedMarkdown" class="markdown-container"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, watchEffect } from 'vue';
|
||||
import { marked } from 'marked'; // Use named import
|
||||
|
||||
export default {
|
||||
name: 'MarkdownViewer',
|
||||
props: {
|
||||
markdownContent: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const convertedMarkdown = ref('');
|
||||
|
||||
watchEffect(() => {
|
||||
convertedMarkdown.value = marked.parse(props.markdownContent); // Use marked.parse()
|
||||
});
|
||||
|
||||
return {
|
||||
convertedMarkdown
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-container {
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-container h1,
|
||||
.markdown-container h2,
|
||||
.markdown-container h3 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.markdown-container p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.markdown-container code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.markdown-container pre {
|
||||
background-color: #f4f4f4;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.markdown-container hr {
|
||||
width: 80%;
|
||||
height: 1px;
|
||||
background: linear-gradient(to bottom, rgba(217, 217, 217, 0.6), rgba(114, 114, 114, 0.6));
|
||||
margin: 20px 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
10
src/main.js
Normal file
10
src/main.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import './style.css'
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(store);
|
||||
app.mount('#app')
|
49
src/pages/About.vue
Normal file
49
src/pages/About.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="site-log">
|
||||
<MarkdownViewer :markdownContent="markdownText" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import MarkdownViewer from '../components/mdRenderer.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MarkdownViewer
|
||||
},
|
||||
setup() {
|
||||
const markdownText = ref('');
|
||||
|
||||
// 加载 Markdown 文件
|
||||
const loadMarkdown = async () => {
|
||||
try {
|
||||
const response = await fetch('log.md'); // 确保路径正确
|
||||
if (response.ok) {
|
||||
markdownText.value = await response.text();
|
||||
} else {
|
||||
console.error('Failed to load markdown file');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading markdown file', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadMarkdown(); // 页面加载时调用
|
||||
});
|
||||
|
||||
return {
|
||||
markdownText
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.site-log {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
219
src/pages/Blog_home.vue
Normal file
219
src/pages/Blog_home.vue
Normal file
@ -0,0 +1,219 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import BlogBox from "../components/Blog_box.vue";
|
||||
|
||||
const tags = ref([
|
||||
{ name: '家猪饲养', active: false },
|
||||
{ name: '猪舍管理', active: false },
|
||||
{ name: '猪饲料配方', active: false },
|
||||
{ name: '猪种选择', active: false },
|
||||
{ name: '猪场建设', active: false },
|
||||
{ name: '环境控制', active: false },
|
||||
{ name: '疫病防控', active: false },
|
||||
{ name: '生猪免疫', active: false },
|
||||
{ name: '饲养技术', active: false },
|
||||
{ name: '家猪繁育', active: false },
|
||||
{ name: '猪瘟预防', active: false },
|
||||
{ name: '生猪疫苗', active: false },
|
||||
{ name: '屠宰加工', active: false },
|
||||
{ name: '猪肉品质', active: false },
|
||||
{ name: '养猪设备', active: false },
|
||||
{ name: '自动喂料', active: false },
|
||||
{ name: '猪粪处理', active: false },
|
||||
{ name: '循环利用', active: false },
|
||||
{ name: '生猪运输', active: false },
|
||||
{ name: '养猪政策', active: false },
|
||||
{ name: '生猪市场', active: false },
|
||||
{ name: '风险控制', active: false },
|
||||
{ name: '遗传改良', active: false },
|
||||
{ name: '饲料添加剂', active: false },
|
||||
{ name: '生长调控', active: false },
|
||||
{ name: '消化率提升', active: false },
|
||||
{ name: '高效养猪', active: false },
|
||||
{ name: '猪场消毒', active: false },
|
||||
{ name: '智能养猪', active: false },
|
||||
{ name: '数据养猪', active: false },
|
||||
{ name: '健康管理', active: false },
|
||||
{ name: '繁殖管理', active: false },
|
||||
{ name: '环境优化', active: false },
|
||||
{ name: '供应链管理', active: false },
|
||||
{ name: '抗生素替代', active: false },
|
||||
{ name: '兽医服务', active: false },
|
||||
{ name: '营养管理', active: false },
|
||||
{ name: '通风系统', active: false },
|
||||
{ name: '饲料采购', active: false },
|
||||
{ name: '种植基地', active: false },
|
||||
{ name: '畜牧业创新', active: false },
|
||||
{ name: '经济效益', active: false },
|
||||
{ name: '信息化管理', active: false },
|
||||
{ name: '肉品追溯', active: false },
|
||||
{ name: '生长周期', active: false },
|
||||
{ name: '劳动管理', active: false },
|
||||
{ name: '粪污利用', active: false },
|
||||
{ name: '智慧畜牧', active: false },
|
||||
{ name: '可持续发展', active: false },
|
||||
{ name: '生态养猪', active: false }
|
||||
]);
|
||||
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
const imageURL = (r1, r2) => {
|
||||
return `https://picsum.photos/${220 + Math.floor(r1 * 500)}/${220 + Math.floor(r2 * 500)}`
|
||||
}
|
||||
const blogs = ref([
|
||||
{ title: '如何提升家猪的生长速度', author: '张强', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-12 08:30', tags: ['家猪饲养', '生长调控'] },
|
||||
{ title: '猪舍管理的最佳实践', author: '李梅', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-11 16:45', tags: ['猪舍管理', '环境优化'] },
|
||||
{ title: '选择优质猪种的重要性', author: '王涛', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-10 10:15', tags: ['猪种选择', '遗传改良'] },
|
||||
{ title: '猪饲料配方调整技巧', author: '刘刚', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-09 14:00', tags: ['猪饲料配方', '饲料添加剂'] },
|
||||
{ title: '现代猪场建设的关键要素', author: '陈峰', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-08 09:50', tags: ['猪场建设', '智能养猪'] },
|
||||
{ title: '如何有效控制养猪环境', author: '赵立', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-07 11:30', tags: ['环境控制', '猪舍管理'] },
|
||||
{ title: '猪免疫接种的最佳时机', author: '孙华', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-06 13:00', tags: ['生猪免疫', '疫病防控'] },
|
||||
{ title: '如何提高猪肉的品质', author: '周凯', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-05 15:00', tags: ['猪肉品质', '屠宰加工'] },
|
||||
{ title: '智能化养猪设备的应用', author: '杨峰', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-04 09:20', tags: ['养猪设备', '自动喂料'] },
|
||||
{ title: '猪粪处理与资源回收的创新', author: '吴敏', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-03 17:30', tags: ['猪粪处理', '循环利用'] },
|
||||
{ title: '生猪疫苗的选用标准', author: '高雪', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-02 11:00', tags: ['生猪疫苗', '疫病防控'] },
|
||||
{ title: '如何建立高效的猪场管理体系', author: '张涛', image: imageURL(Math.random(),Math.random()), creatTime: '2025-02-01 13:30', tags: ['饲养技术', '风险控制'] },
|
||||
{ title: '从猪场到市场的供应链管理', author: '李丹', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-31 16:40', tags: ['供应链管理', '生猪市场'] },
|
||||
{ title: '抗生素替代技术的研究与应用', author: '周鑫', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-30 10:50', tags: ['抗生素替代', '饲料添加剂'] },
|
||||
{ title: '生态养猪:环境友好与可持续发展', author: '刘婧', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-29 08:00', tags: ['生态养猪', '可持续发展'] },
|
||||
{ title: '家猪繁殖管理的最佳方法', author: '陈光', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-28 09:00', tags: ['家猪繁育', '繁殖管理'] },
|
||||
{ title: '猪场智能化的未来趋势', author: '王慧', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-27 14:10', tags: ['智能养猪', '智慧畜牧'] },
|
||||
{ title: '优化猪肉品质的生产流程', author: '周华', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-26 16:25', tags: ['猪肉品质', '生长周期'] },
|
||||
{ title: '高效养猪的创新技术', author: '刘磊', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-25 13:40', tags: ['高效养猪', '饲养技术'] },
|
||||
{ title: '猪场管理的数字化转型', author: '张丽', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-24 12:10', tags: ['信息化管理', '数据养猪'] },
|
||||
{ title: '猪肉产品追溯系统的建设', author: '李平', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-23 15:30', tags: ['肉品追溯', '生猪免疫'] },
|
||||
{ title: '如何减少猪场的疫病传播', author: '赵新', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-22 17:00', tags: ['疫病防控', '猪舍管理'] },
|
||||
{ title: '科学管理猪场劳动成本', author: '王萌', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-21 11:15', tags: ['劳动管理', '经济效益'] },
|
||||
{ title: '猪种改良与遗传优化的前景', author: '孙凯', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-20 09:00', tags: ['遗传改良', '猪种选择'] },
|
||||
{ title: '生猪运输过程中风险的防控', author: '杨华', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-19 12:40', tags: ['生猪运输', '风险控制'] },
|
||||
{ title: '猪场的环保与粪污利用技术', author: '王波', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-18 14:10', tags: ['粪污利用', '生态养猪'] },
|
||||
{ title: '智能化管理提升养猪效益', author: '赵雪', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-17 13:30', tags: ['智能养猪', '高效养猪'] },
|
||||
{ title: '家猪养殖中的生长调控策略', author: '陈婷', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-16 10:30', tags: ['生长调控', '家猪饲养'] },
|
||||
{ title: '如何高效利用猪饲料', author: '李伟', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-15 09:10', tags: ['猪饲料配方', '饲料添加剂'] },
|
||||
{ title: '猪场废水处理与资源再利用', author: '周丹', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-14 12:30', tags: ['猪粪处理', '循环利用'] },
|
||||
{ title: '猪场防疫技术与管理措施', author: '王鑫', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-13 14:50', tags: ['疫病防控', '生猪免疫'] },
|
||||
{ title: '现代养猪业的科技创新', author: '刘辉', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-12 16:00', tags: ['畜牧业创新', '智能养猪'] },
|
||||
{ title: '新型猪肉加工技术解析', author: '高林', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-11 11:20', tags: ['屠宰加工', '猪肉品质'] },
|
||||
{ title: '猪场的健康管理体系建设', author: '陈杰', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-10 08:40', tags: ['健康管理', '饲养技术'] },
|
||||
{ title: '自动化养猪技术的优势分析', author: '李斌', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-09 13:30', tags: ['养猪设备', '自动喂料'] },
|
||||
{ title: '猪场食品安全与监管体系', author: '张文', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-08 15:10', tags: ['生猪免疫', '肉品追溯'] },
|
||||
{ title: '精准农业与家猪养殖的结合', author: '李龙', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-07 09:50', tags: ['精准农业', '智能养猪'] },
|
||||
{ title: '如何提高家猪的繁殖效率', author: '王丽', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-06 10:20', tags: ['家猪繁育', '繁殖管理'] },
|
||||
{ title: '猪舍设计与环境调控技术', author: '孙瑶', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-05 11:00', tags: ['猪舍管理', '环境控制'] },
|
||||
{ title: '如何选择最合适的猪种进行养殖', author: '赵林', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-04 14:00', tags: ['猪种选择', '遗传改良'] },
|
||||
{ title: '猪场环境污染控制与治理技术', author: '周波', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-03 16:30', tags: ['环境优化', '猪粪处理'] },
|
||||
{ title: '猪肉市场分析与趋势预测', author: '王海', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-02 10:50', tags: ['生猪市场', '经济效益'] },
|
||||
{ title: '家猪养殖中的健康风险控制', author: '杨东', image: imageURL(Math.random(),Math.random()), creatTime: '2025-01-01 09:30', tags: ['风险控制', '健康管理'] },
|
||||
{ title: '家猪繁育管理的难点与对策', author: '李彬', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-31 15:40', tags: ['家猪繁育', '繁殖管理'] },
|
||||
{ title: '猪场生产效率的提升路径', author: '陈明', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-30 13:00', tags: ['高效养猪', '饲养技术'] },
|
||||
{ title: '猪场废物利用与绿色养殖', author: '周宇', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-29 11:20', tags: ['循环利用', '生态养猪'] },
|
||||
{ title: '生猪运输中的安全问题与防控', author: '王飞', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-28 14:50', tags: ['生猪运输', '风险控制'] },
|
||||
{ title: '家猪免疫管理的关键措施', author: '刘薇', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-27 16:30', tags: ['生猪免疫', '疫病防控'] },
|
||||
{ title: '如何使用智能设备提高养猪效率', author: '赵婷', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-26 10:40', tags: ['智能养猪', '养猪设备'] },
|
||||
{ title: '猪肉品质的影响因素分析', author: '张莉', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-25 13:30', tags: ['猪肉品质', '生长周期'] },
|
||||
{ title: '猪场管理中数据分析的应用', author: '李帅', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-24 14:10', tags: ['数据养猪', '信息化管理'] },
|
||||
{ title: '猪种选择对养殖效益的影响', author: '王月', image: imageURL(Math.random(),Math.random()), creatTime: '2024-12-23 16:40', tags: ['猪种选择', '经济效益'] }
|
||||
])
|
||||
|
||||
|
||||
|
||||
const toggleTag = (tag) => {
|
||||
tag.active = !tag.active;
|
||||
};
|
||||
|
||||
const filterBlogs = () => {
|
||||
return blogs.value.filter(blog => {
|
||||
const matchesTags = blog.tags.some(tag => tags.value.find(t => t.name === tag && t.active));
|
||||
const matchesSearch = blog.title.toLowerCase().includes(searchQuery.value.toLowerCase());
|
||||
return (matchesTags || tags.value.every(t => !t.active)) && matchesSearch;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="filters">
|
||||
<div class="tags">
|
||||
<span v-for="tag in tags" :key="tag.name" :class="{ active: tag.active }" @click="toggleTag(tag)">
|
||||
{{ tag.name }}
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" v-model="searchQuery" placeholder="搜索博客..." class="search-bar" />
|
||||
</div>
|
||||
|
||||
<div class="blogs">
|
||||
<BlogBox v-for="blog in filterBlogs()" :key="blog.title" :blog="blog" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
height: calc(100vh - 100px);
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
//margin-top: 70px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* 允许标签换行 */
|
||||
gap: 10px;
|
||||
justify-content: center; /* 使标签居中 */
|
||||
}
|
||||
|
||||
.tags span {
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.tags span.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 10px;
|
||||
margin: 20px 0;
|
||||
width: 50%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.blogs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-light .tags span.active {
|
||||
background-color: #ffb74d;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.theme-light .search-bar {
|
||||
border: 1px solid #ffb74d;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
51
src/pages/Home.vue
Normal file
51
src/pages/Home.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container-pig">
|
||||
<img alt="猪图" src="https://q6.itc.cn/images01/20241006/86f4aab7fded4c2283842e79f7e6eda3.jpeg"/>
|
||||
<h1>6A级金牌养猪基地 </h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.container-pig {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.container-pig img {
|
||||
position:absolute ;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.container-pig h1 {
|
||||
z-index: 19;
|
||||
font-size: 80px;
|
||||
font-weight: bold;
|
||||
animation: flashColors 0.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes flashColors {
|
||||
0% {
|
||||
color: red;
|
||||
}
|
||||
49.9% {
|
||||
color: red;
|
||||
}
|
||||
50% {
|
||||
color: yellow;
|
||||
}99.9% {
|
||||
color: yellow;
|
||||
}
|
||||
100% {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
315
src/pages/Login.vue
Normal file
315
src/pages/Login.vue
Normal file
@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<div class="auth-tabs">
|
||||
<button :class="{ active: mode === 'login' }" @click="mode = 'login'">登录</button>
|
||||
<button :class="{ active: mode === 'register' }" @click="mode = 'register'">注册</button>
|
||||
</div>
|
||||
|
||||
<transition name="fade" mode="out-in">
|
||||
<div key="login" v-if="mode === 'login'" class="auth-form">
|
||||
<form @submit.prevent="handleLogin">
|
||||
<h2>欢迎回来</h2>
|
||||
<div class="form-group">
|
||||
<input type="text" placeholder="用户名" v-model="loginInfo.user"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input :type="viewPSWD?'text':'password'" placeholder="密码" v-model="loginInfo.password"/>
|
||||
</div>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
<div key="register" v-else class="auth-form">
|
||||
<form @submit.prevent="handleRegister">
|
||||
<h2>创建新账号</h2>
|
||||
<div class="form-group">
|
||||
<input type="text" placeholder="用户名" v-model="loginInfo.user"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="email" placeholder="邮箱" v-model="loginInfo.email"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" placeholder="密码" v-model="loginInfo.password"/>
|
||||
</div>
|
||||
<button type="submit">注册</button>
|
||||
</form>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue'
|
||||
import swal from "../utils/sweetalert.js";
|
||||
import store from "../store/index.js";
|
||||
import router from "../router/index.js";
|
||||
import AuthService from "../../services/auth.js";
|
||||
|
||||
const mode = ref('login')
|
||||
const viewPSWD = ref(false)
|
||||
|
||||
const loginInfo = ref({
|
||||
user: '',
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const handleLogin = async () => {
|
||||
await (async function () {
|
||||
if (
|
||||
!(loginInfo.value.user && loginInfo.value.password)
|
||||
) {
|
||||
swal.tip('error', '用户名和密码不得为空!');
|
||||
return;
|
||||
}
|
||||
store.commit('startLoading', "正在验证...");
|
||||
|
||||
const result = await AuthService.login(loginInfo.value.user, loginInfo.value.password);
|
||||
|
||||
if (result.code === 1) {
|
||||
swal.tip('error', '用户名或密码错误!');
|
||||
return;
|
||||
}
|
||||
if (result.code === 2) {
|
||||
swal.tip('error', '服务端程序出错...');
|
||||
return;
|
||||
}
|
||||
if (result.code === 3) {
|
||||
swal.tip('error', '网络连接错误');
|
||||
return;
|
||||
}
|
||||
store.commit('setToken', result.token);
|
||||
|
||||
store.commit('startLoading', '请稍后...');
|
||||
await AuthService.setSelfInfo();
|
||||
if (!store.state.token) {
|
||||
swal.tip('error', '未知错误...');
|
||||
return;
|
||||
}
|
||||
if (!store.state.userInfo.uid) {
|
||||
swal.tip('info', '登录成功, 加载用户信息失败');
|
||||
return;
|
||||
}
|
||||
store.commit('stopLoading');
|
||||
swal.tip('success', '登录成功! 即将跳转');
|
||||
loginInfo.value.isFinish = true;
|
||||
setTimeout(() => {
|
||||
router.push('/');
|
||||
}, 2000);
|
||||
}
|
||||
)
|
||||
();
|
||||
store.commit('stopLoading');
|
||||
}
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!(loginInfo.value.user && loginInfo.value.password && loginInfo.value.email)) {
|
||||
swal.tip('error', '用户名、邮箱及密码不得为空!');
|
||||
store.commit('stopLoading');
|
||||
return;
|
||||
}
|
||||
if (!(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(loginInfo.value.email))) {
|
||||
swal.tip('error', '邮箱格式不正确');
|
||||
store.commit('stopLoading');
|
||||
return;
|
||||
}
|
||||
store.commit('startLoading', "请稍后...");
|
||||
try {
|
||||
|
||||
const message = await AuthService.sendMessage(loginInfo.value.user, loginInfo.value.email, loginInfo.value.password);
|
||||
|
||||
if (message.code !== 0) {
|
||||
swal.tip('error', '注册失败(已存在此用户)');
|
||||
store.commit('stopLoading');
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
swal.tip('error', '服务器连接错误...');
|
||||
store.commit('stopLoading');
|
||||
return;
|
||||
}
|
||||
store.commit('stopLoading');
|
||||
swal.window('info', '邮箱验证链接发送成功!请前往邮箱查看', null, 'ok', '好的');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* =======================================
|
||||
全局变量
|
||||
========================================== */
|
||||
:global(:root) {
|
||||
--bg-color: #000;
|
||||
--accent-color: #005757;
|
||||
--text-color: #fff;
|
||||
--card-bg: rgba(0, 0, 0, 0.8);
|
||||
--input-bg: #222;
|
||||
--input-text: #fff;
|
||||
--border-color: #00ffff;
|
||||
}
|
||||
|
||||
:global(.theme-light) {
|
||||
--bg-color: #fff;
|
||||
--accent-color: #eea2a2;
|
||||
--text-color: #2f2f2f;
|
||||
--card-bg: #f9f9f9;
|
||||
--input-bg: #fff;
|
||||
--input-text: #2f2f2f;
|
||||
--border-color: #2f2f2f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* =======================================
|
||||
整体页面布局
|
||||
========================================== */
|
||||
.auth-page {
|
||||
min-height: calc(100vh - 60px);
|
||||
margin-top: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
background: linear-gradient(45deg, var(--bg-color), var(--accent-color));
|
||||
background-size: 400% 400%;
|
||||
animation: gradientAnimation 15s ease infinite, bgDisplay 0.5s forwards;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
@keyframes bgDisplay {
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradientAnimation {
|
||||
0% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================
|
||||
表单卡片
|
||||
========================================== */
|
||||
.auth-card {
|
||||
background: var(--card-bg);
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
width: 320px;
|
||||
text-align: center;
|
||||
height: 400px;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
animation: cardFadeIn 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes cardFadeIn {
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================================
|
||||
登册切换
|
||||
========================================== */
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.auth-tabs button {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-color);
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.auth-tabs button.active {
|
||||
border-color: var(--accent-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* =======================================
|
||||
表单
|
||||
========================================== */
|
||||
.auth-form h2 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 5px;
|
||||
background: var(--input-bg);
|
||||
color: var(--input-text);
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 5px var(--accent-color);
|
||||
}
|
||||
|
||||
.auth-form button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: var(--accent-color);
|
||||
color: var(--bg-color);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s, transform 0.2s;
|
||||
}
|
||||
|
||||
.auth-form button:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.auth-form button:active {
|
||||
transform: scale(0.980);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
195
src/pages/Projects_home.vue
Normal file
195
src/pages/Projects_home.vue
Normal file
@ -0,0 +1,195 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import Projects_projectBox from '../components/Projects_projectBox.vue'
|
||||
|
||||
// 搜索输入
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 用于生成随机图片尺寸
|
||||
const imageURL = (r1, r2) => {
|
||||
return `https://picsum.photos/${220 + Math.floor(r1 * 500)}/${220 + Math.floor(r2 * 500)}`
|
||||
}
|
||||
|
||||
// 示例项目数据
|
||||
const projects = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '智能养殖系统',
|
||||
author: ["张三", "李四"],
|
||||
image: imageURL(Math.random(), Math.random()),
|
||||
status: '进行中',
|
||||
tags: ['智能养殖', '自动喂养', '数据分析', '物联网']
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '绿色生态猪场',
|
||||
author: ["王五", "赵六"],
|
||||
image: imageURL(Math.random(), Math.random()),
|
||||
status: '已完成',
|
||||
tags: ['环保', '生态养殖', '有机饲料', '循环利用']
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '基因优化猪种',
|
||||
author: ["刘七", "杨八"],
|
||||
image: imageURL(Math.random(), Math.random()),
|
||||
status: '待定',
|
||||
tags: ['基因研究', '猪种改良', '优质肉类', '育种技术']
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '猪肉生产透明化',
|
||||
author: ["孙九", "钱十"],
|
||||
image: imageURL(Math.random(), Math.random()),
|
||||
status: '暂停',
|
||||
tags: ['透明供应链', '动物福利', '溯源技术', '肉品安全']
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '猪场废弃物处理',
|
||||
author: ["吴十一", "郑十二"],
|
||||
image: imageURL(Math.random(), Math.random()),
|
||||
status: '进行中',
|
||||
tags: ['废物利用', '环保技术', '沼气发电', '饲料再生']
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '猪肉替代品研发',
|
||||
author: ["陈十三", "冯十四"],
|
||||
image: imageURL(Math.random(), Math.random()),
|
||||
status: '已完成',
|
||||
tags: ['植物肉', '替代蛋白', '创新食品', '健康饮食']
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '猪养殖智能监控',
|
||||
author: ["郑十五", "许十六"],
|
||||
image: imageURL(Math.random(), Math.random()),
|
||||
status: '进行中',
|
||||
tags: ['远程监控', '人工智能', '动物健康', '环境控制']
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '猪饲料创新配方',
|
||||
author: ["冯十七", "唐十八"],
|
||||
image: imageURL(Math.random(), Math.random()),
|
||||
status: '待定',
|
||||
tags: ['营养优化', '创新饲料', '成本降低', '动物健康']
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
/**
|
||||
* 解析搜索栏的输入,将其按空格拆分为多个关键字 tokens
|
||||
* 例如输入 "Vue3 应用" => ["Vue3", "应用"]
|
||||
*/
|
||||
const parseSearchInput = (input) => {
|
||||
return input
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean) // 过滤空字符串
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据搜索栏输入,过滤项目
|
||||
* 需求:搜索关键字既可以匹配项目名称,也可以匹配项目的 tags
|
||||
* 并且多个关键字之间使用 "AND" 逻辑
|
||||
*/
|
||||
const filterProjects = () => {
|
||||
// 如果没有输入任何搜索,直接返回全部
|
||||
if (!searchQuery.value.trim()) {
|
||||
return projects.value
|
||||
}
|
||||
|
||||
// 获取用户输入的关键词数组
|
||||
const tokens = parseSearchInput(searchQuery.value.toLowerCase())
|
||||
|
||||
return projects.value.filter((project) => {
|
||||
// 将项目名称和标签都转换为小写,用于匹配
|
||||
const nameLower = project.name.toLowerCase()
|
||||
const tagsLower = project.tags.map((tag) => tag.toLowerCase())
|
||||
|
||||
// 对于每个 token,都要求能在名称或标签中找到
|
||||
return tokens.every((token) => {
|
||||
// 名称包含 token,或某个标签包含 token
|
||||
return (
|
||||
nameLower.includes(token) ||
|
||||
tagsLower.some((tag) => tag.includes(token))
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="filters">
|
||||
<!-- 将标签的功能合并到搜索栏内 -->
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索(多个标签或关键词,空格分割)"
|
||||
class="search-bar"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 按照搜索结果展示项目 -->
|
||||
<div class="projects">
|
||||
<Projects_projectBox
|
||||
v-for="project in filterProjects()"
|
||||
:key="project.name"
|
||||
:project="project"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
height: calc(100vh - 100px);
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 筛选区域 */
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
padding: 10px;
|
||||
margin: 20px 0;
|
||||
width: 50%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* 项目区域 */
|
||||
.projects {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条,可按需保留 */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 亮色模式下可适当调整样式 */
|
||||
.theme-light .search-bar {
|
||||
border: 1px solid #ffb74d;
|
||||
}
|
||||
</style>
|
160
src/pages/Tools_home.vue
Normal file
160
src/pages/Tools_home.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ToolsBox from "../components/Tools_box.vue";
|
||||
|
||||
const categories = ref([
|
||||
{ name: '家猪饲养', active: false },
|
||||
{ name: '猪舍管理', active: false },
|
||||
{ name: '猪种选择', active: false },
|
||||
{ name: '健康管理', active: false },
|
||||
{ name: '市场分析', active: false },
|
||||
{ name: '饲料管理', active: false },
|
||||
{ name: '生产管理', active: false },
|
||||
{ name: '智能农业', active: false },
|
||||
{ name: '繁殖管理', active: false }
|
||||
]);
|
||||
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
const imageURL = (r1, r2) => {
|
||||
return `https://picsum.photos/${220 + Math.floor(r1 * 500)}/${220 + Math.floor(r2 * 500)}`;
|
||||
};
|
||||
|
||||
const tools = ref([
|
||||
{ title: '猪饲料配方推荐', description: '根据不同阶段推荐饲料配方', image: imageURL(Math.random(), Math.random()), category: '家猪饲养' },
|
||||
{ title: '猪舍监测工具', description: '帮助管理猪舍环境参数的工具', image: imageURL(Math.random(), Math.random()), category: '猪舍管理' },
|
||||
{ title: '优质猪种推荐', description: '提供猪种选择与建议', image: imageURL(Math.random(), Math.random()), category: '猪种选择' },
|
||||
{ title: '猪群健康监测', description: '实时监控猪群健康状态', image: imageURL(Math.random(), Math.random()), category: '健康管理' },
|
||||
{ title: '猪肉价格分析', description: '提供最新猪肉市场价格数据', image: imageURL(Math.random(), Math.random()), category: '市场分析' },
|
||||
{ title: '饲料成本优化', description: '根据不同饲料成本进行优化建议', image: imageURL(Math.random(), Math.random()), category: '饲料管理' },
|
||||
{ title: '猪舍通风系统', description: '推荐最佳猪舍通风方案', image: imageURL(Math.random(), Math.random()), category: '猪舍管理' },
|
||||
{ title: '疾病预防与疫苗', description: '帮助制定猪群疫苗接种计划', image: imageURL(Math.random(), Math.random()), category: '健康管理' },
|
||||
{ title: '生猪生长监控', description: '追踪生猪的生长情况与速度', image: imageURL(Math.random(), Math.random()), category: '生产管理' },
|
||||
{ title: '猪只体重跟踪', description: '监控猪只体重的变化情况', image: imageURL(Math.random(), Math.random()), category: '生产管理' },
|
||||
{ title: '清洁与消毒工具', description: '提供猪舍清洁和消毒方法建议', image: imageURL(Math.random(), Math.random()), category: '猪舍管理' },
|
||||
{ title: '温湿度调控系统', description: '帮助调控猪舍内温湿度的系统', image: imageURL(Math.random(), Math.random()), category: '猪舍管理' },
|
||||
{ title: '猪只运动量跟踪', description: '监控猪只的运动量与活动情况', image: imageURL(Math.random(), Math.random()), category: '健康管理' },
|
||||
{ title: '猪舍智能喂养', description: '智能化猪舍喂养系统,减少人工干预', image: imageURL(Math.random(), Math.random()), category: '智能农业' },
|
||||
{ title: '生猪屠宰信息', description: '实时追踪生猪屠宰信息和数据', image: imageURL(Math.random(), Math.random()), category: '生产管理' },
|
||||
{ title: '饲料成分分析', description: '分析饲料中的各种营养成分', image: imageURL(Math.random(), Math.random()), category: '饲料管理' },
|
||||
{ title: '猪只健康评估', description: '帮助评估猪只健康状况的工具', image: imageURL(Math.random(), Math.random()), category: '健康管理' },
|
||||
{ title: '猪舍自动清理', description: '自动清理猪舍的工具', image: imageURL(Math.random(), Math.random()), category: '猪舍管理' },
|
||||
{ title: '猪群繁殖优化', description: '优化猪群繁殖效率的工具', image: imageURL(Math.random(), Math.random()), category: '繁殖管理' },
|
||||
{ title: '空气质量监测', description: '监测猪舍空气质量,提供改善建议', image: imageURL(Math.random(), Math.random()), category: '猪舍管理' },
|
||||
]);
|
||||
|
||||
|
||||
// Toggle function for categories, only one category can be active at a time
|
||||
const toggleCategory = (category) => {
|
||||
if (category.active) {
|
||||
category.active = false; // Deselect if already selected
|
||||
} else {
|
||||
categories.value.forEach(cat => cat.active = false); // Deselect all other categories
|
||||
category.active = true; // Select the clicked category
|
||||
}
|
||||
};
|
||||
|
||||
const filterTools = () => {
|
||||
return tools.value.filter(tool => {
|
||||
const matchesCategory = categories.value.some(cat => cat.active && cat.name === tool.category);
|
||||
const matchesSearch = tool.title.toLowerCase().includes(searchQuery.value.toLowerCase());
|
||||
return (!categories.value.some(cat => cat.active) || matchesCategory) && matchesSearch;
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="filters">
|
||||
<div class="categories">
|
||||
<span
|
||||
v-for="category in categories"
|
||||
:key="category.name"
|
||||
:class="{ active: category.active }"
|
||||
@click="toggleCategory(category)"
|
||||
>
|
||||
{{ category.name }}
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" v-model="searchQuery" placeholder="搜索工具..." class="search-bar" />
|
||||
</div>
|
||||
|
||||
<div class="tools">
|
||||
<ToolsBox
|
||||
v-for="tool in filterTools()"
|
||||
:key="tool.title"
|
||||
:tool="tool"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
height: calc(100vh - 100px);
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.categories {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.categories span {
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.categories span.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
padding: 10px;
|
||||
margin: 20px 0;
|
||||
width: 50%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-light .categories span.active {
|
||||
background-color: #ffb74d;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.theme-light .search-bar {
|
||||
border: 1px solid #ffb74d;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
203
src/pages/accountPages/Account.vue
Normal file
203
src/pages/accountPages/Account.vue
Normal file
@ -0,0 +1,203 @@
|
||||
<script setup>
|
||||
import {ref, onMounted} from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import UserInfo from './Account_userInfo.vue';
|
||||
import store from "../../store/index.js"; // 引入右侧用户信息组件
|
||||
|
||||
const isMobile = ref(false); // 控制是否为移动端布局
|
||||
|
||||
// 监听视口宽度变化,决定是否展示右侧的用户信息界面
|
||||
const checkWindowSize = () => {
|
||||
isMobile.value = window.innerWidth < 1200;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkWindowSize();
|
||||
window.addEventListener('resize', checkWindowSize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="account-container">
|
||||
<div class="sidebar">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/account/setting">账号设置</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/account/self-page">个人主页</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/account/works-manage">稿件管理</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/account/draft">草稿箱</router-link>
|
||||
</li>
|
||||
<li v-if="store.state.userInfo.role_id >= 1">
|
||||
<router-link to="/account/upload-log">管理员: 上传日志</router-link>
|
||||
</li>
|
||||
|
||||
<li v-if="isMobile">
|
||||
<router-link to="/account">用户信息</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 中间视图区 -->
|
||||
<div class="content">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
<!-- 右侧用户信息显示 -->
|
||||
<div class="user-info" v-if="!isMobile && !/^\/account\/*$/.test(useRoute().path)">
|
||||
<UserInfo/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.account-container {
|
||||
display: flex;
|
||||
flex-wrap: inherit;
|
||||
height: calc(100vh - 60px);
|
||||
overflow-x: hidden;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
background-color: #2e2e2e;
|
||||
padding: 20px;
|
||||
transition: background-color 0.3s;
|
||||
border-right: 1px solid #444;
|
||||
}
|
||||
|
||||
.sidebar ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar li {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
background-color: #64b5f6; /* 蓝色点缀 */
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
padding: 20px 0;
|
||||
overflow-y: scroll;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
/* 右侧用户信息部分 */
|
||||
.user-info {
|
||||
width: 280px;
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark Mode Styles */
|
||||
.account-container {
|
||||
background-color: #181818;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #2e2e2e;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
background-color: #64b5f6; /* 蓝色点缀 */
|
||||
}
|
||||
|
||||
.user-info {
|
||||
background-color: #2e2e2e;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
margin-left: 0;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.theme-light .account-container {
|
||||
background-color: #fafafa;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.theme-light .sidebar {
|
||||
background-color: #fff;
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.theme-light .sidebar a {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.theme-light .sidebar a:hover {
|
||||
background-color: #ffb74d; /* 黄色点缀 */
|
||||
}
|
||||
|
||||
.theme-light .content {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.theme-light .user-info {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.theme-light .sidebar a:hover {
|
||||
background-color: #ffb74d; /* 黄色点缀 */
|
||||
}
|
||||
|
||||
@media (max-width: 745px) {
|
||||
.account-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-right: none;
|
||||
}
|
||||
.sidebar ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar li {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
42
src/pages/accountPages/Account_admin_uploadLog.vue
Normal file
42
src/pages/accountPages/Account_admin_uploadLog.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<h1>上传日志</h1>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
color: #f5f6f7;
|
||||
padding: 20px 0;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
.theme-light .container {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
h1,p {
|
||||
margin: 0 25px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
100
src/pages/accountPages/Account_draft.vue
Normal file
100
src/pages/accountPages/Account_draft.vue
Normal file
@ -0,0 +1,100 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import AccountWorkPiece from "../../components/AccountWorkPiece.vue";
|
||||
|
||||
// 示例:草稿数据
|
||||
const drafts = ref([
|
||||
{
|
||||
cover: 'https://img1.baidu.com/it/u=427213910,646438716&fm=253',
|
||||
title: '打破传统界限:家猪饲养与繁殖的创新模式',
|
||||
createdTime: '2077-01-01 11:45',
|
||||
lastModifiedTime: '2077-01-02 09:19'
|
||||
}
|
||||
])
|
||||
|
||||
function createNewDraft() {
|
||||
console.log('新建博客草稿')
|
||||
// 在此处执行新建草稿逻辑
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 顶部按钮居中 -->
|
||||
<div class="top-bar">
|
||||
<button class="create-btn" @click="createNewDraft">新建博客</button>
|
||||
</div>
|
||||
<!-- 下面循环渲染草稿展示条 -->
|
||||
<div class="draft-list">
|
||||
<AccountWorkPiece
|
||||
v-for="(item, index) in drafts"
|
||||
:key="index"
|
||||
:cover="item.cover"
|
||||
:title="item.title"
|
||||
:createdTime="item.createdTime"
|
||||
:lastModifiedTime="item.lastModifiedTime"
|
||||
:isDraft="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #f5f6f7;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.theme-light .container {
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #3b6ea8;
|
||||
color: #f5f6f7;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.create-btn:hover {
|
||||
background-color: #2f5687;
|
||||
}
|
||||
|
||||
.theme-light .create-btn {
|
||||
background-color: #ffc107;
|
||||
color: #333333;
|
||||
}
|
||||
.theme-light .create-btn:hover {
|
||||
background-color: #e0a806;
|
||||
}
|
||||
|
||||
.draft-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
109
src/pages/accountPages/Account_selfpage.vue
Normal file
109
src/pages/accountPages/Account_selfpage.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const showIntro = ref(false);
|
||||
const showBlog = ref(false);
|
||||
const showProjects = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
|
||||
<div class="settings-item">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="showIntro" />
|
||||
展示简介
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="showBlog" />
|
||||
展示个人博客
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-item">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="showProjects" />
|
||||
展示参与的项目
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #f5f6f7;
|
||||
padding: 20px 0;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
/* Settings Item Styles */
|
||||
.settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 18px;
|
||||
width: 80%;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.checkbox-label input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
accent-color: #007bff;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.checkbox-label input:checked {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Dark Mode Styles */
|
||||
.selfpage-container {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
color: #fff;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.checkbox-label input {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
/* Light Mode Styles */
|
||||
.theme-light .selfpage-container {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.theme-light .checkbox-label {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.theme-light .checkbox-label input {
|
||||
accent-color: #ffb74d;
|
||||
border: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.theme-light .checkbox-label input:checked {
|
||||
border-color: #ffb74d;
|
||||
}
|
||||
|
||||
/* Fade-in animation */
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
484
src/pages/accountPages/Account_setting.vue
Normal file
484
src/pages/accountPages/Account_setting.vue
Normal file
@ -0,0 +1,484 @@
|
||||
<script setup>
|
||||
import {ref} from 'vue'
|
||||
import AuthService from "../../../services/auth.js";
|
||||
import router from "../../router/index.js";
|
||||
import store from "../../store/index.js";
|
||||
import api from "../../utils/axios.js";
|
||||
import swal from "../../utils/sweetalert.js";
|
||||
|
||||
|
||||
const permissionLevel = ref({
|
||||
"0" : "普通用户",
|
||||
"1" : "管理员"
|
||||
})
|
||||
|
||||
const isAvatarHover = ref(false)
|
||||
const isEditingUsername = ref(false)
|
||||
|
||||
const tempUsername = ref(store.state.userInfo.username)
|
||||
|
||||
const isEditingIntro = ref(false)
|
||||
|
||||
const userIntro = ref('')
|
||||
|
||||
const tempIntro = ref(userIntro.value)
|
||||
|
||||
const fileInput = ref(null);
|
||||
const openFileDialog = () => {
|
||||
fileInput.value.click();
|
||||
}
|
||||
|
||||
const handleFileChange = async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
fileInput.value = file;
|
||||
store.commit('startLoading', '正在上传图片...');
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
const jsonData = JSON.stringify({TOKEN: store.state.token});
|
||||
formData.append("jsondata", jsonData);
|
||||
const response = await api.post("/changeprofile", formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code === 0) {
|
||||
await AuthService.setSelfInfo();
|
||||
swal.tip('success', '头像上传成功! ');
|
||||
} else {
|
||||
swal.tip('error', '未知错误...');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("败:", error);
|
||||
swal.tip('error', '网络连接错误');
|
||||
}
|
||||
store.commit('stopLoading');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function editUsername() {
|
||||
isEditingUsername.value = true
|
||||
tempUsername.value = store.state.userInfo.username
|
||||
}
|
||||
|
||||
function confirmEditUsername() {
|
||||
swal.tip('error', '不让改');
|
||||
isEditingUsername.value = false
|
||||
}
|
||||
|
||||
function cancelEditUsername() {
|
||||
isEditingUsername.value = false
|
||||
}
|
||||
|
||||
function logout() {
|
||||
AuthService.logout();
|
||||
router.replace('/login')
|
||||
}
|
||||
|
||||
// 进入编辑简介
|
||||
function editIntro() {
|
||||
isEditingIntro.value = true
|
||||
tempIntro.value = userIntro.value
|
||||
}
|
||||
|
||||
// 取消编辑简介
|
||||
function cancelEditIntro() {
|
||||
isEditingIntro.value = false
|
||||
tempIntro.value = userIntro.value
|
||||
}
|
||||
|
||||
// 保存编辑简介
|
||||
function saveIntro() {
|
||||
userIntro.value = tempIntro.value
|
||||
isEditingIntro.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="profile-box">
|
||||
<!-- 头像 -->
|
||||
<div
|
||||
class="avatar-wrapper"
|
||||
@mouseenter="isAvatarHover = true"
|
||||
@mouseleave="isAvatarHover = false"
|
||||
>
|
||||
<img
|
||||
:src="store.getters.profileImage"
|
||||
alt="用户头像"
|
||||
class="avatar-image"
|
||||
/>
|
||||
<div :style="isAvatarHover ? 'opacity: 1;' : 'opacity: 0;'" class="avatar-overlay" @click="openFileDialog">
|
||||
可修改头像
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
accept="image/png, image/jpg, image/jpeg"
|
||||
style="display: none"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 信息 -->
|
||||
<div class="user-info">
|
||||
<div class="username-row">
|
||||
<label>用户名:</label>
|
||||
<span v-if="!isEditingUsername" class="username-text">
|
||||
{{ store.state.userInfo.username }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
class="username-input"
|
||||
v-model="tempUsername"
|
||||
/>
|
||||
|
||||
<!-- 改名 -->
|
||||
<span
|
||||
v-if="!isEditingUsername"
|
||||
class="edit-emoji"
|
||||
@click="editUsername"
|
||||
>
|
||||
🖊
|
||||
</span>
|
||||
<div v-if="isEditingUsername">
|
||||
<button
|
||||
class="confirm-btn"
|
||||
@click="confirmEditUsername"
|
||||
v-if="tempUsername !== store.state.userInfo.username"
|
||||
>
|
||||
√
|
||||
</button>
|
||||
<button
|
||||
class="cancel-btn"
|
||||
@click="cancelEditUsername"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="permission-row">
|
||||
<label>账号权限:</label>
|
||||
<span>{{ permissionLevel[store.state.userInfo.role_id] }}</span>
|
||||
</div>
|
||||
|
||||
<div class="register-date">
|
||||
<label>注册日期:</label>
|
||||
<span>{{ store.state.userInfo.birth?.split(' ')[0] || '未知' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button
|
||||
class="logout-btn"
|
||||
@click="logout"
|
||||
>登出
|
||||
</button>
|
||||
<div class="halving-line" />
|
||||
<label class="color-mode-box">
|
||||
<input type="radio" v-model="store.state.theme" value="light">亮色模式
|
||||
</label>
|
||||
<label class="color-mode-box">
|
||||
<input type="radio" v-model="store.state.theme" value="dark">深色模式
|
||||
</label>
|
||||
|
||||
<!-- -->
|
||||
<!-- <div class="intro-box">-->
|
||||
<!-- <label>个人简介:</label>-->
|
||||
<!-- <textarea-->
|
||||
<!-- class="intro-textarea"-->
|
||||
<!-- :readonly="!isEditingIntro"-->
|
||||
<!-- v-model="tempIntro"-->
|
||||
<!-- placeholder="空空如也~"-->
|
||||
<!-- />-->
|
||||
<!-- <!– 修改/保存/取消 按钮组 –>-->
|
||||
<!-- <div class="intro-actions">-->
|
||||
<!-- <!– 默认只显示“修改” –>-->
|
||||
<!-- <button-->
|
||||
<!-- v-if="!isEditingIntro"-->
|
||||
<!-- class="intro-edit-btn"-->
|
||||
<!-- @click="editIntro"-->
|
||||
<!-- >-->
|
||||
<!-- 修改-->
|
||||
<!-- </button>-->
|
||||
<!-- <!– 编辑状态下,显示“保存”和“取消” –>-->
|
||||
<!-- <div v-else class="intro-edit-group">-->
|
||||
<!-- <button class="intro-save-btn" @click="saveIntro">保存</button>-->
|
||||
<!-- <button class="intro-cancel-btn" @click="cancelEditIntro">取消</button>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ========== 公共容器区 ========== */
|
||||
.container {
|
||||
//height: calc(100vh - 60px);
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #f5f6f7;
|
||||
padding: 20px 0;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.theme-light .container {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
color: white;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.theme-light input, .theme-light textarea {
|
||||
color: #2a2a2a;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* ========== 个人信息区 ========== */
|
||||
.profile-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.theme-light .avatar-image {
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.avatar-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #f5f6f7;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.username-row,
|
||||
.permission-row,
|
||||
.register-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.username-text {
|
||||
margin: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.halving-line {
|
||||
width: 80%;
|
||||
height: 1px;
|
||||
background: linear-gradient(to bottom, rgba(217, 217, 217, 0.6), rgba(114, 114, 114, 0.6));
|
||||
margin: 20px 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ========== 编辑用户名输入框 ========== */
|
||||
.username-input {
|
||||
padding: 4px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* ========== 编辑笔按钮 ========== */
|
||||
.edit-emoji {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.edit-emoji:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ========== 确定按钮 ========== */
|
||||
.confirm-btn, .cancel-btn {
|
||||
margin: 0 3px;
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
background-color: #3b6ea8;
|
||||
color: #f5f6f7;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.confirm-btn:hover {
|
||||
background-color: #2f5687;
|
||||
}
|
||||
|
||||
/* 亮色模式下的确定按钮 */
|
||||
.theme-light .confirm-btn, .theme-light .cancel-btn {
|
||||
background-color: #ffc107;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.theme-light .confirm-btn:hover, .theme-light .cancel-btn:hover {
|
||||
background-color: #e0a806;
|
||||
}
|
||||
|
||||
/* ========== 颜色模式区 ========== */
|
||||
|
||||
.color-mode-box {
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.color-mode-box input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
accent-color: #007bff;
|
||||
margin-right: 10px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.color-mode-box input:checked {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.theme-light .color-mode-box input {
|
||||
accent-color: #ffb74d;
|
||||
}
|
||||
|
||||
/* ========== 个人简介区 ========== */
|
||||
.intro-box {
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.intro-textarea {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
margin-top: 5px;
|
||||
resize: vertical;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.intro-actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.intro-edit-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ========== 修改/保存/取消/登出 按钮 ========== */
|
||||
.intro-edit-btn,
|
||||
.intro-save-btn,
|
||||
.intro-cancel-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background-color: #3b6ea8;
|
||||
color: #f5f6f7;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.intro-edit-btn:hover,
|
||||
.intro-save-btn:hover,
|
||||
.intro-cancel-btn:hover {
|
||||
background-color: #2f5687;
|
||||
}
|
||||
|
||||
.theme-light .intro-edit-btn,
|
||||
.theme-light .intro-save-btn,
|
||||
.theme-light .intro-cancel-btn {
|
||||
background-color: #ffc107;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.theme-light .intro-edit-btn:hover,
|
||||
.theme-light .intro-save-btn:hover,
|
||||
.theme-light .intro-cancel-btn:hover {
|
||||
background-color: #e0a806;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background-color: orangered;
|
||||
color: white;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background-color: #be3300;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
59
src/pages/accountPages/Account_userInfo.vue
Normal file
59
src/pages/accountPages/Account_userInfo.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import store from "../../store/index.js";
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="user-info-card">
|
||||
<div style="position: relative;">
|
||||
<img :src="store.getters.profileImage" alt="User Avatar" class="avatar"/>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<h3>{{ store.state.userInfo.username }}</h3>
|
||||
<p>注册日期:{{ store.state.userInfo.birth?.split(' ')[0] || '未知' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-info-card {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.user-info-card h3 button {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.theme-light .user-info-card h3 {
|
||||
color: #252525;
|
||||
}
|
||||
|
||||
.user-info-card p {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 20px;
|
||||
transition: transform 2s ease;
|
||||
}
|
||||
|
||||
.avatar:hover {
|
||||
transform: rotate(3600deg);
|
||||
transition: transform 10s ease;
|
||||
}
|
||||
|
||||
</style>
|
103
src/pages/accountPages/Account_worksmanage.vue
Normal file
103
src/pages/accountPages/Account_worksmanage.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import AccountWorkPiece from "../../components/AccountWorkPiece.vue";
|
||||
|
||||
const works = ref([
|
||||
{
|
||||
cover: 'https://img1.baidu.com/it/u=427213910,646438716&fm=253',
|
||||
title: '现代化家猪饲养技术:从饲料到环境的全方位提升',
|
||||
createdTime: '2077-01-01 11:45',
|
||||
lastModifiedTime: '2077-01-02 09:19'
|
||||
},
|
||||
{
|
||||
cover: 'https://img1.baidu.com/it/u=427213910,646438716&fm=253',
|
||||
title: '家猪繁殖管理的艺术:提高生育率与健康水平',
|
||||
createdTime: '2077-01-01 11:45',
|
||||
lastModifiedTime: '2077-01-02 09:19'
|
||||
}
|
||||
])
|
||||
|
||||
function createNewBlog() {
|
||||
console.log('新建博客')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="top-bar">
|
||||
<button class="create-btn" @click="createNewBlog">新建博客</button>
|
||||
</div>
|
||||
<div class="works-list">
|
||||
<AccountWorkPiece
|
||||
v-for="(item, index) in works"
|
||||
:key="index"
|
||||
:cover="item.cover"
|
||||
:title="item.title"
|
||||
:createdTime="item.createdTime"
|
||||
:lastModifiedTime="item.lastModifiedTime"
|
||||
:isDraft="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 容器基础样式 */
|
||||
.container {
|
||||
width: 100%;
|
||||
overflow-y: auto; /* 或 scroll */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #f5f6f7;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
:deep(.theme-light) .container {
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #3b6ea8;
|
||||
color: #f5f6f7;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.create-btn:hover {
|
||||
background-color: #2f5687;
|
||||
}
|
||||
|
||||
.theme-light .create-btn {
|
||||
background-color: #ffc107;
|
||||
color: #333333;
|
||||
}
|
||||
.theme-light .create-btn:hover {
|
||||
background-color: #e0a806;
|
||||
}
|
||||
|
||||
.works-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
25
src/plugins/vuexLocalStorage.js
Normal file
25
src/plugins/vuexLocalStorage.js
Normal file
@ -0,0 +1,25 @@
|
||||
export default function createPersistedStatePlugin(options = {}) {
|
||||
const storageKey = options.key || 'vuex-partial-state'
|
||||
const whitelist = options.whitelist || [] // 需要存储的 state key 列表
|
||||
|
||||
return store => {
|
||||
// 1. 在 store 初始化时恢复数据
|
||||
const savedState = JSON.parse(localStorage.getItem(storageKey) || '{}')
|
||||
Object.keys(savedState).forEach(key => {
|
||||
if (whitelist.includes(key)) {
|
||||
store.state[key] = savedState[key]
|
||||
}
|
||||
})
|
||||
|
||||
// 2. 监听 mutation,每次变更后存入 localStorage
|
||||
store.subscribe((mutation, state) => {
|
||||
const partialState = {}
|
||||
whitelist.forEach(key => {
|
||||
if (state[key] !== undefined) {
|
||||
partialState[key] = state[key]
|
||||
}
|
||||
})
|
||||
localStorage.setItem(storageKey, JSON.stringify(partialState))
|
||||
})
|
||||
}
|
||||
}
|
78
src/router/index.js
Normal file
78
src/router/index.js
Normal file
@ -0,0 +1,78 @@
|
||||
import {createRouter, createWebHistory} from 'vue-router';
|
||||
import store from '../store';
|
||||
import AuthService from "../../services/auth.js";
|
||||
import Home from '../pages/Home.vue';
|
||||
import Login from "../pages/Login.vue";
|
||||
import Blog_home from "../pages/Blog_home.vue";
|
||||
import Account from "../pages/accountPages/Account.vue";
|
||||
import Account_selfpage from "../pages/accountPages/Account_selfpage.vue";
|
||||
import Account_worksmanage from "../pages/accountPages/Account_worksmanage.vue";
|
||||
import Account_setting from "../pages/accountPages/Account_setting.vue";
|
||||
import Account_draft from "../pages/accountPages/Account_draft.vue";
|
||||
import Account_userInfo from "../pages/accountPages/Account_userInfo.vue";
|
||||
import Account_admin_uploadLog from "../pages/accountPages/Account_admin_uploadLog.vue";
|
||||
import Projects from "../pages/Projects_home.vue";
|
||||
import Tools_home from "../pages/Tools_home.vue";
|
||||
import About from "../pages/About.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
}, {
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login
|
||||
}, {
|
||||
path: '/blog',
|
||||
name: 'Blog',
|
||||
component: Blog_home
|
||||
}, {
|
||||
path: '/projects',
|
||||
name: 'Projects',
|
||||
component: Projects
|
||||
}, {
|
||||
path: '/tools',
|
||||
name: 'Tools',
|
||||
component: Tools_home
|
||||
}, {
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: About
|
||||
}, {
|
||||
path: '/account',
|
||||
component: Account,
|
||||
children: [
|
||||
{path: 'self-page', component: Account_selfpage},
|
||||
{path: 'works-manage', component: Account_worksmanage},
|
||||
{path: 'draft', component: Account_draft},
|
||||
{path: 'setting', component: Account_setting},
|
||||
{path: '', component: Account_userInfo},
|
||||
{path: 'upload-log', component: Account_admin_uploadLog}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(), // 使用HTML5历史模式
|
||||
routes
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (!store.state.userInfo.uid && store.state.token) {
|
||||
AuthService.setSelfInfo();
|
||||
}
|
||||
if (to.path === '/login' && store.state.userInfo.uid) {
|
||||
next('/account');
|
||||
}
|
||||
if (to.path === '/account' && !store.state.userInfo.uid) {
|
||||
next('/login');
|
||||
}
|
||||
if (to.path === '/account/upload-log' && store.state.userInfo.role_id < 1) {
|
||||
next('/account')
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
53
src/store/index.js
Normal file
53
src/store/index.js
Normal file
@ -0,0 +1,53 @@
|
||||
import { createStore } from 'vuex';
|
||||
import createPersistedStatePlugin from '../plugins/vuexLocalStorage';
|
||||
import api from "../utils/axios.js";
|
||||
import {getDomain} from "../utils/getDomain.js";
|
||||
|
||||
const store = createStore({
|
||||
state: {
|
||||
theme: localStorage.getItem('theme') || 'dark',
|
||||
loading: {},
|
||||
token: null,
|
||||
userInfo: {}
|
||||
},
|
||||
mutations: {
|
||||
toggleTheme(state) {
|
||||
state.theme = state.theme === 'dark' ? 'light' : 'dark'
|
||||
},
|
||||
setToken(state, token) {
|
||||
state.token = token;
|
||||
},
|
||||
setUserInfo(state, obj) {
|
||||
if (obj === {} || !obj) {
|
||||
state.userInfo = {};
|
||||
}
|
||||
state.userInfo = {...state.userInfo, ...obj};
|
||||
},
|
||||
startLoading(state, text) {
|
||||
state.loading.isLoading = {isLoading: true, text: '请稍后...'};
|
||||
if (text) {
|
||||
state.loading.text = text;
|
||||
}
|
||||
},
|
||||
stopLoading(state) {
|
||||
state.loading.isLoading = false;
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
currentTheme: state => state.theme,
|
||||
hasUserInfo: state => !!state.userInfo.uid,
|
||||
profileImage: state => {
|
||||
if (state.userInfo.profile) {
|
||||
return `https://${getDomain()}/data/user/profile/` + state.userInfo.profile;
|
||||
}else {
|
||||
return `https://${getDomain()}/data/user/profile/default.jpg`
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [createPersistedStatePlugin({
|
||||
key: 'cyberStorage',
|
||||
whitelist: ['theme', 'userInfo'],
|
||||
})]
|
||||
})
|
||||
|
||||
export default store;
|
106
src/style.css
Normal file
106
src/style.css
Normal file
@ -0,0 +1,106 @@
|
||||
/* 全局样式:默认夜间模式 */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: rgb(20, 20, 20); /* 夜间背景 */
|
||||
color: #0ff; /* 夜间文字颜色 */
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 白天模式 */
|
||||
body.theme-light {
|
||||
background-color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 全局链接样式 */
|
||||
a {
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
body:not(.theme-light) a {
|
||||
color: #0ff;
|
||||
}
|
||||
body.theme-light a {
|
||||
color: #232323;
|
||||
}
|
||||
a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 通用容器 */
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 按钮基础样式 */
|
||||
button {
|
||||
font-family: inherit;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 8px solid #f3f3f3; /* 背景色 */
|
||||
border-top: 8px solid #3498db; /* 动画颜色 */
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/*::-webkit-scrollbar {*/
|
||||
/* display: none;*/
|
||||
/*}*/
|
||||
|
||||
/*滚动条 */
|
||||
|
||||
/* 深色模式:默认 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #444;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #222;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 亮色模式 */
|
||||
.theme-light ::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.theme-light ::-webkit-scrollbar-track {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* 选中的滚动条滑块效果 */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #888;
|
||||
}
|
||||
|
||||
.theme-light ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #aaa;
|
||||
}
|
31
src/utils/axios.js
Normal file
31
src/utils/axios.js
Normal file
@ -0,0 +1,31 @@
|
||||
import axios from 'axios';
|
||||
import store from '../store';
|
||||
import {getDomain} from "./getDomain.js";
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: `https://${getDomain()}:5001`,
|
||||
timeout: 6000,
|
||||
});
|
||||
|
||||
// 请求拦截器:自动加Token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = store.state.token;
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// 响应拦截器:处理错误 & 统一返回数据格式
|
||||
api.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => {
|
||||
console.error('请求出错:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default api;
|
3
src/utils/getDomain.js
Normal file
3
src/utils/getDomain.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function getDomain() {
|
||||
return window.location.hostname === 'localhost' ? 'mva-cyber.club' : window.location.hostname;
|
||||
}
|
38
src/utils/sweetalert.js
Normal file
38
src/utils/sweetalert.js
Normal file
@ -0,0 +1,38 @@
|
||||
import Swal from 'sweetalert2';
|
||||
|
||||
|
||||
const swalInstantiations = {
|
||||
tip: Swal.mixin({
|
||||
toast: true, // 弹窗类型为 Toast
|
||||
position: 'top', // 弹窗显示位置
|
||||
|
||||
showConfirmButton: false, // 不显示确认按钮
|
||||
timer: 3000, // 弹窗显示3秒后自动关闭
|
||||
didOpen: (toast) => {
|
||||
toast.addEventListener('mouseenter', Swal.stopTimer);
|
||||
toast.addEventListener('mouseleave', Swal.resumeTimer);
|
||||
}
|
||||
}),
|
||||
window: Swal.mixin({
|
||||
title: '确认?',
|
||||
text: '您确定吗?',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
didOpen: (toast) => {
|
||||
toast.addEventListener('mouseenter', Swal.stopTimer);
|
||||
toast.addEventListener('mouseleave', Swal.resumeTimer);
|
||||
}
|
||||
})
|
||||
};
|
||||
const swal = {
|
||||
tip: (icon, title, text, position) => {
|
||||
swalInstantiations.tip.fire({icon: icon, title: title, text: text})
|
||||
},
|
||||
window: (icon, title, text, confirm, cancel) => {
|
||||
return swalInstantiations.window.fire({icon: icon, title: title, text: text, confirmButtonText: confirm, cancelButtonText: cancel})
|
||||
}
|
||||
}
|
||||
|
||||
export default swal
|
146
test.html
Normal file
146
test.html
Normal file
@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GSAP 多元素滚动触发 + 视差效果</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.parallax-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 120vh;
|
||||
background: url('https://bj.bcebos.com/bjh-pixel/16973229531047913132_0_ainote_new.jpg') center/cover;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 100px;
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.box1 {
|
||||
background-color: #3498db;
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
|
||||
.box2 {
|
||||
background-color: #e74c3c;
|
||||
transform: translateX(200px);
|
||||
}
|
||||
|
||||
.box3 {
|
||||
background-color: #2ecc71;
|
||||
transform: translateY(100px) scale(0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="parallax-bg"></div>
|
||||
|
||||
<div class="spacer">⬇️ 向下滚动看看动画效果 ⬇️</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="box box1">1</div>
|
||||
<div class="box box2">2</div>
|
||||
<div class="box box3">3</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer">🎉 滚动结束 🎉</div>
|
||||
|
||||
<script>
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// 让 box1 从左滑入
|
||||
gsap.to(".box1", {
|
||||
x: 0, // 从 -200px 移动到原位
|
||||
opacity: 1,
|
||||
duration: 1.5, // 动画时长 1.5s
|
||||
ease: "power2.out",
|
||||
scrollTrigger: {
|
||||
trigger: ".box1",
|
||||
start: "top 80%", // 当 box1 滚动到视口 80% 处时触发
|
||||
end: "top 50%",
|
||||
scrub: true // 让动画随滚动平滑执行
|
||||
}
|
||||
});
|
||||
|
||||
// 让 box2 从右滑入
|
||||
gsap.to(".box2", {
|
||||
x: 0, // 从 200px 移动到原位
|
||||
opacity: 1,
|
||||
duration: 1.5,
|
||||
ease: "power2.out",
|
||||
scrollTrigger: {
|
||||
trigger: ".box2",
|
||||
start: "top 85%",
|
||||
end: "top 50%",
|
||||
scrub: 1 // 增加动画延迟,让滚动更顺滑
|
||||
}
|
||||
});
|
||||
|
||||
// 让 box3 向上滑入并放大
|
||||
gsap.to(".box3", {
|
||||
y: 0,
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
duration: 1.5,
|
||||
ease: "elastic.out(1, 0.5)", // 弹性效果
|
||||
scrollTrigger: {
|
||||
trigger: ".box3",
|
||||
start: "top 90%",
|
||||
end: "top 40%",
|
||||
scrub: 2 // 让动画更随滚动渐变
|
||||
}
|
||||
});
|
||||
|
||||
// 背景视差滚动
|
||||
gsap.to(".parallax-bg", {
|
||||
y: -100, // 让背景慢速上移
|
||||
scrollTrigger: {
|
||||
trigger: "body",
|
||||
start: "top top",
|
||||
end: "bottom top",
|
||||
scrub: 3 // 让背景滚动缓慢
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
7
vite.config.js
Normal file
7
vite.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user