全栈开发前端代码:黑马程序员SpringBoot3+Vue3全套视频教程,springboot+vue企业级全栈开,big-event

全栈开发前端代码:黑马程序员SpringBoot3+Vue3全套视频教程,springboot+vue企业级全栈开,big-event

目录:希望对大家有帮助

①项目压缩包:

1.要启动的指令:可以参考下面的文件

①登录、注册页面

src\views\login\LoginPage.vue

src\api\user.js

src\stores\modules\user.js

src\stores\index.js

②首页实现的页面

src\views\layout\LayoutContainer.vue

src\api\user.js

user下面的小模块:实现改用户信息、图像、密码

src\***ponents\PageContainer.vue

src\views\user\UserProfile.vue

src\views\user\UserAvatar.vue

src\views\user\UserPassword.vue

③文章的目录编辑:

src\***ponents\PageContainer.vue

src\views\article\ArticleChannel.vue

src\views\article\***ponents\ChannelEdit.vue

④文章的详情管理:

src\views\article\ArticleManage.vue

src\views\article\***ponents\ChannelSelect.vue

src\views\article\***ponents\ArticleEdit.vue

src\utils\format.js

⑤全局的路由:


①项目压缩包:

📎day13-大事件管理系统-Vue3-big-event-admin.zip

1.要启动的指令:可以参考下面的文件

📎day12~day13-大事件管理系统-new.pdf

📎11-day12-day14-大事件管理系统.zip

npm install -g pnpm

pnpm create vue

pnpm dlx husky-init && pnpm install

pnpm i lint-staged -D

2.自己原版的笔记:在语雀

https://www.yuque.***/wangruiquandayi3banrengongzhineng/bmgrcg/fgp0su8yx0bb5039?singleDoc# 《②big-event前端代码》# 《②big-event前端代码》

①登录、注册页面

src\views\login\LoginPage.vue

<script setup>
  import { userRegisterService, userLoginService } from '@/api/user.js'
  import { User, Lock } from '@element-plus/icons-vue'
  import { ref, watch } from 'vue'
  import { useUserStore } from '@/stores'
  import { useRouter } from 'vue-router'
  const isRegister = ref(false)

  //获取整个表单的信息,进行统一的校验
  //只有信息都输入正确后,才能进行注册的
  const form = ref()

  // 整个的用于提交的form数据对象
  const formModel = ref({
    username: '',
    password: '',
    repassword: ''
  })
  // 整个表单的校验规则
  // 1. 非空校验 required: true      message消息提示,  trigger 触发校验的时机 blur change
  // 2. 长度校验 min:xx, max: xx
  // 3. 正则校验 pattern: 正则规则    \S 非空字符
  // 4. 自定义校验 => 自己写逻辑校验 (校验函数)
  //    validator: (rule, value, callback)
  //    (1) rule  当前校验规则相关的信息
  //    (2) value 所校验的表单元素目前的表单值
  //    (3) callback 无论成功还是失败,都需要 callback 回调
  //        - callback() 校验成功
  //        - callback(new Error(错误信息)) 校验失败
  const rules = {
    username: [
      { required: true, message: '请输入用户名', trigger: 'blur' },
      { min: 5, max: 10, message: '用户名必须是 5-10位 的字符', trigger: 'blur' }
    ],
    password: [
      { required: true, message: '请输入密码', trigger: 'blur' },
      {
        pattern: /^\S{6,15}$/,
        message: '密码必须是 6-15位 的非空字符',
        trigger: 'blur'
      }
    ],
    repassword: [
      { required: true, message: '请输入密码', trigger: 'blur' },
      {
        pattern: /^\S{6,15}$/,
        message: '密码必须是 6-15位 的非空字符',
        trigger: 'blur'
      },
      {
        validator: (rule, value, callback) => {
          // 判断 value 和 当前 form 中收集的 password 是否一致
          if (value !== formModel.value.password) {
            callback(new Error('两次输入密码不一致'))
          } else {
            callback() // 就算校验成功,也需要callback
          }
        },
        trigger: 'blur'
      }
    ]
  }

  const register = async () => {
    // 注册成功之前,先进行校验,校验成功 → 请求,校验失败 → 自动提示
    //validate()是官网提供的方法,已经直接暴露,可以直接使用
    await form.value.validate()
    await userRegisterService(formModel.value)
    ElMessage.su***ess('注册成功')
    isRegister.value = false
  }

  const userStore = useUserStore()
  const router = useRouter()

  const login = async () => {
    await form.value.validate()
    const res = await userLoginService(formModel.value)
    userStore.setToken(res.data.token)
    ElMessage.su***ess('登录成功')
    router.push('/')
  }

  // 切换的时候,重置表单内容
  watch(isRegister, () => {
    formModel.value = {
      username: '',
      password: '',
      repassword: ''
    }
  })
</script>

<template>
  <!-- 
  1. 结构相关
  el-row表示一行,一行分成24份 
  el-col表示列  
  (1) :span="12"  代表在一行中,占12份 (50%)
  (2) :span="6"   表示在一行中,占6份  (25%)
  (3) :offset="3" 代表在一行中,左侧margin份数

  el-form 整个表单组件
  el-form-item 表单的一行 (一个表单域)
  el-input 表单元素(输入框)
  2. 校验相关
  (1) el-form => :model="ruleForm"      绑定的整个form的数据对象 { xxx, xxx, xxx }
  (2) el-form => :rules="rules"         绑定的整个rules规则对象  { xxx, xxx, xxx }
  (3) 表单元素 => v-model="ruleForm.xxx" 给表单元素,绑定form的子属性
  (4) el-form-item => prop配置生效的是哪个校验规则 (和rules中的字段要对应)
  -->
  <el-row class="login-page">
    <!--左边的大事件的图片-->
    <el-col :span="12" class="bg"></el-col>
    <el-col :span="6" :offset="3" class="form">
      <!-- 注册相关表单 -->
      <el-form
        :model="formModel"
        :rules="rules"
        ref="form"
        size="large"
        auto***plete="off"
        v-if="isRegister"
      >
        <el-form-item>
          <h1>注册</h1>
        </el-form-item>
        <el-form-item prop="username">
          <el-input
            v-model="formModel.username"
            :prefix-icon="User"
            placeholder="请输入用户名"
          ></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="formModel.password"
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          ></el-input>
        </el-form-item>
        <el-form-item prop="repassword">
          <el-input
            v-model="formModel.repassword"
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入再次密码"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button
            @click="register"
            class="button"
            type="primary"
            auto-insert-space
          >
            注册
          </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
        :model="formModel"
        :rules="rules"
        ref="form"
        size="large"
        auto***plete="off"
        v-else
      >
        <el-form-item>
          <h1>登录</h1>
        </el-form-item>
        <el-form-item prop="username">
          <el-input
            v-model="formModel.username"
            :prefix-icon="User"
            placeholder="请输入用户名"
          ></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="formModel.password"
            name="password"
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          ></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
            @click="login"
            class="button"
            type="primary"
            auto-insert-space
            >登录</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>
.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>

src\api\user.js

import request from '@/utils/request'

// 注册接口
export const userRegisterService = ({ username, password, repassword }) =>
  request.post('/api/reg', { username, password, repassword })

// 登录接口
export const userLoginService = ({ username, password }) =>
  request.post('/api/login', { username, password })

// 获取用户基本信息
export const userGetInfoService = () => request.get('/my/userinfo')

export const userUpdateInfoService = ({ id, nickname, email }) =>
  request.put('/my/userinfo', { id, nickname, email })

export const userUploadAvatarService = (avatar) =>
  request.patch('/my/update/avatar', { avatar })

export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) =>
  request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd })

src\stores\modules\user.js

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { userGetInfoService } from '../../api/user'

// 用户模块 token setToken removeToken
export const useUserStore = defineStore(
  'big-user',
  () => {
    const token = ref('')
    const setToken = (newToken) => {
      token.value = newToken
    }

    const removeToken = () => {
      token.value = ''
    }

    //当退出登录时,要进行置空
    const user = ref({})

    const getUser = async () => {
      const res = await userGetInfoService() // 请求获取数据
      user.value = res.data.data
    }

    const setUser = (obj) => {
      user.value = obj
    }

    return {
      token,
      setToken,
      removeToken,
      user,
      getUser,
      setUser
    }
  },
  {
    persist: true
  }
)

src\stores\index.js

import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(persist)

export default pinia

//实现了全部的输出,其他模块导入就方便了
export * from './modules/user'
export * from './modules/counter'

// import { useUserStore } from './modules/user'
// export { useUserStore }
// import { useCountStore } from './modules/counter'
// export { useCountStore }

②首页实现的页面

src\views\layout\LayoutContainer.vue

<script setup>
import {
  Management,
  Promotion,
  UserFilled,
  User,
  Crop,
  EditPen,
  SwitchButton,
  CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
import { useUserStore } from '@/stores'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()

// 开始就取得User的属性
onMounted(() => {
  userStore.getUser()
})

const handle***mand = async (key) => {
  if (key === 'logout') {
    // 退出操作
    await ElMessageBox.confirm('你确认要进行退出么', '温馨提示', {
      type: 'warning',
      confirmButtonText: '确认',
      cancelButtonText: '取消'
    })

    // 清除本地的数据 (token + user信息)
    userStore.removeToken() //移除key
    userStore.setUser({})
    router.push('/login')
  } else {
    // 跳转操作
    router.push(`/user/${key}`)
  }
}
</script>

<template>
  <!-- 
    el-menu 整个菜单组件
      :default-active="$route.path"  配置默认高亮的菜单项
      router  router选项开启,el-menu-item 的 index 就是点击跳转的路径

    el-menu-item 菜单项
      index="/article/channel" 配置的是访问的跳转路径,配合default-active的值,实现高亮
      如果路径一致,则要实现高亮
  -->
  <el-container class="layout-container">
    <!--左边导航栏的信息-->
    <el-aside width="200px">
      <div class="el-aside__logo"></div>
      <el-menu
        active-text-color="#ffd04b"
        background-color="#232323"
        :default-active="$route.path"
        text-color="#fff"
        router
      >
        <!--里面的模块分类-->
        <el-menu-item index="/article/channel">
          <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 index="/user">
          <!-- 多级菜单的标题 - 具名插槽 title -->
          <template #title>
            <el-icon><UserFilled /></el-icon>
            <span>个人中心</span>
          </template>

          <!--展开的内容 - 默认插槽 -->
          <el-menu-item index="/user/profile">
            <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/password">
            <el-icon><EditPen /></el-icon>
            <span>重置密码</span>
          </el-menu-item>
        </el-sub-menu>
      </el-menu>
    </el-aside>

    <!--右边的区域-->
    <el-container>
      <el-header>
        <div>
          黑马程序员:<strong>{{
            userStore.user.nickname || userStore.user.username
          }}</strong>
        </div>
        <el-dropdown placement="bottom-end" @***mand="handle***mand">
          <!-- 展示给用户,默认看到的 -->
          <span class="el-dropdown__box">
            <el-avatar :src="userStore.user.user_pic || avatar" />
            <el-icon><CaretBottom /></el-icon>
          </span>

          <!-- 折叠的下拉部分 -->
          <!-- 在图像下面有一个下拉框 -->
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item ***mand="profile" :icon="User"
                >基本资料</el-dropdown-item
              >
              <el-dropdown-item ***mand="avatar" :icon="Crop"
                >更换头像</el-dropdown-item
              >
              <el-dropdown-item ***mand="password" :icon="EditPen"
                >重置密码</el-dropdown-item
              >
              <el-dropdown-item ***mand="logout" :icon="SwitchButton"
                >退出登录</el-dropdown-item
              >
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-header>

      <!--路由来填充-->
      <el-main>
        <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>

src\api\user.js

  1. user作者路由的操作
import request from '@/utils/request'

// 注册接口
export const userRegisterService = ({ username, password, repassword }) =>
  request.post('/api/reg', { username, password, repassword })

// 登录接口
export const userLoginService = ({ username, password }) =>
  request.post('/api/login', { username, password })

// 获取用户基本信息→后端是存储到redis缓存的
export const userGetInfoService = () => request.get('/my/userinfo')

export const userUpdateInfoService = ({ id, nickname, email }) =>
  request.put('/my/userinfo', { id, nickname, email })

export const userUploadAvatarService = (avatar) =>
  request.patch('/my/update/avatar', { avatar })

export const userUpdatePassService = ({ old_pwd, new_pwd, re_pwd }) =>
  request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd })

user下面的小模块:实现改用户信息、图像、密码

src\***ponents\PageContainer.vue
  1. 设计插槽的方式:具名插槽、默认插槽
<script setup>
defineProps({
  title: {
    requiered: true,
    type: String
  }
})
</script>

<template>
  <el-card class="page-container">
    <template #header>
      <div class="header">
        <span>{{ title }}</span>

        <!--具名插槽:指定了一个名为 "extra" 的插槽,父组件可以通过这个名称将内容插入到这个插槽中-->
        <div class="extra">
          <slot name="extra"></slot>
        </div>
      </div>
    </template>
    <slot></slot>
  </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>
src\views\user\UserProfile.vue

<script setup>
import PageContainer from '@/***ponents/PageContainer.vue'
import { useUserStore } from '@/stores'
import { ref } from 'vue'
import { userUpdateInfoService } from '@/api/user'

const formRef = ref()

// 快速从useUserStore里面解析出方法、属性
// getUser快速实现更新的动作
const {
  user: { username, nickname, email, id, getUser }
} = useUserStore()

// 基于上面的字段进行初始化了
const userInfo = ref({ username, nickname, email, id })

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 onSubmit = async () => {
  // 进行全局的校验,
  const valid = await formRef.value.validate()
  if (valid) {
    await userUpdateInfoService(userInfo.value)

    //更新数据
    await getUser()
    ElMessage.su***ess('修改成功')
  }
}
</script>

<template>
  <page-container title="基本资料">
    <el-row>
      <el-col :span="12">
        <el-form
          :model="userInfo"
          :rules="rules"
          ref="formRef"
          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 @click="onSubmit" type="primary">提交修改</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </page-container>
</template>
src\views\user\UserAvatar.vue
  1. 学会了借用别人的功能: 借用了上面上传图片的功能

<script setup>
import PageContainer from '@/***ponents/PageContainer.vue'
import { ref } from 'vue'
import { Plus, Upload } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores'
import { userUploadAvatarService } from '../../api/user'

const userStore = useUserStore()
const uploadRef = ref()
const imgUrl = ref(userStore.user.user_pic)

// 选择更新图片
const onUploadFile = (file) => {
  const reader = new FileReader()
  reader.readAsDataURL(file.raw)
  reader.onload = () => {
    imgUrl.value = reader.result
  }
}

// 上传图片进行更新
const onUpdateAvatar = async () => {
  await userUploadAvatarService(imgUrl.value)

  //马上更新信息→更新图像,不用再进行刷新
  await userStore.getUser()
  ElMessage({ type: 'su***ess', message: '更换头像成功' })
}
</script>

<template>
  <page-container title="更换头像">
    <el-row>
      <el-col :span="12">
        <el-upload
          ref="uploadRef"
          class="avatar-uploader"
          :auto-upload="false"
          :show-file-list="false"
          :on-change="onUploadFile"
        >
          <img v-if="imgUrl" :src="imgUrl" class="avatar" />
          <img v-else src="@/assets/avatar.jpg" width="278" />
        </el-upload>

        <br />
        <!--借用了+号上传图片,借用了别人的功能
        借用了上面上传图片的功能-->
        <el-button
          @click="uploadRef.$el.querySelector('input').click()"
          type="primary"
          :icon="Plus"
          size="large"
        >
          选择图片
        </el-button>
        <el-button
          @click="onUpdateAvatar"
          type="su***ess"
          :icon="Upload"
          size="large"
        >
          上传头像
        </el-button>
      </el-col>
    </el-row>
  </page-container>
</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>
src\views\user\UserPassword.vue

<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores'
import { userUpdatePassService } from '@/api/user'

const formRef = ref()
const router = useRoute()
const userStore = useUserStore()

const pwdForm = ref({
  old_pwd: '',
  new_pwd: '',
  re_pwd: ''
})

const checkOldSame = (rule, value, cb) => {
  if (value === pwdForm.value.old_pwd) {
    cb(new Error('原密码和新密码不能一样!'))
  } else {
    cb()
  }
}

const checkNewSame = (rule, value, cb) => {
  if (value !== pwdForm.value.new_pwd) {
    cb(new Error('新密码和确认再次输入的新密码不一样!'))
  } else {
    cb()
  }
}
const rules = {
  // 原密码
  old_pwd: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    }
  ],
  // 新密码
  new_pwd: [
    { required: true, message: '请输入新密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    },
    { validator: checkOldSame, trigger: 'blur' }
  ],
  // 确认新密码
  re_pwd: [
    { required: true, message: '请再次确认新密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密码长度必须是6-15位的非空字符串',
      trigger: 'blur'
    },
    { validator: checkNewSame, trigger: 'blur' }
  ]
}

const onSubmit = async () => {
  const valid = await formRef.value.validate()
  if (valid) {
    await userUpdatePassService(pwdForm.value)
    ElMessage({ type: 'su***ess', message: '更换密码成功' })
    userStore.setToken('')
    userStore.setUser({})
    router.push('/login')
  }
}
const onReset = () => {
  formRef.value.reseetFields()
}
</script>
<template>
  <page-container title="重置密码">
    <el-row>
      <el-col :span="12">
        <el-form
          :model="pwdForm"
          :rules="rules"
          ref="formRef"
          label-width="100px"
          size="large"
        >
          <el-form-item label="原密码" prop="old_pwd">
            <el-input v-model="pwdForm.old_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item label="新密码" prop="new_pwd">
            <el-input v-model="pwdForm.new_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item label="确认新密码" prop="re_pwd">
            <el-input v-model="pwdForm.re_pwd" type="password"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button @click="onSubmit" type="primary">修改密码</el-button>
            <el-button @click="onReset">重置</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </page-container>
</template>

③文章的目录编辑:

src\***ponents\PageContainer.vue

  1. 也是往里面进行填充的
<script setup>
defineProps({
  title: {
    requiered: true,
    type: String
  }
})
</script>

<template>
  <el-card class="page-container">
    <template #header>
      <div class="header">
        <span>{{ title }}</span>

        <!--具名插槽:指定了一个名为 "extra" 的插槽,父组件可以通过这个名称将内容插入到这个插槽中-->
        <div class="extra">
          <slot name="extra"></slot>
        </div>
      </div>
    </template>
    <slot></slot>
  </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>
src\views\article\ArticleChannel.vue
<script setup>
import { ref } from 'vue'
import { Edit, Delete } from '@element-plus/icons-vue'
import { artGetChannelsService, artDelChannelService } from '../../api/article'
import ChannelEdit from './***ponents/ChannelEdit.vue'
const channelList = ref([])

// 是加载旋转的效果
const loading = ref(false)

//绑定了组件,可以获取里面的方法
const dialog = ref()

const getChannelList = async () => {
  loading.value = true
  const res = await artGetChannelsService()
  channelList.value = res.data.data
  loading.value = false
}

// 开始就调用获取数据
getChannelList()

// row是获取一行的数据
const onDelChannel = async (row) => {
  await ElMessageBox.confirm('你确认要删除该分类么', '温馨提示', {
    type: 'warning',
    confirmButtonText: '确认',
    cancelButtonText: '取消'
  })
  await artDelChannelService(row.id)
  ElMessage.su***ess('删除成功')
  getChannelList()
}

// 编辑文章分类要进行回显
const onEditChannel = (row) => {
  dialog.value.open(row)
}

// 添加文章分类不需要→调用子组件的方法
const onAddChannel = () => {
  dialog.value.open({})
}

// 在成功后,
const onSu***ess = () => {
  getChannelList()
}
</script>

<template>
  <page-container title="文章分类">
    <template #extra>
      <el-button @click="onAddChannel">添加分类</el-button>
    </template>

    <el-table v-loading="loading" :data="channelList" style="width: 100%">
      <el-table-column type="index" label="序号" width="100"></el-table-column>
      <el-table-column prop="cate_name" label="分类名称"></el-table-column>
      <el-table-column prop="cate_alias" label="分类别名"></el-table-column>
      <el-table-column label="操作" width="150">
        <!-- row 就是 channelList 的一项, $index 下标 -->
        <template #default="{ row, $index }">
          <el-button
            :icon="Edit"
            circle
            plain
            type="primary"
            @click="onEditChannel(row, $index)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            plain
            type="danger"
            @click="onDelChannel(row, $index)"
          ></el-button>
        </template>
      </el-table-column>

      <template #empty>
        <el-empty description="没有数据"></el-empty>
      </template>
    </el-table>

    <channel-edit ref="dialog" @su***ess="onSu***ess"></channel-edit>
  </page-container>
</template>

<style lang="scss" scoped></style>
src\views\article\***ponents\ChannelEdit.vue
  1. 进行文章分类的编辑→区分添加、编辑文章(编辑要进行回显)
<script setup>
import { ref } from 'vue'
import { artEditChannelService, artAddChannelService } from '@/api/article.js'

const dialogVisible = ref(false) //绑定窗口是否出现

const formRef = ref()

const formModel = ref({
  cate_name: '',
  cate_alias: '' //分类的别名
})
const rules = {
  cate_name: [
    { required: true, message: '请输入分类名称', trigger: 'blur' },
    {
      pattern: /^\S{1,10}$/,
      message: '分类名必须是 1-10 位的非空字符',
      trigger: 'blur'
    }
  ],
  cate_alias: [
    { required: true, message: '请输入分类别名', trigger: 'blur' },
    {
      pattern: /^[a-zA-Z0-9]{1,15}$/,
      message: '分类名必须是 1-15 位的字母或数字',
      trigger: 'blur'
    }
  ]
}

const emit = defineEmits(['su***ess']) //给父组件的方法,实现更新

const onSubmit = async () => {
  // 先进行全局的校验
  await formRef.value.validate()

  //区分编辑、添加
  const isEdit = formModel.value.id

  if (isEdit) {
    await artEditChannelService(formModel.value)
    ElMessage.su***ess('编辑成功')
  } else {
    await artAddChannelService(formModel.value)
    ElMessage.su***ess('添加成功')
  }
  //关闭弹层、
  dialogVisible.value = false
  //进行子传父
  emit('su***ess')
}

// 组件对外暴露一个方法 open,基于open传来的参数,区分添加还是编辑
// open({})  => 表单无需渲染,说明是添加
// open({ id, cate_name, ... })  => 表单需要渲染,说明是编辑
// open调用后,可以打开弹窗
const open = (row) => {
  dialogVisible.value = true
  // 使用了展开运算符,进行一一赋值
  formModel.value = { ...row } // 添加 → 重置了表单内容,编辑 → 存储了需要回显的数据
}

// 向外暴露方法
defineExpose({
  open
})
</script>

<template>
  <el-dialog
    v-model="dialogVisible"
    :title="formModel.id ? '编辑分类' : '添加分类'"
    width="30%"
  >
    <el-form
      ref="formRef"
      :model="formModel"
      :rules="rules"
      label-width="100px"
      style="padding-right: 30px"
    >
      <el-form-item label="分类名称" prop="cate_name">
        <el-input
          v-model="formModel.cate_name"
          placeholder="请输入分类名称"
        ></el-input>
      </el-form-item>

      <el-form-item label="分类别名" prop="cate_alias">
        <el-input
          v-model="formModel.cate_alias"
          placeholder="请输入分类别名"
        ></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="onSubmit"> 确认 </el-button>
      </span>
    </template>
  </el-dialog>
</template>

④文章的详情管理:

src\views\article\ArticleManage.vue

  1. 重点知识: <!--在vue2里面 v-model :value和@input的简写

在vue3里面 (v-model :modelValue)和@update:modelValue的简写

下面是要绑定的复选框,实现里面的选择的更新-->

<script setup>
import { Delete, Edit } from '@element-plus/icons-vue'
import { ref } from 'vue'
import { formatTime } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'

import { artDelService, artGetListService } from '../../api/article'
import ArticleEdit from './***ponents/ArticleEdit.vue'
import channelSelect from './***ponents/ChannelSelect.vue'

// 假数据
// const articleList = ref([
//   {
//     id: 5961,
//     title: '新的文章啊',
//     pub_date: '2022-07-10 14:53:52.604',
//     state: '已发布',
//     cate_name: '体育'
//   },
//   {
//     id: 5962,
//     title: '新的文章啊',
//     pub_date: '2022-07-10 14:54:30.904',
//     state: null,
//     cate_name: '体育'
//   }
// ])

// const onEditArticle = (row) => {
//   console.log(row)
// }

const onDeleteArticle = async (row) => {
  await ElMessageBox.confirm('你确认删除文章吗?', '温馨提示', {
    type: 'warning',
    confirmButtonText: '确认',
    cancelButtonText: '取消'
  })
  await artDelService(row.id)
  ElMessage({ type: 'su***ess', message: '删除成功了' })
  getArticleList()
}

const params = ref({
  pagenum: 1,
  pagesize: 5,
  cate_id: '', // 复选框绑定的默认值
  state: ''
})

const articleList = ref([])
const total = ref(0)
const loading = ref(false)

const getArticleList = async () => {
  loading.value = true
  const res = await artGetListService(params.value)
  articleList.value = res.data.data
  total.value = res.data.total
  console.log(res.data.data)
  loading.value = false //加载数据结束
}

// 开始进行初始化的数据
getArticleList()

// 处理分页的逻辑→每页数量发生改变
const onSizeChange = (size) => {
  // 只有是没页数变化了,那么原本正在访问的当前页面意义不大,数据大概率不再那一页了
  // 重新从第一页渲染即可
  params.value.pagenum = 1
  params.value.pagesize = size
  getArticleList()
}

// 处理到了第几页
const onCurrentChange = (page) => {
  params.value.pagenum = page

  // 基于最新页面,进行渲染数据
  getArticleList()
}

// 搜索逻辑=》按照最新的条件,重新检索,从第一页开始展示
const onSearch = () => {
  params.value.pagenum = 1 // 重置页面
  getArticleList()
}

// 重置逻辑→将筛选的条件清空,重新检索,从第一页开始展示
const onReset = () => {
  params.value.pagenum = 1
  params.value.cate_id = ''
  params.value.state = ''
  getArticleList()
}

// const visibleDrawer = ref(false)

// const onAddArticle = () => {
//   visibleDrawer.value = true
// }

// 封装抽屉
const articleEditRef = ref()

// 编辑新增逻辑
const onAddArticle = () => {
  articleEditRef.value.open({})
}

const onEditArticle = (row) => {
  articleEditRef.value.open(row)
}

// 父组件监听事件,重新渲染
const onSu***ess = (type) => {
  if (type === 'add') {
    const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)
    params.value.pagenum = lastPage
  }
  getArticleList()
}
</script>

<template>
  <page-container title="文章管理">
    <template #extra>
      <el-button type="primary" @click="onAddArticle">发布文章</el-button>
    </template>

    <el-form inline>
      <el-form-item label="文章分类:">
        <!--在vue2里面 v-model :value和@input的简写
            在vue3里面 (v-model :modelValue)和@update:modelValue的简写
            下面是要绑定的复选框,实现里面的选择的更新-->
        <channel-select width="100%" v-model="params.cate_id"></channel-select>
      </el-form-item>

      <el-form-item label="发布状态:">
        <el-select v-model="params.state">
          <el-option label="已发布" value="已发布"></el-option>
          <el-option label="草稿" value="草稿"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button @click="onSearch" type="primary">搜索</el-button>
        <el-button @click="onReset">重置</el-button>
      </el-form-item>
    </el-form>

    <!--下面的部分-->
    <el-table :data="articleList" style="width: 100%" v-loading="loading">
      <el-table-column label="文章标题" width="400">
        <!--里面是默认的链接插槽,row是一行的数据-->
        <template #default="{ row }">
          <el-link type="primary" :underline="false">{{ row.title }}</el-link>
        </template>
      </el-table-column>

      <el-table-column label="分类" prop="cate_name"></el-table-column>
      <el-table-column label="发表时间" prop="pub_date">
        <template #default="{ row }">
          {{ formatTime(row.pub_data) }}
        </template>
      </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="onEditArticle(row)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            plain
            type="danger"
            @click="onDeleteArticle(row)"
          ></el-button>
        </template>
      </el-table-column>

      <template #empty>
        <el-empty description="没有数据" />
      </template>
    </el-table>

    <!--分页模块-->
    <el-pagination
      v-model:current-page="params.pagenum"
      v-model:page-size="params.pagesize"
      :page-sizes="[2, 3, 4, 5, 10]"
      layout="jumper, total, sizes, prev, pager, next"
      background
      :total="total"
      @size-change="onSizeChange"
      @current-change="onCurrentChange"
      style="margin-top: 20px; justify-content: flex-end"
    />
  </page-container>

  <!-- 准备抽屉容器
  <el-drawer v-model="visibleDrawer" title="大标题" direction="rtl" size="50%">
    <span>Hi there!</span>
  </el-drawer> -->

  <!-- 弹窗 -->
  <article-edit ref="articleEditRef" @su***ess="onSu***ess"></article-edit>
</template>
src\views\article\***ponents\ChannelSelect.vue
  1. 选择复选框:vue3实现数据的双向绑定
<script setup>
import { artGetChannelsService } from '@/api/article'
import { ref } from 'vue'

// 父传子进行接收
defineProps({
  modelValue: {
    type: [Number, String]
  },
  width: {
    type: String
  }
})

// 子传父进行更新
const emit = defineEmits(['update:modelValue'])

const channelList = ref([])

const getChannelList = async () => {
  // 是获取文章的分类 channel
  const res = await artGetChannelsService()
  channelList.value = res.data.data
}

getChannelList()
</script>

<!--开始是没有选中的
    把$event里面选中的值,传给父类,实现复选框的更新→实现了双向的绑定-->
<template>
  <el-select
    :modelValue="modelValue"
    @update:modelValue="emit('update:modelValue', $event)"
    :style="{ width }"
  >
    <el-option
      v-for="channel in channelList"
      :key="channel.id"
      :label="channel.cate_name"
      :value="channel.id"
    ></el-option>
  </el-select>
</template>
src\views\article\***ponents\ArticleEdit.vue

<script setup>
import { ref } from 'vue'
import ChannelSelect from './channelselect.vue'
import { Plus } from '@element-plus/icons-vue'

import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import {
  artGetDetailService,
  artPublishService,
  artEditService
} from '../../../api/article'
import { ElMessage } from 'element-plus'

import { baseURL } from '@/utils/request'
import axios from 'axios'

const visibleDrawer = ref(false)
const defaultForm = {
  title: '',
  cate_id: '',
  cover_img: '',
  content: '',
  state: ''
}

const formModel = ref({ ...defaultForm })
const formRef = ref()
const editorRef = ref()

// 将网络图片地址转换为File对象
async function imageUrlToFile(url, fileName) {
  try {
    // 第一步:使用axios获取网络图片数据
    const response = await axios.get(url, { responseType: 'arraybuffer' })
    const imageData = response.data

    // 第二步:将图片数据转换为Blob对象
    const blob = new Blob([imageData], {
      type: response.headers['content-type']
    })

    // 第三步:创建一个新的File对象
    const file = new File([blob], fileName, { type: blob.type })

    return file
  } catch (error) {
    console.error('将图片转换为File对象时发生错误:', error)
    throw error
  }
}

const imgUrl = ref('')

const open = async (row) => {
  visibleDrawer.value = true
  if (row.id) {
    console.log('编辑回显')
    const res = await artGetDetailService(row.id)
    formModel.value = res.data.data
    // 图片需要单独处理进行回显
    imgUrl.value = baseURL + formModel.value.cover_img

    // 注意:提交给后台,需要的数据格式,是file对象的格式的
    // 需要将网络图片的地址→转换成为file对象,存储起来,方便提交
    formModel.value.cover_img = await imageUrlToFile(
      imgUrl.value,
      formModel.value.cover_img
    )
  } else {
    console.log('添加功能')
    formModel.value = { ...defaultForm }
    imgUrl.value = ''
    editorRef.value.setHTML('') //进行清空
  }
}

defineExpose({
  open
})

// 图片上传逻辑
// onSelectFile是element提供的函数,改变的钩子,图片上传
const onUploadFile = (uploadFile) => {
  console.log(uploadFile)
  // 预览图片
  imgUrl.value = URL.createObjectURL(uploadFile.raw)
  // 立刻将图片对象转入提交
  formModel.value.cover_img = uploadFile.raw
}

const emit = defineEmits(['su***ess'])

const onPublish = async (state) => {
  // 将已发布还是草稿状态,存入 state
  formModel.value.state = state

  // 转换 formData 数据
  const fd = new FormData()
  for (let key in formModel.value) {
    fd.append(key, formModel.value[key])
  }

  if (formModel.value.id) {
    if (formModel.value.id) {
      await artEditService(fd)
      ElMessage.su***ess('编辑成功')
      visibleDrawer.value = false
      emit('su***ess', 'edit')
    } else {
      // 添加请求
      await artPublishService(fd)
      ElMessage.su***ess('添加成功')
      visibleDrawer.value = false
      emit('su***ess', 'add')
    }
  }
}
</script>

<template>
  <el-drawer
    v-model="visibleDrawer"
    :title="formModel.id ? '编辑文章' : '添加文章'"
    direction="rtl"
    size="50%"
  >
    <!-- 发表文章表单 -->
    <el-form :model="formModel" ref="formRef" label-width="100px">
      <el-form-item label="文章标题" prop="title">
        <el-input v-model="formModel.title" placeholder="请输入标题"></el-input>
      </el-form-item>

      <el-form-item label="文章分类" prop="cate_id">
        <channel-select
          v-model="formModel.cate_id"
          width="100%"
        ></channel-select>
      </el-form-item>

      <!--此处不需要element-plus自动上传,不需要配置action参数
      只需要做前端的本地预览即可,无需提交前上传图片,以免取消,浪费空间
      语法:URL.createObjectURL(...)创建本地预览的地址,来预览-->
      <el-form-item label="文章封面" prop="cover_img">
        <el-upload
          class="avatar-uploader"
          :auto-upload="false"
          :show-file-list="false"
          :on-change="onUploadFile"
        >
          <img v-if="imgUrl" :src="imgUrl" class="avatar" />
          <el-icon v-else class="avatar-uploader-icon">
            <Plus />
          </el-icon>
        </el-upload>
      </el-form-item>

      <el-form-item label="文章内容" prop="content">
        <div class="editor">
          <div class="editor">
            <quill-editor
              ref="editorRef"
              theme="snow"
              v-model:content="formModel.content"
              contentType="html"
            >
            </quill-editor>
          </div>
        </div>
      </el-form-item>

      <el-form-item>
        <el-button @click="onPublish('已发布')" type="primary">发布</el-button>
        <el-button @click="onPublish('草稿')" type="info">草稿</el-button>
      </el-form-item>
    </el-form>
  </el-drawer>
</template>

<style lang="scss" scoped>
.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>
src\utils\format.js
import { dayjs } from 'element-plus'

export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')

⑤全局的路由:

import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores'

// createRouter 创建路由实例
// 配置 history 模式
// 1. history模式:createWebHistory     地址栏不带 #
// 2. hash模式:   createWebHashHistory 地址栏带 #
// console.log(import.meta.env.DEV)

// vite 中的环境变量 import.meta.env.BASE_URL  就是 vite.config.js 中的 base 配置项
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/login', ***ponent: () => import('@/views/login/LoginPage.vue') }, // 登录页
    {
      path: '/',
      ***ponent: () => import('@/views/layout/LayoutContainer.vue'),
      redirect: '/article/manage',
      children: [
        {
          path: '/article/manage',
          ***ponent: () => import('@/views/article/ArticleManage.vue')
        },
        {
          path: '/article/channel',
          ***ponent: () => import('@/views/article/ArticleChannel.vue')
        },
        {
          path: '/user/profile',
          ***ponent: () => import('@/views/user/UserProfile.vue')
        },
        {
          path: '/user/avatar',
          ***ponent: () => import('@/views/user/UserAvatar.vue')
        },
        {
          path: '/user/password',
          ***ponent: () => import('@/views/user/UserPassword.vue')
        }
      ]
    }
  ]
})

// 登录访问拦截 => 默认是直接放行的
// 根据返回值决定,是放行还是拦截
// 返回值:
// 1. undefined / true  直接放行
// 2. false 拦回from的地址页面
// 3. 具体路径 或 路径对象  拦截到对应的地址
//    '/login'   { name: 'login' }
router.beforeEach((to) => {
  // 如果没有token, 且访问的是非登录页,拦截到登录,其他情况正常放行
  const useStore = useUserStore()
  if (!useStore.token && to.path !== '/login') return '/login'
})

export default router

转载请说明出处内容投诉
CSS教程_站长资源网 » 全栈开发前端代码:黑马程序员SpringBoot3+Vue3全套视频教程,springboot+vue企业级全栈开,big-event

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买