first commit
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
29
README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# big-event
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
8
jsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
4147
package-lock.json
generated
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "big-event",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"axios": "^1.7.9",
|
||||
"element-plus": "^2.9.3",
|
||||
"pinia": "^2.3.0",
|
||||
"pinia-persistedstate-plugin": "^0.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"sass": "^1.83.4",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-vue-devtools": "^7.6.8"
|
||||
}
|
||||
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
13
src/App.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
31
src/api/article.js
Normal file
@ -0,0 +1,31 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const articleCategoryListService = () => {
|
||||
/*const tokenStore = useTokenStore()
|
||||
return request.get('/category', {
|
||||
headers: {
|
||||
Authorization: tokenStore.token //pinia里面定义的响应式数据不需要.value
|
||||
}
|
||||
})*/
|
||||
return request.get('/category')
|
||||
}
|
||||
|
||||
export const addCategoryService = (categoryData) => {
|
||||
return request.post('/category', categoryData)
|
||||
}
|
||||
|
||||
export const updateCategoryService = (categoryData) => {
|
||||
return request.put(`/category`, categoryData)
|
||||
}
|
||||
|
||||
export const deleteCategoryService = (id) => {
|
||||
return request.delete(`/category?id=`+id)
|
||||
}
|
||||
|
||||
export const articleListService = (params) => {
|
||||
return request.get('/article', {params: params})
|
||||
}
|
||||
|
||||
export const articleAddService = (articleData) => {
|
||||
return request.post('/article',articleData)
|
||||
}
|
48
src/api/user.js
Normal file
@ -0,0 +1,48 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
//注册接口
|
||||
export const userRegisterService = (registerData)=>{
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for(let key in registerData){
|
||||
params.append(key,registerData[key]);
|
||||
}
|
||||
|
||||
return request.post('/user/register',params);
|
||||
}
|
||||
|
||||
//登录接口
|
||||
export const userLoginService = (loginData)=>{
|
||||
const params = new URLSearchParams();
|
||||
for(let key in loginData){
|
||||
params.append(key,loginData[key]);
|
||||
}
|
||||
|
||||
return request.post('/user/login',params);
|
||||
}
|
||||
|
||||
//获取用户详细信息
|
||||
export const userInfoService = ()=>{
|
||||
return request.get('/user/userinfo')
|
||||
}
|
||||
|
||||
//修改个人信息
|
||||
export const userInfoUpdateService = (userInfoData)=>{
|
||||
return request.put('/user/update',userInfoData)
|
||||
}
|
||||
|
||||
//修改头像
|
||||
export const userAvatarUpdateService = (avatarUrl)=>{
|
||||
const params = new URLSearchParams();
|
||||
params.append('avatarUrl',avatarUrl)
|
||||
return request.patch('/user/updateAvatar',params)
|
||||
}
|
||||
|
||||
//修改密码
|
||||
export const userPasswordUpdateService = (passwordData) => {
|
||||
return request.patch('/user/updatePwd', passwordData, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
BIN
src/assets/avatar.jpg
Normal file
After Width: | Height: | Size: 9.7 KiB |
86
src/assets/base.css
Normal file
@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
BIN
src/assets/cover.jpg
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
src/assets/default.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
src/assets/login_bg.jpg
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
src/assets/login_title.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
src/assets/logo.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
1
src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
BIN
src/assets/logo2.png
Normal file
After Width: | Height: | Size: 11 KiB |
35
src/assets/main.css
Normal file
@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
20
src/assets/main.scss
Normal file
@ -0,0 +1,20 @@
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
transform: translateX(-30px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
transform: translateX(30px);
|
||||
opacity: 0;
|
||||
}
|
21
src/main.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia';
|
||||
import {createPersistedState} from'pinia-persistedstate-plugin'
|
||||
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
|
||||
const pinia = createPinia()
|
||||
const persist = createPersistedState()
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
pinia.use(persist)
|
||||
app.use(pinia)
|
||||
app.use(ElementPlus,{locale:zhCn})
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
36
src/router/index.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import LoginView from '@/views/LoginView.vue'
|
||||
import LayoutView from '@/views/LayoutView.vue';
|
||||
|
||||
import ArticleCategoryVue from '@/views/article/ArticleCategory.vue'
|
||||
import ArticleManageVue from '@/views/article/ArticleManage.vue'
|
||||
import UserAvatarVue from '@/views/user/UserAvatar.vue'
|
||||
import UserInfoVue from '@/views/user/UserInfo.vue'
|
||||
import UserResetPasswordVue from '@/views/user/UserResetPassword.vue'
|
||||
|
||||
|
||||
//define the routing table
|
||||
const routes = [
|
||||
{path: '/login', component: LoginView},
|
||||
{
|
||||
path: '/',
|
||||
component: LayoutView,
|
||||
redirect: '/article/category',
|
||||
children:[
|
||||
{ path: '/article/category', component: ArticleCategoryVue },
|
||||
{ path: '/article/manage', component: ArticleManageVue },
|
||||
{ path: '/user/info', component: UserInfoVue },
|
||||
{ path: '/user/avatar', component: UserAvatarVue },
|
||||
{ path: '/user/resetPassword', component: UserResetPasswordVue }
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: routes,
|
||||
})
|
||||
|
||||
//export router
|
||||
export default router
|
31
src/stores/token.js
Normal file
@ -0,0 +1,31 @@
|
||||
//定义store
|
||||
import {defineStore} from 'pinia'
|
||||
import {ref} from 'vue'
|
||||
/*
|
||||
第一个参数:名字,唯一性
|
||||
第二个参数:函数,函数的内部可以定义状态的所有内容
|
||||
|
||||
返回值: 函数
|
||||
*/
|
||||
export const useTokenStore = defineStore('token',()=>{
|
||||
//定义状态的内容
|
||||
|
||||
//1.响应式变量
|
||||
const token = ref('')
|
||||
|
||||
//2.定义一个函数,修改token的值
|
||||
const setToken = (newToken)=>{
|
||||
token.value = newToken
|
||||
}
|
||||
|
||||
//3.函数,移除token的值
|
||||
const removeToken = ()=>{
|
||||
token.value=''
|
||||
}
|
||||
|
||||
return {
|
||||
token,setToken,removeToken
|
||||
}
|
||||
},{
|
||||
persist:true//持久化存储
|
||||
});
|
21
src/stores/userInfo.js
Normal file
@ -0,0 +1,21 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {ref} from 'vue'
|
||||
const useUserInfoStore = defineStore('userInfo',()=>{
|
||||
//定义状态相关的内容
|
||||
|
||||
const info = ref({})
|
||||
|
||||
const setInfo = (newInfo)=>{
|
||||
info.value = newInfo
|
||||
}
|
||||
|
||||
|
||||
const removeInfo = ()=>{
|
||||
info.value = {}
|
||||
}
|
||||
|
||||
return {info,setInfo,removeInfo}
|
||||
|
||||
},{persist:true})
|
||||
|
||||
export default useUserInfoStore;
|
53
src/utils/request.js
Normal file
@ -0,0 +1,53 @@
|
||||
//定制请求的实例
|
||||
|
||||
//导入axios npm install axios
|
||||
import axios from 'axios';
|
||||
import { useTokenStore } from '@/stores/token';
|
||||
import { ElMessage } from 'element-plus';
|
||||
//定义一个变量,记录公共的前缀 , baseURL
|
||||
const baseURL = '/api';
|
||||
const instance = axios.create({baseURL})
|
||||
|
||||
//添加请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config)=>{
|
||||
//获取token
|
||||
const tokenStore = useTokenStore();
|
||||
//给请求头添加token
|
||||
if(tokenStore.token){
|
||||
config.headers.Authorization = tokenStore.token;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
err=>{
|
||||
//请求错误使用把异步的状态转化成失败的状态
|
||||
return Promise.reject(err);
|
||||
}
|
||||
)
|
||||
|
||||
import router from '@/router';
|
||||
|
||||
//添加响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
result=>{
|
||||
if (result.data.code === 0){
|
||||
return result.data;
|
||||
} else{
|
||||
ElMessage.error(result.data.message?result.data.message:'服务异常');
|
||||
return Promise.reject(result.data);
|
||||
}
|
||||
},
|
||||
err=>{
|
||||
//未登录,提示,并且跳转登录页面
|
||||
if (err.response.status === 401){
|
||||
ElMessage.info('请先登录');
|
||||
router.push('/login');
|
||||
}else{
|
||||
ElMessage.error('服务异常');
|
||||
}
|
||||
|
||||
return Promise.reject(err);//异步的状态转化成失败的状态
|
||||
}
|
||||
)
|
||||
|
||||
export default instance;
|
290
src/views/LayoutView.vue
Normal file
@ -0,0 +1,290 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import {
|
||||
Management,
|
||||
Promotion,
|
||||
UserFilled,
|
||||
User,
|
||||
Crop,
|
||||
EditPen,
|
||||
SwitchButton,
|
||||
CaretBottom
|
||||
} from '@element-plus/icons-vue';
|
||||
import avatar from '@/assets/default.png';
|
||||
import { userInfoService } from '@/api/user.js';
|
||||
import useUserInfoStore from '@/stores/userInfo.js';
|
||||
import { useTokenStore } from '@/stores/token.js';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const tokenStore = useTokenStore();
|
||||
const userInfoStore = useUserInfoStore();
|
||||
const router = useRouter();
|
||||
const isPortrait = ref(false);
|
||||
|
||||
const getUserInfo = async () => {
|
||||
let result = await userInfoService();
|
||||
userInfoStore.setInfo(result.data);
|
||||
};
|
||||
|
||||
const handleCommand = (command) => {
|
||||
if (command === 'logout') {
|
||||
ElMessageBox.confirm(
|
||||
'您确认要退出吗?',
|
||||
'温馨提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
tokenStore.removeToken();
|
||||
userInfoStore.removeInfo();
|
||||
router.push('/login');
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '退出登录成功',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '用户取消了退出登录',
|
||||
});
|
||||
});
|
||||
} else {
|
||||
router.push('/user/' + command);
|
||||
}
|
||||
};
|
||||
|
||||
const checkIsPortrait = () => {
|
||||
isPortrait.value = window.innerWidth / window.innerHeight < 1;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkIsPortrait();
|
||||
window.addEventListener('resize', checkIsPortrait);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkIsPortrait);
|
||||
});
|
||||
|
||||
getUserInfo();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<!-- 左侧菜单(宽高比大于1时显示) -->
|
||||
<el-aside v-if="!isPortrait" width="200px">
|
||||
<div class="el-aside__logo"></div>
|
||||
<el-menu active-text-color="#ffd04b" background-color="#232323" text-color="#fff" router>
|
||||
<el-menu-item index="/article/category">
|
||||
<el-icon>
|
||||
<Management />
|
||||
</el-icon>
|
||||
<span>文章分类</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/article/manage">
|
||||
<el-icon>
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<span>文章管理</span>
|
||||
</el-menu-item>
|
||||
<el-sub-menu>
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<UserFilled />
|
||||
</el-icon>
|
||||
<span>个人中心</span>
|
||||
</template>
|
||||
<el-menu-item index="/user/info">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>基本资料</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/user/avatar">
|
||||
<el-icon>
|
||||
<Crop />
|
||||
</el-icon>
|
||||
<span>更换头像</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/user/resetPassword">
|
||||
<el-icon>
|
||||
<EditPen />
|
||||
</el-icon>
|
||||
<span>重置密码</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<!-- 顶部菜单(宽高比小于1时显示) -->
|
||||
<el-header v-if="isPortrait" class="mobile-header">
|
||||
<el-menu mode="horizontal" active-text-color="#ffd04b" background-color="#232323" text-color="#fff" router>
|
||||
<el-menu-item index="/article/category">
|
||||
<el-icon>
|
||||
<Management />
|
||||
</el-icon>
|
||||
<span>文章分类</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/article/manage">
|
||||
<el-icon>
|
||||
<Promotion />
|
||||
</el-icon>
|
||||
<span>文章管理</span>
|
||||
</el-menu-item>
|
||||
<el-sub-menu>
|
||||
<template #title>
|
||||
<el-icon>
|
||||
<UserFilled />
|
||||
</el-icon>
|
||||
<span>个人中心</span>
|
||||
</template>
|
||||
<el-menu-item index="/user/info">
|
||||
<el-icon>
|
||||
<User />
|
||||
</el-icon>
|
||||
<span>基本资料</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/user/avatar">
|
||||
<el-icon>
|
||||
<Crop />
|
||||
</el-icon>
|
||||
<span>更换头像</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/user/resetPassword">
|
||||
<el-icon>
|
||||
<EditPen />
|
||||
</el-icon>
|
||||
<span>重置密码</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
<el-dropdown placement="bottom-end" @command="handleCommand">
|
||||
<span class="el-dropdown__box">
|
||||
<el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" />
|
||||
<el-icon>
|
||||
<CaretBottom />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="info" :icon="User">基本资料</el-dropdown-item>
|
||||
<el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
|
||||
<el-dropdown-item command="resetPassword" :icon="EditPen">重置密码</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-header>
|
||||
<!-- 右侧主区域 -->
|
||||
<el-container>
|
||||
<!-- 头部区域 -->
|
||||
<el-header v-if="!isPortrait">
|
||||
<div>程序员:<strong>{{ userInfoStore.info.nickname }}</strong></div>
|
||||
<el-dropdown placement="bottom-end" @command="handleCommand">
|
||||
<span class="el-dropdown__box">
|
||||
<el-avatar :src="userInfoStore.info.userPic ? userInfoStore.info.userPic : avatar" />
|
||||
<el-icon>
|
||||
<CaretBottom />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="info" :icon="User">基本资料</el-dropdown-item>
|
||||
<el-dropdown-item command="avatar" :icon="Crop">更换头像</el-dropdown-item>
|
||||
<el-dropdown-item command="resetPassword" :icon="EditPen">重置密码</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" :icon="SwitchButton">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-header>
|
||||
<!-- 中间区域 -->
|
||||
<el-main>
|
||||
<router-view></router-view>
|
||||
</el-main>
|
||||
<!-- 底部区域 -->
|
||||
<el-footer>大事件 ©2025 Created by xclele</el-footer>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
|
||||
.el-aside {
|
||||
background-color: #232323;
|
||||
|
||||
&__logo {
|
||||
height: 120px;
|
||||
background: url('@/assets/logo.png') no-repeat center / 120px auto;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-header {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.el-dropdown__box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-icon {
|
||||
color: #999;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media (orientation: portrait) {
|
||||
.el-aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
background-color: #232323;
|
||||
.el-menu {
|
||||
width: 100%;
|
||||
}
|
||||
.el-dropdown__box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.el-icon {
|
||||
color: #999;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
223
src/views/LoginView.vue
Normal file
@ -0,0 +1,223 @@
|
||||
<script setup>
|
||||
import { User, Lock } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
//控制注册与登录表单的显示, 默认显示注册
|
||||
const isRegister = ref(false)
|
||||
//定义数据模型
|
||||
const registerData = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
rePassword: ''
|
||||
})
|
||||
|
||||
const loginData = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
//repassword校验规则
|
||||
const checkRePassword = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次确认密码'))
|
||||
} else if (value !== registerData.value.password) {
|
||||
callback(new Error('两次输入密码不一致!'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
//定义表单校验规则
|
||||
const rules = {
|
||||
username:[
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 5, max: 16, message: '长度在 5 到 16 个字符', trigger: 'blur' }
|
||||
],
|
||||
password:[
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 5, max: 16, message: '长度在 5 到 16 个字符', trigger: 'blur' }
|
||||
],
|
||||
rePassword:[
|
||||
{validator:checkRePassword, trigger: 'blur'}
|
||||
]
|
||||
}
|
||||
|
||||
//调用后台接口注册
|
||||
import { userRegisterService,userLoginService } from '@/api/user.js'
|
||||
const register = async() => {
|
||||
//registerdata是响应式对象,所以要用registerData.value
|
||||
|
||||
let result = await userRegisterService(registerData.value)
|
||||
if (result.code === 0){
|
||||
//注册成功
|
||||
ElMessage.success(result.msg? result.msg:'注册成功')
|
||||
}else{
|
||||
//注册失败
|
||||
ElMessage.error('注册失败')
|
||||
}
|
||||
}
|
||||
|
||||
//调用后台接口登录
|
||||
import {useRouter} from 'vue-router';
|
||||
import { useTokenStore } from '@/stores/token.js';
|
||||
const router = useRouter();
|
||||
const tokenStore = useTokenStore();
|
||||
|
||||
const login = async() => {
|
||||
//loginData是响应式对象,所以要用loginData.value
|
||||
let result = await userLoginService(loginData.value)
|
||||
if (result.code === 0){
|
||||
//登录成功
|
||||
ElMessage.success(result.msg? result.msg:'登录成功')
|
||||
//存储token
|
||||
tokenStore.setToken(result.data)
|
||||
router.push('/')
|
||||
}else{
|
||||
//登录失败
|
||||
ElMessage.error('登录失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//根据宽高比判断是否为移动端
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const isPortrait = ref(false);
|
||||
|
||||
const checkIsPortrait = () => {
|
||||
isPortrait.value = window.innerWidth / window.innerHeight < 1;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkIsPortrait();
|
||||
window.addEventListener('resize', checkIsPortrait);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkIsPortrait);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-row class="login-page">
|
||||
<el-col :span="12" class="bg" v-if="!isPortrait"></el-col>
|
||||
<el-col :span="isPortrait ? 24 :12" :offset="0" class="form">
|
||||
<!-- 注册表单 -->
|
||||
<el-form ref="form" size="medium" autocomplete="off" v-if="isRegister" :model="registerData" :rules="rules">
|
||||
<el-form-item>
|
||||
<h1>注册</h1>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username">
|
||||
<el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="rePassword">
|
||||
<el-input :prefix-icon="Lock" type="password" placeholder="请输入再次密码" v-model="registerData.rePassword"></el-input>
|
||||
</el-form-item>
|
||||
<!-- 注册按钮 -->
|
||||
<el-form-item>
|
||||
<el-button class="button" type="primary" auto-insert-space @click="register">
|
||||
注册
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item class="flex">
|
||||
<el-link type="info" :underline="false" @click="isRegister = false">
|
||||
← 返回
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 登录表单 -->
|
||||
<el-form ref="form" size="large" autocomplete="off" v-else :model="loginData" :rules="rules" @keyup.enter="login"> <!--复用登陆表单-->
|
||||
<el-form-item>
|
||||
<h1>登录</h1>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username">
|
||||
<el-input :prefix-icon="User" placeholder="请输入用户名" v-model="loginData.username"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="loginData.password"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item class="flex">
|
||||
<div class="flex">
|
||||
<el-checkbox>记住我</el-checkbox>
|
||||
<el-link type="primary" :underline="false">忘记密码?</el-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<!-- 登录按钮 -->
|
||||
<el-form-item>
|
||||
<el-button class="button" type="primary" auto-insert-space @click="login">登录</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item class="flex">
|
||||
<el-link type="info" :underline="false" @click="isRegister = true">
|
||||
注册 →
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 样式 */
|
||||
@media (orientation: Landscape){
|
||||
.login-page {
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
|
||||
.bg {
|
||||
background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
|
||||
url('@/assets/login_bg.jpg') no-repeat center / cover;
|
||||
border-radius: 0 20px 20px 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
padding: calc(10%) ;
|
||||
|
||||
.title {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: portrait) {
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
|
||||
.title {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
189
src/views/article/ArticleCategory.vue
Normal file
@ -0,0 +1,189 @@
|
||||
<script setup>
|
||||
import {
|
||||
Edit,
|
||||
Delete
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
const categories = ref([
|
||||
{
|
||||
"id": 3,
|
||||
"categoryName": "美食",
|
||||
"categoryAlias": "my",
|
||||
"createTime": "2023-09-02 12:06:59",
|
||||
"updateTime": "2023-09-02 12:06:59"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"categoryName": "娱乐",
|
||||
"categoryAlias": "yl",
|
||||
"createTime": "2023-09-02 12:08:16",
|
||||
"updateTime": "2023-09-02 12:08:16"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"categoryName": "军事",
|
||||
"categoryAlias": "js",
|
||||
"createTime": "2023-09-02 12:08:33",
|
||||
"updateTime": "2023-09-02 12:08:33"
|
||||
}
|
||||
])
|
||||
|
||||
//控制添加分类弹窗
|
||||
const dialogVisible = ref(false)
|
||||
//控制删除确认弹窗
|
||||
const deleteDialogVisible = ref(false)
|
||||
//弹窗title
|
||||
const title=ref('')
|
||||
|
||||
//显示编辑弹窗
|
||||
const showEditDialog = (row) => {
|
||||
dialogVisible.value = true;
|
||||
title.value = '编辑分类';
|
||||
//数据拷贝
|
||||
categoryModel.value.categoryName = row.categoryName;
|
||||
categoryModel.value.categoryAlias = row.categoryAlias;
|
||||
//扩展id属性
|
||||
categoryModel.value.id = row.id;
|
||||
}
|
||||
|
||||
//显示删除确认弹窗
|
||||
const showDeleteDialog = (row) => {
|
||||
deleteDialogVisible.value = true;
|
||||
//数据拷贝
|
||||
categoryModel.value.categoryName = row.categoryName;
|
||||
categoryModel.value.categoryAlias = row.categoryAlias;
|
||||
//读取id属性
|
||||
categoryModel.value.id = row.id;
|
||||
}
|
||||
|
||||
//添加分类数据模型
|
||||
const categoryModel = ref({
|
||||
categoryName: '',
|
||||
categoryAlias: ''
|
||||
})
|
||||
//添加分类表单校验
|
||||
const rules = {
|
||||
categoryName: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
],
|
||||
categoryAlias: [
|
||||
{ required: true, message: '请输入分类别名', trigger: 'blur' },
|
||||
]
|
||||
}
|
||||
|
||||
//按照分类获取文章列表
|
||||
import { articleCategoryListService,addCategoryService,updateCategoryService,deleteCategoryService } from '@/api/article.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const articleCategoryList = async() => {
|
||||
let result = await articleCategoryListService();
|
||||
categories.value = result.data;
|
||||
}
|
||||
articleCategoryList(); //调用函数
|
||||
|
||||
//添加分类
|
||||
const addCategory = async() => {
|
||||
//调用接口添加分类
|
||||
let result = await addCategoryService(categoryModel.value);
|
||||
ElMessage.success(result.message ? result.message : '添加成功');
|
||||
|
||||
//刷新列表并关闭弹窗
|
||||
articleCategoryList();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
//编辑分类
|
||||
const updateCategory = async() => {
|
||||
//调用接口编辑分类
|
||||
let result = await updateCategoryService(categoryModel.value);
|
||||
ElMessage.success(result.message ? result.message : '修改成功');
|
||||
|
||||
//刷新列表并关闭弹窗
|
||||
articleCategoryList();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
//删除分类
|
||||
const deleteCategory = async() => {
|
||||
//调用接口删除分类
|
||||
console.log(categoryModel.value.id)
|
||||
let result = await deleteCategoryService(categoryModel.value.id);
|
||||
ElMessage.success(result.message ? result.message : '删除成功');
|
||||
|
||||
//刷新列表
|
||||
articleCategoryList();
|
||||
deleteDialogVisible.value = false;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<el-card class="page-container">
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<span>文章分类</span>
|
||||
<div class="extra">
|
||||
<el-button type="primary" @click="title='添加分类';dialogVisible = true">添加分类</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="categories" style="width: 100%">
|
||||
<el-table-column label="序号" width="100" type="index"> </el-table-column>
|
||||
<el-table-column label="分类名称" prop="categoryName"></el-table-column>
|
||||
<el-table-column label="分类别名" prop="categoryAlias"></el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button :icon="Edit" circle plain type="primary" @click="showEditDialog(row)"></el-button>
|
||||
<el-button :icon="Delete" circle plain type="danger" @click="showDeleteDialog(row)"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="没有数据" />
|
||||
</template>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加,编辑分类弹窗 -->
|
||||
<el-dialog v-model="dialogVisible" :title="title" width="400">
|
||||
<el-form :model="categoryModel" :rules="rules" label-width="100px" style="padding-right: 30px">
|
||||
<el-form-item label="分类名称" prop="categoryName">
|
||||
<el-input v-model="categoryModel.categoryName" minlength="1" maxlength="10"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类别名" prop="categoryAlias">
|
||||
<el-input v-model="categoryModel.categoryAlias" minlength="1" maxlength="15"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="title==='添加分类' ? addCategory() : updateCategory()"> 确认 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!--删除确认弹窗-->
|
||||
<el-dialog title="提示" v-model="deleteDialogVisible" width="400">
|
||||
<span>确认删除该分类吗?</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="deleteDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="deleteCategory()"> 确认 </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
294
src/views/article/ArticleManage.vue
Normal file
@ -0,0 +1,294 @@
|
||||
<script setup>
|
||||
import {
|
||||
Edit,
|
||||
Delete
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
//文章分类数据模型
|
||||
const categories = ref([
|
||||
{
|
||||
"id": 1,
|
||||
"categoryName": "dawdwa",
|
||||
"categoryAlias": "adw",
|
||||
"createTime": "2023-09-02 12:06:59",
|
||||
"updateTime": "2023-09-02 12:06:59"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"categoryName": "美食",
|
||||
"categoryAlias": "my",
|
||||
"createTime": "2023-09-02 12:06:59",
|
||||
"updateTime": "2023-09-02 12:06:59"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"categoryName": "娱乐",
|
||||
"categoryAlias": "yl",
|
||||
"createTime": "2023-09-02 12:08:16",
|
||||
"updateTime": "2023-09-02 12:08:16"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"categoryName": "军事",
|
||||
"categoryAlias": "js",
|
||||
"createTime": "2023-09-02 12:08:33",
|
||||
"updateTime": "2023-09-02 12:08:33"
|
||||
}
|
||||
])
|
||||
|
||||
//用户搜索时选中的分类id
|
||||
const categoryId=ref('')
|
||||
|
||||
//用户搜索时选中的发布状态
|
||||
const state=ref('')
|
||||
|
||||
//文章列表数据模型
|
||||
const articles = ref([
|
||||
{
|
||||
"id": 5,
|
||||
"title": "陕西旅游攻略",
|
||||
"content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
|
||||
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
|
||||
"state": "草稿",
|
||||
"categoryId": 2,
|
||||
"createTime": "2023-09-03 11:55:30",
|
||||
"updateTime": "2023-09-03 11:55:30"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "陕西旅游攻略",
|
||||
"content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
|
||||
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
|
||||
"state": "草稿",
|
||||
"categoryId": 2,
|
||||
"createTime": "2023-09-03 11:55:30",
|
||||
"updateTime": "2023-09-03 11:55:30"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "陕西旅游攻略",
|
||||
"content": "兵马俑,华清池,法门寺,华山...爱去哪去哪...",
|
||||
"coverImg": "https://big-event-gwd.oss-cn-beijing.aliyuncs.com/9bf1cf5b-1420-4c1b-91ad-e0f4631cbed4.png",
|
||||
"state": "草稿",
|
||||
"categoryId": 2,
|
||||
"createTime": "2023-09-03 11:55:30",
|
||||
"updateTime": "2023-09-03 11:55:30"
|
||||
},
|
||||
])
|
||||
|
||||
//分页条数据模型
|
||||
const pageNum = ref(1)//当前页
|
||||
const total = ref(20)//总条数
|
||||
const pageSize = ref(3)//每页条数
|
||||
|
||||
//当每页条数发生了变化,调用此函数
|
||||
const onSizeChange = (size) => {
|
||||
pageSize.value = size
|
||||
getArticleList()
|
||||
}
|
||||
//当前页码发生变化,调用此函数
|
||||
const onCurrentChange = (num) => {
|
||||
pageNum.value = num
|
||||
getArticleList()
|
||||
}
|
||||
|
||||
//按照分类获取文章列表
|
||||
import { articleCategoryListService } from '@/api/article.js'
|
||||
|
||||
const articleCategoryList = async() => {
|
||||
let result = await articleCategoryListService();
|
||||
categories.value = result.data;
|
||||
}
|
||||
articleCategoryList(); //调用函数
|
||||
|
||||
//从后端获取文章信息
|
||||
import { articleListService } from '@/api/article'
|
||||
const getArticleList = async() => {
|
||||
const params = {
|
||||
pageNum: pageNum.value,
|
||||
pageSize: pageSize.value,
|
||||
categoryId: categoryId.value ?categoryId.value :null,
|
||||
state: state.value ? state.value : null
|
||||
}
|
||||
|
||||
const result = await articleListService(params)
|
||||
|
||||
total.value = result.data.total
|
||||
articles.value = result.data.items
|
||||
}
|
||||
getArticleList() //打开页面的时候获取文章列表
|
||||
|
||||
import { QuillEditor } from '@vueup/vue-quill'
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||
import {Plus} from '@element-plus/icons-vue'
|
||||
//控制抽屉是否显示
|
||||
const visibleDrawer = ref(false)
|
||||
//添加表单数据模型
|
||||
const articleModel = ref({
|
||||
title: '',
|
||||
categoryId: '',
|
||||
coverImg: '',
|
||||
content:'',
|
||||
state:''
|
||||
})
|
||||
|
||||
//添加文章
|
||||
import { articleAddService } from "@/api/article"
|
||||
import { ElMessage } from 'element-plus'
|
||||
const addArticle = async(clickState) => {
|
||||
articleModel.value.state = clickState
|
||||
let result = await articleAddService(articleModel.value)
|
||||
|
||||
ElMessage.success(result.message? result.message: '添加成功')
|
||||
visibleDrawer.value = false
|
||||
getArticleList()
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<el-card class="page-container">
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<span>文章管理</span>
|
||||
<div class="extra">
|
||||
<el-button type="primary" @click="visibleDrawer=true">添加文章</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 搜索表单 -->
|
||||
<el-form inline>
|
||||
<el-form-item label="文章分类:">
|
||||
<el-select placeholder="请选择" v-model="categoryId">
|
||||
<el-option
|
||||
v-for="c in categories"
|
||||
:key="c.id"
|
||||
:label="c.categoryName"
|
||||
:value="c.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发布状态:">
|
||||
<el-select placeholder="请选择" v-model="state">
|
||||
<el-option label="已发布" value="已发布"></el-option>
|
||||
<el-option label="草稿" value="草稿"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getArticleList">搜索</el-button>
|
||||
<el-button @click="categoryId='';state=''">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 文章列表 -->
|
||||
<el-table :data="articles" style="width: 100%">
|
||||
<el-table-column label="文章标题" width="400" prop="title"></el-table-column>
|
||||
<el-table-column label="分类" prop="categoryId"></el-table-column>
|
||||
<el-table-column label="发表时间" prop="createTime"> </el-table-column>
|
||||
<el-table-column label="状态" prop="state"></el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default>
|
||||
<el-button :icon="Edit" circle plain type="primary"></el-button>
|
||||
<el-button :icon="Delete" circle plain type="danger"></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="没有数据" />
|
||||
</template>
|
||||
</el-table>
|
||||
<!-- 分页条 -->
|
||||
<el-pagination v-model:current-page="pageNum" v-model:page-size="pageSize" :page-sizes="[3, 5 ,10, 15]"
|
||||
layout="jumper, total, sizes, prev, pager, next" background :total="total" @size-change="onSizeChange"
|
||||
@current-change="onCurrentChange" style="margin-top: 20px; justify-content: flex-end" />
|
||||
|
||||
<!-- 抽屉 -->
|
||||
<el-drawer v-model="visibleDrawer" title="添加文章" direction="rtl" size="50%">
|
||||
<!-- 添加文章表单 -->
|
||||
<el-form :model="articleModel" label-width="100px" >
|
||||
<el-form-item label="文章标题" >
|
||||
<el-input v-model="articleModel.title" placeholder="请输入标题"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章分类">
|
||||
<el-select placeholder="请选择" v-model="articleModel.categoryId">
|
||||
<el-option v-for="c in categories" :key="c.id" :label="c.categoryName" :value="c.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章封面">
|
||||
|
||||
<el-upload class="avatar-uploader" :auto-upload="false" :show-file-list="false">
|
||||
<img v-if="articleModel.coverImg" :src="articleModel.coverImg" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章内容">
|
||||
<div class="editor">
|
||||
<quill-editor
|
||||
theme="snow"
|
||||
v-model:content="articleModel.content"
|
||||
contentType="html"
|
||||
>
|
||||
</quill-editor>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="addArticle('已发布')">发布</el-button>
|
||||
<el-button type="info" @click="addArticle('草稿')">草稿</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</el-card>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
/* 抽屉样式 */
|
||||
.avatar-uploader {
|
||||
:deep() {
|
||||
.avatar {
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.el-upload {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
}
|
||||
|
||||
.el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-icon.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 178px;
|
||||
height: 178px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.editor {
|
||||
width: 100%;
|
||||
:deep(.ql-editor) {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
3
src/views/user/UserAvatar.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
更换头像
|
||||
</template>
|
64
src/views/user/UserInfo.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import useUserInfoStore from '@/stores/userInfo.js';
|
||||
|
||||
const userInfoStore = useUserInfoStore()
|
||||
const userInfo = ref({...userInfoStore.info}) //解构info数组
|
||||
|
||||
//更新个人信息
|
||||
import { userInfoUpdateService } from '@/api/user'
|
||||
import { ElMessage } from 'element-plus';
|
||||
const userInfoUpdate = async() =>{
|
||||
let result = await userInfoUpdateService(userInfo.value)
|
||||
if (result.code === 0){
|
||||
//注册成功
|
||||
ElMessage.success(result.message? result.message: '修改成功')
|
||||
userInfoStore.setInfo(userInfo.value)
|
||||
}else{
|
||||
//注册失败
|
||||
ElMessage.error('修改失败')
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
nickname: [
|
||||
{ required: true, message: '请输入用户昵称', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^\S{2,10}$/,
|
||||
message: '昵称必须是2-10位的非空字符串',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入用户邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<el-card class="page-container">
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<span>基本资料</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form :model="userInfo" :rules="rules" label-width="100px" size="large">
|
||||
<el-form-item label="登录名称">
|
||||
<el-input v-model="userInfo.username" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称" prop="nickname">
|
||||
<el-input v-model="userInfo.nickname"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户邮箱" prop="email">
|
||||
<el-input v-model="userInfo.email"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="userInfoUpdate">提交修改</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</template>
|
82
src/views/user/UserResetPassword.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const resetPwdData = ref({
|
||||
old_pwd: '',
|
||||
new_pwd: '',
|
||||
re_pwd: ''
|
||||
})
|
||||
|
||||
//修改完重新登录
|
||||
import useUserInfoStore from '@/stores/userInfo.js';
|
||||
import { useTokenStore } from '@/stores/token.js';
|
||||
import router from '@/router';
|
||||
|
||||
const tokenStore = useTokenStore();
|
||||
const userInfoStore = useUserInfoStore();
|
||||
const reLogin = () => {
|
||||
tokenStore.removeToken();
|
||||
userInfoStore.removeInfo();
|
||||
router.push('/login');
|
||||
}
|
||||
|
||||
//更新个人密码
|
||||
import { userPasswordUpdateService } from '@/api/user'
|
||||
import { ElMessage } from 'element-plus';
|
||||
const userPasswordUpdate = async() =>{
|
||||
console.log(resetPwdData.value)
|
||||
let result = await userPasswordUpdateService(resetPwdData.value)
|
||||
if (result.code === 0){
|
||||
//修改成功
|
||||
ElMessage.success(result.message? result.message:'修改成功')
|
||||
userInfoStore.setInfo(userInfo.value)
|
||||
reLogin()
|
||||
ElMessage({type: 'success',message: '密码已修改,请用新密码重新登陆',});
|
||||
}else{
|
||||
//修改失败
|
||||
ElMessage.error('修改失败')
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
old_pwd: [
|
||||
{ required: true, message: '请输入当前密码', trigger: 'blur' },
|
||||
{ min: 5, max: 16, message: '密码长度在 5 到 16 个字符', trigger: 'blur' }
|
||||
],
|
||||
new_pwd: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 5, max: 16, message: '密码长度在 5 到 16 个字符', trigger: 'blur' }
|
||||
],
|
||||
re_pwd: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{ min: 5, max: 16, message: '密码长度在 5 到 16 个字符', trigger: 'blur' }
|
||||
],
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<el-card class="page-container">
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<span>修改密码</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form :model="resetPwdData" :rules="rules" label-width="100px" size="large">
|
||||
<el-form-item label="原密码" prop="old_pwd">
|
||||
<el-input v-model="resetPwdData.old_pwd" show-password="false"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="new_pwd">
|
||||
<el-input v-model="resetPwdData.new_pwd" show-password="false"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="重复新密码" prop="re_pwd">
|
||||
<el-input v-model="resetPwdData.re_pwd" show-password="false"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="userPasswordUpdate">提交修改</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</template>
|
27
vite.config.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080', //后台服务所在的源
|
||||
changeOrigin: true, //更改源
|
||||
rewrite: (path) => path.replace(/^\/api/, '') ///api换成空
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|