Bladeren bron

提交文章管理后台

Lijy 6 maanden geleden
bovenliggende
commit
d0fc729119
34 gewijzigde bestanden met toevoegingen van 4952 en 0 verwijderingen
  1. 2 0
      fhKeeper/formulahousekeeper/articleBackend/.gitattributes
  2. 30 0
      fhKeeper/formulahousekeeper/articleBackend/.gitignore
  3. 3 0
      fhKeeper/formulahousekeeper/articleBackend/.vscode/extensions.json
  4. 29 0
      fhKeeper/formulahousekeeper/articleBackend/README.md
  5. 13 0
      fhKeeper/formulahousekeeper/articleBackend/index.html
  6. 8 0
      fhKeeper/formulahousekeeper/articleBackend/jsconfig.json
  7. 3098 0
      fhKeeper/formulahousekeeper/articleBackend/package-lock.json
  8. 25 0
      fhKeeper/formulahousekeeper/articleBackend/package.json
  9. BIN
      fhKeeper/formulahousekeeper/articleBackend/public/favicon.ico
  10. 15 0
      fhKeeper/formulahousekeeper/articleBackend/src/App.vue
  11. 43 0
      fhKeeper/formulahousekeeper/articleBackend/src/api/article.js
  12. 44 0
      fhKeeper/formulahousekeeper/articleBackend/src/api/user.js
  13. BIN
      fhKeeper/formulahousekeeper/articleBackend/src/assets/avatar.jpg
  14. BIN
      fhKeeper/formulahousekeeper/articleBackend/src/assets/cover.jpg
  15. BIN
      fhKeeper/formulahousekeeper/articleBackend/src/assets/default.png
  16. BIN
      fhKeeper/formulahousekeeper/articleBackend/src/assets/login_bg.jpg
  17. BIN
      fhKeeper/formulahousekeeper/articleBackend/src/assets/login_title.png
  18. BIN
      fhKeeper/formulahousekeeper/articleBackend/src/assets/logo.png
  19. BIN
      fhKeeper/formulahousekeeper/articleBackend/src/assets/logo2.png
  20. 20 0
      fhKeeper/formulahousekeeper/articleBackend/src/assets/main.scss
  21. 38 0
      fhKeeper/formulahousekeeper/articleBackend/src/main.js
  22. 36 0
      fhKeeper/formulahousekeeper/articleBackend/src/router/index.js
  23. 35 0
      fhKeeper/formulahousekeeper/articleBackend/src/stores/token.js
  24. 18 0
      fhKeeper/formulahousekeeper/articleBackend/src/stores/userInfo.js
  25. 41 0
      fhKeeper/formulahousekeeper/articleBackend/src/utils/quillEditorOptions.js
  26. 83 0
      fhKeeper/formulahousekeeper/articleBackend/src/utils/request.js
  27. 215 0
      fhKeeper/formulahousekeeper/articleBackend/src/views/Layout.vue
  28. 224 0
      fhKeeper/formulahousekeeper/articleBackend/src/views/Login.vue
  29. 205 0
      fhKeeper/formulahousekeeper/articleBackend/src/views/article/ArticleCategory.vue
  30. 416 0
      fhKeeper/formulahousekeeper/articleBackend/src/views/article/ArticleManage.vue
  31. 105 0
      fhKeeper/formulahousekeeper/articleBackend/src/views/user/UserAvatar.vue
  32. 61 0
      fhKeeper/formulahousekeeper/articleBackend/src/views/user/UserInfo.vue
  33. 114 0
      fhKeeper/formulahousekeeper/articleBackend/src/views/user/UserResetPassword.vue
  34. 31 0
      fhKeeper/formulahousekeeper/articleBackend/vite.config.js

+ 2 - 0
fhKeeper/formulahousekeeper/articleBackend/.gitattributes

@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto

+ 30 - 0
fhKeeper/formulahousekeeper/articleBackend/.gitignore

@@ -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 - 0
fhKeeper/formulahousekeeper/articleBackend/.vscode/extensions.json

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

+ 29 - 0
fhKeeper/formulahousekeeper/articleBackend/README.md

@@ -0,0 +1,29 @@
+# BigEventFront
+
+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) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.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 - 0
fhKeeper/formulahousekeeper/articleBackend/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 8 - 0
fhKeeper/formulahousekeeper/articleBackend/jsconfig.json

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

File diff suppressed because it is too large
+ 3098 - 0
fhKeeper/formulahousekeeper/articleBackend/package-lock.json


+ 25 - 0
fhKeeper/formulahousekeeper/articleBackend/package.json

@@ -0,0 +1,25 @@
+{
+  "name": "bigeventfront",
+  "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.6.5",
+    "element-plus": "^2.4.4",
+    "pinia": "^2.1.7",
+    "pinia-persistedstate-plugin": "^0.1.0",
+    "vue": "^3.3.11",
+    "vue-router": "^4.2.5"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^4.5.2",
+    "sass": "^1.69.7",
+    "vite": "^5.0.10"
+  }
+}

BIN
fhKeeper/formulahousekeeper/articleBackend/public/favicon.ico


+ 15 - 0
fhKeeper/formulahousekeeper/articleBackend/src/App.vue

@@ -0,0 +1,15 @@
+<script setup>
+import LoginVue from './views/Login.vue';
+</script>
+
+<template>
+  <!--引入 Login.vue 文件,测试效果-->
+  <!--<LoginVue></LoginVue>-->
+  
+  <!--声明路由标签,这样就可以自动根据访问路径加载对应的 vue 页面-->
+  <router-view></router-view>
+</template>
+
+<style scoped>
+
+</style>

+ 43 - 0
fhKeeper/formulahousekeeper/articleBackend/src/api/article.js

@@ -0,0 +1,43 @@
+import request from '@/utils/request.js'
+import { userTokenStore } from '@/stores/token.js'
+// 文章标签列表查询
+export const articleCategoryListService=()=>{
+    const tokenStore=userTokenStore();
+    // 在 pinia 中定义的 ref() 响应式数据,不需要 .value 格式赋值取值
+    //console.log("当前进入了 article.js")
+    //return request.get('/category',{headers:{'Authorization':tokenStore.token}})
+    // 添加请求拦截器之后,就不用向上面这一行代码这样手动添加身份 token 了
+    return request.get('/category')
+}
+
+// 文章标签添加
+export const articleCategoryAddService=(categoryData)=>{
+    // 如果 categoryData 是json格式数据,这里可以直接附加
+    return request.post('/category',categoryData);
+}
+
+// 文章标签列表修改
+export const articleCategoryUpdateService=(categoryData)=>{
+    return request.put('/category',categoryData)
+}
+
+// 文章标签列表删除
+export const articleCategoryDeleteService=(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)
+    return request.post('/article/insertOrUpdateArticle',articleData)
+}
+
+// 删除文章
+export const articleDeleteService=(params)=>{
+    return request.get('/article/deleteById', {params:params})
+}

+ 44 - 0
fhKeeper/formulahousekeeper/articleBackend/src/api/user.js

@@ -0,0 +1,44 @@
+// 导入 request.js 请求工具
+import request from '@/utils/request.js'
+
+// 提供调用注册接口的函数,使用箭头函数写法,registerData 表示函数接收的参数,userRegisterService 是函数名称
+export const userRegisterService =(registerData)=>{
+    // registerData 是 json 数据,无法直接当作 url 参数传递,因此需要处理
+    // 借助于 UserSearchParams 完成传递
+    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 userPasswordResetService=(resetData)=>{
+    return request.patch('/user/updatePwd',resetData)
+}

BIN
fhKeeper/formulahousekeeper/articleBackend/src/assets/avatar.jpg


BIN
fhKeeper/formulahousekeeper/articleBackend/src/assets/cover.jpg


BIN
fhKeeper/formulahousekeeper/articleBackend/src/assets/default.png


BIN
fhKeeper/formulahousekeeper/articleBackend/src/assets/login_bg.jpg


BIN
fhKeeper/formulahousekeeper/articleBackend/src/assets/login_title.png


BIN
fhKeeper/formulahousekeeper/articleBackend/src/assets/logo.png


BIN
fhKeeper/formulahousekeeper/articleBackend/src/assets/logo2.png


+ 20 - 0
fhKeeper/formulahousekeeper/articleBackend/src/assets/main.scss

@@ -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;
+}

+ 38 - 0
fhKeeper/formulahousekeeper/articleBackend/src/main.js

@@ -0,0 +1,38 @@
+// 替代 main.css 的 main.scss 文件
+import './assets/main.scss'  
+
+import { createApp } from 'vue'
+import App from './App.vue'
+
+// element-plus 相关内容
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+
+// 导入路由
+import router from '@/router/index.js'
+
+// 导入 pinia
+import {createPinia} from 'pinia'
+// 导入 persist 插件
+import { createPersistedState } from 'pinia-persistedstate-plugin'
+
+// 导入中文组件
+import locale from 'element-plus/dist/locale/zh-cn.js'
+
+// 页面创建相关代码
+const app=createApp(App);
+// 使用 pinia,使用 persist 插件
+const pinia = createPinia();
+const persist=createPersistedState()
+pinia.use(persist)
+app.use(pinia)
+// 使用路由
+app.use(router)
+
+// 使用element-plus 和他的中文语言包
+app.use(ElementPlus,{locale});
+
+// 必须最后才能挂载,若先挂载,则 app.use 的那些配置都不生效了。
+app.mount('#app');
+
+

+ 36 - 0
fhKeeper/formulahousekeeper/articleBackend/src/router/index.js

@@ -0,0 +1,36 @@
+import { createRouter,createWebHistory } from "vue-router";
+
+// 导入组件,方便将访问路径与渲染的组件绑定
+import LoginVue from '@/views/Login.vue'
+import LayoutVue from '@/views/Layout.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 UserResetPassword from '@/views/user/UserResetPassword.vue'
+
+
+// 定义路由关系redirect 属性是重定向,这里默认重定向到 /article/category
+const routes=[
+    {path:'/login',component:LoginVue},    
+    {path:'/',component:LayoutVue,redirect:'/article/manage',children:[
+        // 定义子路由
+        {path:'/article/category',component:ArticleCategoryVue},
+        {path:'/article/manage',component:ArticleManageVue},
+        {path:'/user/avatar',component:UserAvatarVue},
+        {path:'/user/info',component:UserInfoVue},
+        {path:'/user/resetPassword',component:UserResetPassword},
+
+    ]}
+]
+
+// 创建路由器
+const router=createRouter({
+    history:createWebHistory(),
+    routes:routes
+})
+
+// 导出路由
+export default router

+ 35 - 0
fhKeeper/formulahousekeeper/articleBackend/src/stores/token.js

@@ -0,0 +1,35 @@
+// 定义 store
+import {defineStore} from 'pinia'
+// 导入响应式对象,后面会用
+import {ref} from 'vue'
+
+/*
+第一个参数:名字唯一性
+第二个参数:函数,函数的内部可以定义状态的所有内容
+
+返回值:函数
+*/
+export const userTokenStore=defineStore('token',()=>{
+    // 下面都是定义状态的内容
+
+    //1.响应式变量
+    const token=ref('')
+
+    //2.定义一个函数修改token的值
+    const setToken=(newToken)=>{
+        token.value=newToken
+    }
+
+    //3.函数,移除token的值
+    const removeToken=()=>{
+        token.value=''
+    }
+
+    //4.返回定义的变量
+    return {
+        token,setToken,removeToken
+    }
+},{
+    // 开启 persist 持久化存储插件
+    persist:true
+})

+ 18 - 0
fhKeeper/formulahousekeeper/articleBackend/src/stores/userInfo.js

@@ -0,0 +1,18 @@
+import { defineStore } from "pinia";
+import {ref} from 'vue'
+const userInfoStore=defineStore('userInfo',()=>{
+    //定义状态相关的内容
+    const info=ref({})
+
+    const setInfo=(newInfo)=>{
+        info.value=newInfo;
+    }
+
+    const removeInfo=()=>{
+        info.value={}
+    }
+
+    return {info,setInfo,removeInfo}
+},{persist:true})
+
+export default userInfoStore

+ 41 - 0
fhKeeper/formulahousekeeper/articleBackend/src/utils/quillEditorOptions.js

@@ -0,0 +1,41 @@
+export const options = {
+  theme: 'snow',
+  debug: 'warn',
+  modules: {
+    // 工具栏配置
+    toolbar: {
+      container: [
+        ['bold', 'italic', 'underline', 'strike'], // 加粗 斜体 下划线 删除线
+        ['blockquote', 'code-block'], // 引用  代码块
+        [{ list: 'ordered' }, { list: 'bullet' }], // 有序、无序列表
+        [{ indent: '-1' }, { indent: '+1' }], // 缩进
+        [{ size: ['small', false, 'large', 'huge'] }], // 字体大小
+        [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
+        [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
+        [{ align: [] }], // 对齐方式
+        ['clean'], // 清除文本格式
+        ['link', 'image'], // 链接、图片、视频
+      ],
+      // ImageDrop: true,//支持图片拖拽
+      // imageResize: { //支持图片大小尺寸修改
+      //   displayStyles: {
+      //     backgroundColor: 'black',
+      //     border: 'none',
+      //     color: 'white'
+      //   },
+      //   modules: ['Resize', 'DisplaySize','Toolbar']
+      // }
+    }
+  },
+  placeholder: '请输入公告内容...',
+  // readOnly: props.readOnly,
+  clipboard: {
+    matchers: [
+      ['img', (node, delta) => {
+        const src = node.getAttribute('src');
+        const id = node.getAttribute('id');
+        delta.insert({ image: { src, 'id': id } });
+      }],
+    ],
+  },
+}

+ 83 - 0
fhKeeper/formulahousekeeper/articleBackend/src/utils/request.js

@@ -0,0 +1,83 @@
+//定制请求的实例
+
+//导入axios  npm install axios
+import axios from 'axios';
+
+// 美化提示信息
+import { ElMessage } from 'element-plus'  
+
+//定义一个变量,记录公共的前缀  ,  baseURL 此刻为了解决跨域问题,改成了 /api ,方便设置代理的时候进行替换
+const baseURL = '/api';
+const instance = axios.create({baseURL})
+
+// request.js 文件中,这样导入无法使用 router
+//import { useRouter } from 'vue-router';
+//const router=useRouter()
+
+
+
+// 添加请求拦截器
+import {userTokenStore} from '@/stores/token.js'
+instance.interceptors.request.use(
+    (config)=>{
+        //请求前的回调
+        //添加token
+        const tokenStore=userTokenStore();
+        //判断有没有token
+        if(tokenStore.token){
+            config.headers.Authorization=tokenStore.token;
+        }
+        return config;
+    },
+    (err)=>{
+        //请求错误的回调
+        Promise.reject(err);
+    }
+)
+
+// 懒加载引入 router ,否则可能会导致加载时机不对,无法解析 Login.vue 视图,非常离谱的错误!
+const getRouter = () => import('@/router');
+//import router from '@/router'
+//添加响应拦截器
+instance.interceptors.response.use(
+    async result=>{
+        // 判断业务状态码
+        if(result.data.code===0){
+            return result.data;
+        }
+        // 业务状态码为1,就弹出服务器端返回从校验信息
+        if(result.data.code===1){
+            ElMessage.error(result.data)
+        }
+        else{
+            // 若是操作失败
+            //alert(result.data.msg?result.data.msg:'操作失败!');
+            //alert('服务异常');    这里美化了提示信息
+            ElMessage.error('服务异常')
+        }
+
+        
+
+        // 异步操作的状态转换成失败
+        return Promise.reject(result.data);
+    },
+    async err=>{
+        const router = await getRouter()
+        if(err.response.status==401){
+            // 未登录,跳转到登录页面
+            ElMessage.error('请先登录')
+            //router.push('/login')
+            router.default.push('/login');  // 注意这里使用 router.default
+        }
+        
+        else{
+            //alert('服务异常');    这里美化了提示信息
+             ElMessage.error('服务异常')
+            
+        }
+        return Promise.reject(err);//异步的状态转化成失败的状态
+        
+    }
+)
+
+export default instance;

+ 215 - 0
fhKeeper/formulahousekeeper/articleBackend/src/views/Layout.vue

@@ -0,0 +1,215 @@
+<script setup>
+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'
+const userInfoStore=useUserInfoStore()
+const getUserInfo=async()=>{
+    //调用接口
+    let result=await userInfoService();
+    //用户信息存储到 pinia 中
+    userInfoStore.setInfo(result.data)
+}
+getUserInfo();
+
+// 下拉菜单中条目被点击后执行的函数
+import {useRouter} from 'vue-router'
+const router=useRouter()
+import { userTokenStore } from '@/stores/token.js';
+import { ElMessageBox,ElMessage } from 'element-plus'
+const tokenStore=userTokenStore();
+const handleCommand=(command)=>{
+    // 判断指令
+    if(command==="logout"){
+        //退出登录
+        // 从 element-plus 上复制的代码
+        ElMessageBox.confirm(
+        '你确认要退出登录吗?',
+        '温馨提示',
+        {
+        confirmButtonText: '确认',
+        cancelButtonText: '取消',
+        type: 'warning',
+        }
+    )
+        .then(async() => {
+            // 退出登录
+            //1.清空pinia中存储的token和个人信息
+            tokenStore.removeToken()
+            userInfoStore.removeInfo()
+            //2.跳转到登录页面
+            router.push("/login")
+
+            ElMessage({
+                type: 'success',
+                message: '退出登录成功!',
+            })
+            
+            
+            })
+            .catch(() => {
+            ElMessage({
+                type: 'info',
+                message: '用户取消退出登录!',
+            })
+        })
+
+    }
+    else{
+        // 路由至个人信息相关功能模块
+        router.push('/user/'+command)
+    }
+}
+
+</script>
+
+<template>
+    <el-container class="layout-container">
+        <!-- 左侧菜单 -->
+        <el-aside width="200px">
+            <div class="el-aside__logo"></div>
+            <el-menu active-text-color="#ffd04b" background-color="#232323"  text-color="#fff"
+                router>
+                <!--index 属性的值不能重复,填写路由访问路径,有点像 href 属性-->
+                <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>
+        <!-- 右侧主区域 -->
+        <el-container>
+            <!-- 头部区域 -->
+            <el-header>
+                <div>文章后台系统:<strong>{{ userInfoStore.info.nickname }}</strong></div>
+                <!--下拉菜单,注意这里的 @command,让其捆绑自定义的事件处理函数,用于对下面个人信息中捆绑的command属性进行处理-->
+                <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>
+                            <!--command 表示当前条目被点击后会触发,在事件函数上可以声明一个参数,接收条目对于的指令-->
+                            <!--command="info" 这里的info指的是当初在 /router/index.js 中设置的路由-->
+                            <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>
+                <!--
+                    <div style="width: 1290px; height: 570px;border: 1px solid red;">
+                    内容展示区
+                </div>
+                -->
+                <!--内容展示区,根据子路由显示不同的页面-->
+                <router-view></router-view>
+            </el-main>
+            <!-- 底部区域 -->
+            <el-footer>大事件 ©2023 Created by 文章后台系统</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;
+    }
+}
+</style>

+ 224 - 0
fhKeeper/formulahousekeeper/articleBackend/src/views/Login.vue

@@ -0,0 +1,224 @@
+<script setup>
+import { User, Lock } from '@element-plus/icons-vue'
+import { ref } from 'vue'
+// 控制注册与登录表单的显示, 默认显示注册
+const isRegister = ref(false)
+
+// 定义注册表单的数据模型,并绑定到对应的标签
+const registerData=ref({
+    username:'',
+    password:'',
+    rePassword:''
+}
+)
+
+// 自定义表单校验规则:校验二次密码是否和第一次输入的一致,参考 element-plus 官方文档编写
+const checkRePassword=(rule,value,callback)=>{
+    if(value===''){
+        callback(new Error("请再次确认密码!"))
+    }else if(value!==registerData.value.password){
+        callback(new Error('请确保两次输入的密码一样!'))
+    }else{
+        callback()
+    }
+}
+
+
+// 定义表单校验规则,由于使用的是 Element-Plus ,所以,表单校验规则需要参考官方文档进行编写
+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 { ElMessage } from 'element-plus'
+
+// 点击注册按钮后,触发这里的方法,发送请求至后端接口,完成注册
+import {userRegisterService,userLoginService} from '@/api/user.js'
+const register=async()=>{
+    // 上面自定义的 registerData 变量是 ref 响应式变量,要想获取值,需要 registerData.value
+    let result=await userRegisterService(registerData.value)
+    
+    /*
+    // 这里的 result.code 和 result.msg ,都是后端开发时,当初定义的返回数据 json 格式。
+    if(result.code===0){
+        // 成功了
+        alert(result.msg?result.msg:'注册成功!')
+    }else{
+        // 失败了
+        alert("注册失败!")
+    }
+    */
+
+    // 利用改良后的响应拦截器,若代码到这里,则说明成功。
+    //alert(result.msg?result.msg:'注册成功!')
+    // 美化提示信息的写法
+    ElMessage({message: '注册成功!',type: 'success',})
+
+}
+
+// 绑定登录表单数据的时候,复用了注册表单的数据模型,因此这里没有重新定义登录数据模型
+// 表单数据校验也是复用注册表单的,也没有创建新的规则
+
+// 使用路由,方便用户点击登录按钮后,能自动跳转主页
+import {useRouter} from 'vue-router'
+const router=useRouter()
+
+// 为了能记录后端的 token,这里使用 pinia
+import {userTokenStore} from '@/stores/token.js'
+const tokenStore=userTokenStore();
+
+// 登录功能点击后触发的函数
+const login=async()=>{
+    // 调用接口,完成登录
+    let result=await userLoginService(registerData.value)
+    
+    /*
+    if(result.code===0){
+        alert(result.msg?result.msg:'登录成功!')
+    }else{
+        alert("登录失败!")
+    }
+    */
+
+    // 利用改良后的响应拦截器,若代码到这里,说明成功
+    //alert(result.msg?result.msg:'登录成功!')
+    // 美化提示信息的写法
+    ElMessage({message: '登录成功!',type: 'success',})
+    
+    // 记录后端传过来的 token
+    tokenStore.setToken(result.data);
+
+    // 跳转到主界面
+    router.push('/')
+}
+
+// 定义清空数据模型,使用户点击注册或返回按钮时,数据清空
+const clearRegisterData=()=>{
+    registerData.value={
+        username:'',
+        password:'',
+        rePassword:''
+    }
+}
+
+</script>
+
+<template>
+    <el-row class="login-page">
+        <el-col :span="12" class="bg"></el-col>
+        <el-col :span="6" :offset="3" class="form">
+            <!-- 注册表单,使用 :model 绑定 script 标签中的的表单数据 registerData ,使用 :rules 绑定 rules 表单校验规则 -->
+            <el-form ref="form" size="large" autocomplete="off" v-if="isRegister" :model="registerData" :rules="rules">
+                <el-form-item>
+                    <h1>注册</h1>
+                </el-form-item>
+                <!--prop 指定使用表单校验规则中 username 那一项-->
+                <el-form-item prop="username">
+                    <!-- v-model 绑定 script 标签中定义的 registerData.username -->
+                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
+                </el-form-item>
+                <!--prop 指定使用表单校验规则中 password 那一项-->
+                <el-form-item prop="password">
+                    <!-- v-model 绑定 script 标签中定义的 registerData.password-->
+                    <el-input :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.password"></el-input>
+                </el-form-item>
+                <!--prop 指定使用表单校验规则中 rePassword 那一项-->
+                <el-form-item prop="rePassword">
+                    <!-- v-model 绑定 script 标签中定义的 registerData.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; clearRegisterData()" >
+                        ← 返回
+                    </el-link>
+                </el-form-item>
+            </el-form>
+            <!-- 登录表单 ,这里复用注册表单的 registerData 变量信息,并复用注册表单的校验规则-->
+            <el-form ref="form" size="large" autocomplete="off" :model="registerData" :rules="rules" v-else>
+                <el-form-item>
+                    <h1>登录</h1>
+                </el-form-item>
+                <!--prop 指定使用表单校验规则中 username 那一项-->
+                <el-form-item prop="username">
+                    <!--这里复用注册表单的 registerData.username-->
+                    <el-input :prefix-icon="User" placeholder="请输入用户名" v-model="registerData.username"></el-input>
+                </el-form-item>
+                <!--prop 指定使用表单校验规则中 password 那一项-->
+                <el-form-item prop="password">
+                    <!--这里复用注册表单的 registerData.password-->
+                    <el-input name="password" :prefix-icon="Lock" type="password" placeholder="请输入密码" v-model="registerData.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; clearRegisterData()" >
+                        注册 →
+                    </el-link>
+                </el-form-item>
+            </el-form>
+        </el-col>
+    </el-row>
+</template>
+
+<style lang="scss" scoped>
+/* 样式 */
+.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 {
+        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>

+ 205 - 0
fhKeeper/formulahousekeeper/articleBackend/src/views/article/ArticleCategory.vue

@@ -0,0 +1,205 @@
+<script setup>
+import {
+    Edit,
+    Delete
+} from '@element-plus/icons-vue'
+import { ref } from 'vue'
+const categorys = 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"
+    }
+])
+
+// 声明一个异步函数并使用
+import {articleCategoryListService,articleCategoryAddService,articleCategoryUpdateService,articleCategoryDeleteService} from '@/api/article.js'
+
+
+const articleCategoryList=async()=>{
+    console.log("ArticleCategory.vue 的 async 异步方法成功进入。")
+    let result=await articleCategoryListService();
+    // 不明白为什么可以直接将返回的 data 赋值给 categorys.value 属性。但是他们的键名是一样的
+    categorys.value=result.data;
+}
+// 必须调用一下才生效!
+articleCategoryList();
+
+
+// 弹窗部分代码
+//控制添加分类的弹窗,默认false不显示
+const dialogVisible = ref(false)
+
+//添加分类的数据模型,这里的模型和发送给后端添加分类的数据模型一致
+const categoryModel = ref({
+    categoryName: '',
+    categoryAlias: ''
+})
+//添加分类的表单校验
+const rules = {
+    categoryName: [
+        { required: true, message: '请输入分类名称', trigger: 'blur' },
+    ],
+    categoryAlias: [
+        { required: true, message: '请输入分类别名', trigger: 'blur' },
+    ]
+}
+
+import {ElMessage} from 'element-plus'
+// 调用接口,提交添加分类的表单数据
+const addCategory=async()=>{
+    // 调用接口
+    let result=await articleCategoryAddService(categoryModel.value);
+    // 如果能得到返回结果,说明成功了
+    ElMessage.success(result.msg?result.msg:'添加成功!')
+
+    // 添加分类成功后,再刷新一下页面,并让当前弹窗消失,因此再调用一下
+    articleCategoryList();
+    dialogVisible.value=false;
+}
+
+// 定义变量,控制标题的展示
+const title=ref('');
+// 展示编辑弹窗
+const showDialog=(row)=>{
+    dialogVisible.value=true;
+    title.value='编辑分类';
+    //数据拷贝
+    categoryModel.value.categoryName=row.categoryName;
+    categoryModel.value.categoryAlias=row.categoryAlias;
+    //扩展id属性,将来需要传递给后端,完成分类的修改
+    categoryModel.value.id=row.id;
+}
+
+// 编辑分类
+const updateCategory=async()=>{
+    // 调用接口
+    let result=await articleCategoryUpdateService(categoryModel.value);
+    ElMessage.success(result.msg?result.msg:'修改成功!')
+
+    // 调用获取所有分类的函数
+    articleCategoryList();
+
+    //隐藏弹窗
+    dialogVisible.value=false;
+}
+
+// 清空模型数据
+const clearData=()=>{
+    categoryModel.value.categoryName=''
+    categoryModel.value.categoryAlias=''
+}
+
+import { ElMessageBox } from 'element-plus'
+// 删除分类
+const deleteCategory=(row)=>{
+    // 从 element-plus 上复制的代码
+    ElMessageBox.confirm(
+    '你确认要删除该分类信息吗?',
+    'Warning',
+    {
+      confirmButtonText: '确认',
+      cancelButtonText: '取消',
+      type: 'warning',
+    }
+  )
+    .then(async() => {
+        // 调用删除接口
+        let result=await articleCategoryDeleteService(row.id)
+      ElMessage({
+        type: 'success',
+        message: '删除分类成功!',
+      })
+      //刷新列表
+      articleCategoryList();
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '用户取消删除!',
+      })
+    })
+
+}
+
+
+
+</script>
+<template>
+    <el-card class="page-container">
+        <template #header>
+            <div class="header">
+                <span >文章标签</span>
+                <div class="extra">
+                    <el-button type="primary" @click="dialogVisible=true;title='添加分类';clearData()">添加分类</el-button>
+                </div>
+            </div>
+        </template>
+        <!--这里的写法是 element-plus 表格的写法,官方文档示例代码就是这样写 prop 属性的-->
+        <el-table :data="categorys" 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 }">-->
+                <template #default="{row}">
+                    <el-button :icon="Edit" circle plain type="primary" @click="showDialog(row)"></el-button>
+                    <el-button :icon="Delete" circle plain type="danger" @click="deleteCategory(row)"></el-button>
+                </template>
+            </el-table-column>
+            <template #empty>
+                <el-empty description="没有数据" />
+            </template>
+        </el-table>
+
+        <!-- 添加分类弹窗 -->
+<el-dialog v-model="dialogVisible" :title="title" width="30%">
+    <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>
+            <!--巧妙的利用 title 值实现当前走添加分类逻辑还是更新分类逻辑-->
+            <el-button type="primary" @click="title=='添加分类'?addCategory():updateCategory()"> 确认 </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>

+ 416 - 0
fhKeeper/formulahousekeeper/articleBackend/src/views/article/ArticleManage.vue

@@ -0,0 +1,416 @@
+<script setup>
+import {
+    Edit,
+    Delete
+} from '@element-plus/icons-vue'
+
+import { ref } from 'vue'
+
+//文章标签数据模型
+const categorys = ref([])
+
+//用户搜索时选中的分类id
+const categoryId=ref('')
+
+//用户搜索时选中的发布状态
+const state=ref('')
+
+//文章列表数据模型
+const articles = ref([])
+
+const productList = ref([
+    { id: '1', label: '工时管家' }, 
+    { id: '2', label: '随访管家' }, 
+    { id: '3', label: '项目管家' }, 
+    { id: '4', label: '客户管家' }, 
+    { id: '5', label: '生产车间管家' }, 
+])
+
+//分页条数据模型
+const pageNum = ref(1)//当前页
+const total = ref(20)//总条数
+const pageSize = ref(10)//每页条数
+
+//当每页条数发生了变化,调用此函数
+const onSizeChange = (size) => {
+    pageSize.value = size
+    articleList();
+}
+//当前页码发生变化,调用此函数
+const onCurrentChange = (num) => {
+    pageNum.value = num
+    articleList();
+}
+//回显文章标签
+import {articleCategoryListService, articleListService,articleAddService, articleDeleteService} from '@/api/article.js'
+const articleCategoryList=async()=>{
+    let result = await articleCategoryListService();
+    categorys.value=result.data;
+}
+articleCategoryList();
+
+//获取文章列表数据
+const articleList=async()=>{
+    let params={
+        pageNum:pageNum.value,
+        pageSize:pageSize.value,
+        categoryId:categoryId.value?categoryId.value:null,
+        state:state.value?state.value:null
+    }
+    let result =await articleListService(params);
+    //渲染视图
+    total.value=result.data.total;
+    articles.value=result.data.items;
+
+    //处理数据,给数据模型扩展一个属性 categoryName ,分类名称
+    for(let i=0;i<articles.value.length;i++){
+        let article=articles.value[i];
+        for(let j=0;j<categorys.value.length;j++){
+            if(article.categoryId==categorys.value[j].id){
+                article.categoryName=categorys.value[j].categoryName
+            }
+        }
+    }
+}
+articleList()
+
+// 添加文章功能
+import {Plus} from '@element-plus/icons-vue'
+//控制抽屉是否显示
+const visibleDrawer = ref(false)
+//添加表单数据模型
+const articleModel = ref({
+    title: '',
+    categoryId: '',
+    coverImg: '',
+    content:'',
+    state:'',
+    profile: '',
+    productId: ''
+})
+const fileList = ref([])
+
+// 文章编辑器组件
+import { QuillEditor } from '@vueup/vue-quill'
+import '@vueup/vue-quill/dist/vue-quill.snow.css'
+import { options } from '../../utils/quillEditorOptions'
+
+//导入token,方便编辑完文章后保存至服务器
+import {userTokenStore} from '@/stores/token.js'
+const tokenStore=userTokenStore();
+
+//上传成功的回调函数
+const uploadSuccess=(result)=>{
+    //将服务器响应的图片地址赋值给 articleModel 的 coverImg
+    articleModel.value.coverImg=result.data;
+    console.log(result.data)
+}
+
+const fileExceedsLimit = () => {
+    ElMessage.warning('只能上传一个文章封面')
+}
+
+// 添加文章
+import {ElMessage, ElMessageBox} from 'element-plus'
+const addArticle=async(clickState)=>{
+    // 把发布文章yes或草稿no赋值给数据模型
+    articleModel.value.state=clickState;
+    const { content, title, profile, categoryId, productId } = articleModel.value
+    let str = ''
+    if(!content) {
+        str += '文章内容不能为空,'
+    }
+    if(!title) {
+        str += '文章标题不能为空,'
+    }
+    if(!profile) {
+        str += '文章封面不能为空,'
+    }
+    if(!categoryId || categoryId.length <= 0) {
+        str += '文章标签不能为空,'
+    }
+    if(fileList.value.length <= 0) {
+        str += '文章封面不能为空,'
+    }
+    if(!productId || productId == '0') {
+        str += '文章所属产品不能为空,'
+    }
+    if(str) {
+        ElMessage.warning(str)
+        return
+    }
+    const formVla = { ...articleModel.value, categoryIds: (articleModel.value.categoryId || []).join(',') }
+    delete formVla.categoryId
+    const formData = new FormData()
+    for (const key in formVla) {
+        if(key == 'coverImg') {
+            let file = fileList.value[0].raw
+            if(!isFile(file)) {
+                file = base64ToFile(file, 'image.png')
+            }
+            formData.append('coverImage', file)
+        } else {
+            formData.append(key, formVla[key])
+        }
+    }
+    //调用接口
+    // let result=await articleAddService(articleModel.value)
+    let result=await articleAddService(formData)
+    ElMessage.success(result.msg?result.msg:"添加文章成功!")
+
+    //让添加文章的抽屉消失
+    visibleDrawer.value = false;
+    //刷新当前列表
+    articleList()
+}
+const isFile = (obj) => {
+    return obj instanceof File;
+}
+
+const base64ToFile = (base64String, fileName) => {
+  const arr = base64String.split(',');
+  const mime = arr[0].match(/:(.*?);/)[1];
+  const bstr = atob(arr[1]);
+  let n = bstr.length;
+  const u8arr = new Uint8Array(n);
+
+  while (n--) {
+    u8arr[n] = bstr.charCodeAt(n);
+  }
+
+  return new File([u8arr], fileName, { type: mime });
+}
+// 修改文章
+const editArticle = (row) => {
+    const { categoryIds, content, title, profile, id, coverImg, productId } = row
+    articleModel.value = {
+        categoryId: categoryIds ? JSON.parse(categoryIds) : [],
+        content,
+        title,
+        profile,
+        id,
+        coverImg: '',
+        productId: productId
+    }
+    if(coverImg) {
+        fileList.value = [{
+            name: '图片',
+            url: `data:image/jpeg;base64, ${coverImg}`,
+            raw: `data:image/jpeg;base64, ${coverImg}`,
+        }]
+    } else {
+        fileList.value = []
+    }
+    
+
+    visibleDrawer.value = true
+}
+
+const deleteArticle = async (row) => {
+    const { id, title } = row
+    ElMessageBox.confirm(
+        `确定删除【${title}】文章吗?`,
+        {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+        }
+    ).then(async () => {
+        let result=await articleDeleteService({ id })
+        ElMessage.success(result.message?result.message:"删除文章成功!")
+        articleList()
+    })
+}
+</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 categorys" 
+                        :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="yes"></el-option>
+                    <el-option label="草稿" value="no"></el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" @click="articleList">搜索</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="categoryNames"></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="{ row }">
+                    <el-button :icon="Edit" circle plain type="primary" @click="editArticle(row)"></el-button>
+                    <el-button :icon="Delete" circle plain type="danger" @click="deleteArticle(row)"></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="`${articleModel.id ? '编辑文章' : '添加文章'}`" 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-input type="textarea"
+                    :autosize="{ minRows: 2, maxRows: 4}" v-model="articleModel.profile" placeholder="请输入简介"></el-input>
+                </el-form-item>
+                <el-form-item label="文章标签">
+                    <el-select placeholder="请选择" multiple v-model="articleModel.categoryId" style="width: 100%;">
+                        <el-option v-for="c in categorys" :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="articleModel.productId" style="width: 100%;">
+                        <el-option v-for="c in productList" :key="c.id" :label="c.label" :value="c.id">
+                        </el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="文章封面">
+                    <!--element-plus 的文件上传组件
+                            auto-upload:是否自动上传
+                            action: 服务器接口路径
+                            name: 上传的文件字段名,也就是参数名称
+                            headers: 设置上传的请求头
+                            on-success: 上传成功的回调函数
+                    -->
+                    <el-upload
+                        v-model:file-list="fileList"
+                        list-type="picture-card"
+                        :auto-upload="false"
+                        :limit="1"
+                        :on-exceed="fileExceedsLimit"
+                    >
+                        <el-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"
+                            :options="options"
+                            >
+                        </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;
+  }
+}
+
+.avatar-uploader .el-upload {
+    border: 1px dashed #d9d9d9;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+}
+.avatar-uploader .el-upload:hover {
+    border-color: #409EFF;  
+}
+.avatar-uploader-icon {
+    font-size: 28px;
+    color: #8c939d;
+    width: 178px;
+    height: 178px;
+    line-height: 178px;
+    text-align: center;
+}
+.avatar {
+    width: 178px;
+    height: 178px;
+    display: block;
+}
+</style>

+ 105 - 0
fhKeeper/formulahousekeeper/articleBackend/src/views/user/UserAvatar.vue

@@ -0,0 +1,105 @@
+<script setup>
+import { Plus, Upload } from '@element-plus/icons-vue'
+import {ref} from 'vue'
+import avatar from '@/assets/default.png'
+const uploadRef = ref()
+
+import {userTokenStore} from '@/stores/token.js'
+const tokenStore=userTokenStore()
+
+import useUserInfoStore from '@/stores/userInfo.js'
+const userInfoStore=useUserInfoStore()
+//用户头像地址
+const imgUrl= ref(userInfoStore.info.userPic)
+
+// 图片上传成功的回调函数
+const uploadSuccess=(result)=>{
+    imgUrl.value=result.data
+}
+
+import {userAvatarUpdateService} from '@/api/user.js'
+import { ElMessage } from 'element-plus'
+//头像修改
+const updateAvatar=async()=>{
+    //调用接口
+     let result=await userAvatarUpdateService(imgUrl.value)
+     ElMessage.success(result.msg?result.msg:"头像修改成功!")
+
+     //修改pinia中的数据
+     userInfoStore.info.userPic=imgUrl.value;
+}
+</script>
+
+<template>
+    <el-card class="page-container">
+        <template #header>
+            <div class="header">
+                <span>更换头像</span>
+            </div>
+        </template>
+        <el-row>
+            <el-col :span="12">
+                <!--
+                    :auto-upload 自动上传
+                    action="/api/upload"  别忘了这里的api是自己当初设置的前端路由标志,会被请求拦截器替换为空
+                    name 是参数名称
+                    :headers 是请求头,里面要加凭证
+                    :on-success 是上传成功后要执行的回调函数
+                -->
+                <el-upload 
+                    ref="uploadRef"
+                    class="avatar-uploader" 
+                    :show-file-list="false"
+                    :auto-upload="true"
+                    action="/api/upload"
+                    name="file"
+                    :headers="{'Authorization':tokenStore.token}"
+                    :on-success="uploadSuccess"
+                    >
+                    <img v-if="imgUrl" :src="imgUrl" class="avatar" />
+                    <img v-else :src="avatar" width="278" />
+                </el-upload>
+                <br />
+                <el-button type="primary" :icon="Plus" size="large"  @click="uploadRef.$el.querySelector('input').click()">
+                    选择图片
+                </el-button>
+                <el-button type="success" :icon="Upload" size="large" @click="updateAvatar">
+                    上传头像
+                </el-button>
+            </el-col>
+        </el-row>
+    </el-card>
+</template>
+
+<style lang="scss" scoped>
+.avatar-uploader {
+    :deep() {
+        .avatar {
+            width: 278px;
+            height: 278px;
+            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: 278px;
+            height: 278px;
+            text-align: center;
+        }
+    }
+}
+</style>

+ 61 - 0
fhKeeper/formulahousekeeper/articleBackend/src/views/user/UserInfo.vue

@@ -0,0 +1,61 @@
+<script setup>
+import { ref } from 'vue'
+// 要想获取用户信息,那么登录后,直接从 pinia 中获取就可以了
+import useUserInfoStore from '@/stores/userInfo.js'
+import { userInfoUpdateService } from '@/api/user'
+import { ElMessage } from 'element-plus'
+const userInfoStore =useUserInfoStore()
+const userInfo = ref({...userInfoStore.info})
+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' }
+    ]
+}
+
+//修改个人信息
+const updateUserInfo=async()=>{
+    //调用接口
+    let result=await userInfoUpdateService(userInfo.value)
+    ElMessage.success(result.msg?result.msg:"用户信息修改成功!")
+
+    //个人信息修改成功后,pinia中的信息也要修改
+    userInfoStore.setInfo(userInfo.value)
+}
+
+</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="updateUserInfo">提交修改</el-button>
+                    </el-form-item>
+                </el-form>
+            </el-col>
+        </el-row>
+    </el-card>
+</template>

+ 114 - 0
fhKeeper/formulahousekeeper/articleBackend/src/views/user/UserResetPassword.vue

@@ -0,0 +1,114 @@
+<script setup>
+import {ref} from 'vue'
+
+// 自定义表单校验规则:校验二次密码是否和第一次输入的一致,参考 element-plus 官方文档编写
+const checkre_pwd=(rule,value,callback)=>{
+    if(value===''){
+        callback(new Error("请再次确认密码!"))
+    }else if(value!==resetData.value.new_pwd){
+        callback(new Error('请确保两次输入的密码一样!'))
+    }else{
+        callback()
+    }
+}
+
+
+// 定义表单校验规则,由于使用的是 Element-Plus ,所以,表单校验规则需要参考官方文档进行编写
+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:[
+        {validator:checkre_pwd,trigger:'blur'}
+    ]
+}
+
+// 定义注册表单的数据模型,并绑定到对应的标签
+const resetData=ref({
+    old_pwd:'',
+    new_pwd:'',
+    re_pwd:''
+}
+)
+
+// 重置密码
+import {userPasswordResetService} from '@/api/user.js'
+import { ElMessage } from 'element-plus'
+const resetPassword=async()=>{
+    let result=await userPasswordResetService(resetData.value);
+    ElMessage.success(result.msg?result.msg:"密码重置成功!")
+}
+
+</script>
+
+<template>
+    <el-col :span="6" :offset="3" class="form">
+    <!-- 登录表单 ,这里复用注册表单的 resetData 变量信息,并复用注册表单的校验规则-->
+    <el-form ref="form" size="large" autocomplete="off" :model="resetData" :rules="rules">
+                <el-form-item>
+                    <h1>密码重置</h1>
+                </el-form-item>
+                <!--prop 指定使用表单校验规则中 old_pwd 那一项-->
+                <el-form-item prop="old_pwd">
+                    <!--这里复用注册表单的 resetData.old_pwd-->
+                    <el-input name="old_pwd" :prefix-icon="Lock" type="password" placeholder="请输入你的旧密码" v-model="resetData.old_pwd"></el-input>
+                </el-form-item>
+                <!--prop 指定使用表单校验规则中 password 那一项-->
+                <el-form-item prop="new_pwd">
+                    <!--这里复用注册表单的 resetData.password-->
+                    <el-input name="new_pwd" :prefix-icon="Lock" type="password" placeholder="请输入要重置的密码" v-model="resetData.new_pwd"></el-input>
+                </el-form-item>
+                <el-form-item prop="re_pwd">
+                    <!--这里复用注册表单的 resetData.re_pwd-->
+                    <el-input name="re_pwd" :prefix-icon="Lock" type="password" placeholder="再次确认密码" v-model="resetData.re_pwd"></el-input>
+                </el-form-item>
+               
+                <!-- 登录按钮 -->
+                <el-form-item>
+                    <!--添加点击事件,点击后进行登录操作-->
+                    <el-button class="button" type="primary" auto-insert-space @click="resetPassword">重置密码</el-button>
+                </el-form-item>
+                
+            </el-form>
+        </el-col>
+</template>
+
+<style lang="scss" scoped>
+/* 样式 */
+.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 {
+        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>

+ 31 - 0
fhKeeper/formulahousekeeper/articleBackend/vite.config.js

@@ -0,0 +1,31 @@
+import { fileURLToPath, URL } from 'node:url'
+
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    vue(),
+  ],
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url))
+    }
+  },
+  // 服务器代理配置
+  server:{
+    host: '0.0.0.0',
+    port: 18972,
+    proxy:{
+      '/api':{
+        // target:'http://localhost:8080',  // 后台服务所在的源,用这个源替换前端服务源
+        target:'http://47.101.180.183:8091',  // 后台服务所在的源,用这个源替换前端服务源
+        // target:'http://192.168.2.17:8091',  // 后台服务所在的源,用这个源替换前端服务源
+        changeOrigin:true,     // 开启修改源
+        rewrite:(path)=>path.replace(/^\/api/,'')   // url路径中的 /api 将会被替换成 '' 
+      }
+    }
+    
+  }
+})