first commit

This commit is contained in:
xclele 2025-01-21 10:09:12 +08:00
commit eedfd97bbd
35 changed files with 5824 additions and 0 deletions

30
.gitignore vendored Normal file
View 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
View File

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

29
README.md Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

4147
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

13
src/App.vue Normal file
View File

@ -0,0 +1,13 @@
<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style>
</style>

31
src/api/article.js Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

86
src/assets/base.css Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
src/assets/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
src/assets/login_bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
src/assets/login_title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

1
src/assets/logo.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

35
src/assets/main.css Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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() => {
//registerdataregisterData.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() => {
//loginDataloginData.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>

View 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>

View 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>

View File

@ -0,0 +1,3 @@
<template>
更换头像
</template>

View 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>

View 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
View 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换成空
}
}
}
})