『VUE3后台—大事件管理系统(已完结)』

news2025/1/11 7:12:15

项目地址:https://gitee.com/csheng-gitee/vue3-big-event-admin
技术栈:VUE3 + Pinia + Pnpm(本项目暂不用 typescript)


一、前期准备工作

1、创建项目

npm install -g pnpm
pnpm create vue

在这里插入图片描述


2、ESLint 配置

  • (1) 禁用 prettier 插件,下载 ESLint 插件;
  • (2) 在 vscode 的 setting.json 设置自动修复:
  // 开启 eslint 自动修复
  "editor.codeActionsOnSave": {
    "source.fixAll": true
  },
  // 关闭保存自动格式化
  "editor.formatOnSave": false,
  • (3) 在项目根目录的 .gitignore.cjs 配置 ESLint 规则:
  rules: {
    'prettier/prettier': [
      'warn',
      {
        singleQuote: true, // 单引号
        semi: false, // 无分号
        printWidth: 80, // 每行宽度至多80字符
        trailingComma: 'none', // 不加对象|数组最后逗号
        endOfLine: 'auto' // 换行符号不限制(win mac 不一致)
      }
    ],
    'vue/multi-word-component-names': [
      'warn',
      {
        ignores: ['index'] // vue组件名称多单词组成(忽略index.vue)
      }
    ],
    'vue/no-setup-props-destructure': ['off'], // 关闭 props 解构的校验
    // 添加未定义变量错误提示,create-vue@3.6.3 关闭,这里加上是为了支持下一个章节演示。
    'no-undef': 'error'
  }

3、husky 代码检查

  • (1) husky 配置:
    此操作须在 bash 窗口输入(&&符号)
pnpm dlx husky-init && pnpm install

修改 .husky/pre-commit 文件

pnpm lint

缺点:默认进行的是全量检查,耗时问题,历史问题。继续步骤2

  • (2) lint-staged 配置:
pnpm i lint-staged -D

配置 package.json

{ 
  "scripts": {
    ....
    "lint-staged": "lint-staged"
  },
  ....
  "lint-staged": {
    "*.{js,ts,vue}": [
      "eslint --fix"
    ]
  }
}

修改 .husky/pre-commit 文件

pnpm lint-staged

4、目录调整

(1)删除 目录下的所有文件:

在这里插入图片描述

(2)修改:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

(3)新增 目录结构

在这里插入图片描述

(4)拷贝全局样式和图片,并且安装预处理器支持

在这里插入图片描述

在这里插入图片描述

pnpm add sass -D

5、VueRouter4 路由语法(可忽略)

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

// createWebHashHistory:hash 模式 / createWebHistory:history 模式
// import.meta.env.BASE_URL :vite.config.js 中的 base 基地址配置
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: []
})

export default router
// vite.config.js
export default defineConfig({
  ....
  base: '/', // 基地址
  ....
})

6、按需引入 Element-Plus

安装 Element-Plus:

pnpm install element-plus

按需导入:

pnpm add -D unplugin-vue-components unplugin-auto-import
// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
...

export default defineConfig({
  plugins: [
    ...
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  ....
})

引入组件:

<!-- App.vue -->
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>

彩蛋:components 文件夹下的组件会自动注册:

<!-- components/TestDemo.vue -->
<test-demo />

7、Pinia 构建用户仓库和持久化

  • (1) 构建用户仓库
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('big-user', () => {
  const token = ref('')
  const setToken = (newToken) => (token.value = newToken)

  return { token, setToken }
})
<!-- App.vue -->
<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
</script>

<template>
  <div>
    <p>token:{{ userStore.token }}</p>
    <el-button type="primary" @click="userStore.setToken('Bearer csheng')"
      >登录</el-button
    >
    <el-button type="success" @click="userStore.setToken('')">退出</el-button>
  </div>
</template>

<style scoped></style>
  • (2) 持久化
pnpm add pinia-plugin-persistedstate -D
// main.js
....
import persist from 'pinia-plugin-persistedstate'
....
app.use(createPinia().use(persist))
...
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore(
  'big-user',
  () => {
    const token = ref('')
    const setToken = (newToken) => (token.value = newToken)

    return { token, setToken }
  },
  {
    persist: true
  }
)
  • (3) 配置仓库统一管理
// 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'
// stores/modules/user.js
原代码不变,仅路径更改了
// main.js
....
import pinia from '@/stores'

const app = createApp(App)

app.use(pinia)
app.use(router)
// App.vue
import { useUserStore } from '@/stores'

8、数据交互 - 请求工具设计

pnpm add axios

新建 utils/request.js

import axios from 'axios'

const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
})

instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    return config
  },
  (err) => Promise.reject(err)
)

instance.interceptors.response.use(
  (res) => {
    // TODO 3. 处理业务失败
    // TODO 4. 摘取核心响应数据
    return res
  },
  (err) => {
    // TODO 5. 处理401错误
    return Promise.reject(err)
  }
)

export default instance

完整版:

import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import router from '@/router'

const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基础地址,超时时间
  baseURL,
  timeout: 10000
})

// 请求拦截器
instance.interceptors.request.use(
  (config) => {
    // TODO 2. 携带token
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = userStore.token
    }
    return config
  },
  (err) => Promise.reject(err)
)

// 响应拦截器
instance.interceptors.response.use(
  (res) => {
    // TODO 4. 摘取核心响应数据
    if (res.data.code === 0) {
      return res
    }
    // TODO 3. 处理业务失败(错误提示,抛出错误)
    ElMessage.error(res.data.message || '服务异常')
    return Promise.reject(res.data)
  },
  (err) => {
    // TODO 5. 处理401错误(权限不足 或 token 过期)
    if (err.response?.status === 401) {
      router.push('/login')
    }

    // 错误的默认情况(只要给提示)
    ElMessage.error(err.response.data.message || '服务异常')
    return Promise.reject(err)
  }
)

export default instance
export { baseURL }

9、整体路由设计

path文件功能路由级别
/loginviews/login/LoginPage.vue登录&注册一级路由
/views/layout/LayoutContainer.vue布局架子一级路由
├─ /article/manageviews/article/ArticleManage.vue文章管理2级路由
├─ /article/channelviews/article/ArticleChannel.vue频道管理2级路由
├─ /user/profileviews/user/UserProfile.vue个人详情2级路由
├─ /user/avatarviews/user/UserAvatar.vue更换头像2级路由
├─ /user/passwordviews/user/UserPassword.vue重置密码2级路由
  • (1) 新建文件:

在这里插入图片描述

  • (2) 配置路由规则:
// router/index.js
routes: [
  {
    path: '/login',
    component: () => import('@/views/login/LoginPage.vue')
  },
  {
    path: '/',
    component: () => import('@/views/layout/LayoutContainer.vue'),
    redirect: '/article/manage',
    children: [
      {
        path: '/article/manage',
        component: () => import('@/views/article/ArticleManage.vue')
      },
      {
        path: '/article/channel',
        component: () => import('@/views/article/ArticleChannel.vue')
      },
      {
        path: '/user/profile',
        component: () => import('@/views/user/UserProfile.vue')
      },
      {
        path: '/user/avatar',
        component: () => import('@/views/user/UserAvatar.vue')
      },
      {
        path: '/user/password',
        component: () => import('@/views/user/UserPassword.vue')
      }
    ]
  }
]
<!-- App.vue -->
<script setup></script>

<template>
  <router-view></router-view>
</template>

<style scoped></style>
<!-- layout/LayoutContainer -->
<template>
  <div>
    <p>布局架子</p>
    <router-view></router-view>
  </div>
</template>

二、登录注册&layout架子

1、登录&注册

静态结构&基本切换

安装 element-plus 图标库:

pnpm install @element-plus/icons-vue

静态结构准备:

<!-- login/LoginPage.vue -->
<script setup>
import { User, Lock } from '@element-plus/icons-vue'
import { ref } from 'vue'

const isRegister = ref(true) // 是否注册
</script>

<template>
  <el-row class="login-page">
    <el-col :span="12" class="bg"></el-col>
    <el-col :span="6" :offset="3" class="form">
      <!-- 注册表单 -->
      <el-form ref="form" size="large" autocomplete="off" v-if="isRegister">
        <el-form-item>
          <h1>注册</h1>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入密码"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            :prefix-icon="Lock"
            type="password"
            placeholder="请输入再次密码"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button 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 ref="form" size="large" autocomplete="off" v-else>
        <el-form-item>
          <h1>登录</h1>
        </el-form-item>
        <el-form-item>
          <el-input :prefix-icon="User" placeholder="请输入用户名"></el-input>
        </el-form-item>
        <el-form-item>
          <el-input
            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 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>

表单校验

<!-- login/LoginPage.vue -->
<script setup>
...
// 注册Form信息
const formModel = ref({
  username: '',
  password: '',
  repassword: ''
})
// rules规则
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) => {
        if (value !== formModel.value.password) {
          callback(new Error('两次输入密码不一致!'))
        } else {
          callback()
        }
      },
      trigger: 'blur'
    }
  ]
}
...
</script>

<template>
	<el-form :model="formModel" :rules="rules" ....>
	   <el-form-item prop="username">
          <el-input v-model="formModel.username"></el-input>
       </el-form-item>
       ....
    </el-form>
</template>

注册功能

// api/user.js
import request from '@/utils/request'

// 注册
export const userRegisterService = ({ username, password, repassword }) =>
  request.post('/api/reg', { username, password, repassword })
// login/LoginPage.vue
import { userRegisterService } from '@/api/user'
....
const form = ref() // form表单实例
....
// 注册
const register = async () => {
  await form.value.validate()
  await userRegisterService(formModel.value)
  ElMessage.success('注册成功')
  isRegister.value = false
}

/************************************************/

<el-button @click="register" ....>注册</el-button>
// .eslintrc.cjs
module.exports = {
  ....
  globals: {
    ElMessage: 'readonly',
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
  }
}

登录功能

// api/user.js
// 登录
export const userLoginService = ({ username, password }) =>
  request.post('/api/login', { username, password })
// login/LoginPage.vue
....
// isRegister 每次变化都清空表单
watch(isRegister, () => {
  formModel.value = {
    username: '',
    password: '',
    repassword: ''
  }
})

// 登录
const userStore = useUserStore()
const router = useRouter()
const login = async () => {
  await form.value.validate()
  
  // 加载效果
  const loading = ElLoading.service({
    lock: true,
    text: '登录中',
    background: 'rgba(0, 0, 0, 0.7)'
  })
  const res = await userLoginService(formModel.value)
  loading.close() // 关闭加载效果
  
  userStore.setToken(res.data.token)
  ElMessage.success('登录成功')
  router.push('/')
}

/************************************************/

<el-button @click="login" ....>登录</el-button>

登录访问拦截

// router/index.js
// 登录访问拦截
router.beforeEach((to) => {
  const userStore = useUserStore()
  if (!userStore.token && to.path !== '/login') return '/login'
})

记住账号密码(补充)

pnpm add js-base64
pnpm add js-cookie
import { Base64 } from 'js-base64'
import Cookies from 'js-cookie'

const isRemember = ref(false) // 是否记住账号密码

// 登录
const login = async () => {
  await form.value.validate()
  
  const loading = ElLoading.service({
    lock: true,
    text: '登录中',
    background: 'rgba(0, 0, 0, 0.7)'
  })
  const res = await userLoginService(formModel.value)
  loading.close()

  // 如果记住密码
  if (isRemember.value) {
    // 转码
    const { username, password } = formModel.value
    const localForm = {
      username: Base64.encode(username),
      password: Base64.encode(password)
    }
    // 存到 Cookies
    Cookies.set('LOCAL_KEY', JSON.stringify(localForm))
  } else {
    Cookies.remove('LOCAL_KEY')
  }

  userStore.setToken(res.data.token)
  ElMessage.success('登录成功')
  router.push('/')
}

// 查询是否存了用户名和密码
onMounted(() => {
  const localForm = Cookies.get('LOCAL_KEY')
  if (localForm) {
    isRemember.value = true
    // 解码回填
    try {
      const { username, password } = JSON.parse(localForm)
      formModel.value.username = Base64.decode(username)
      formModel.value.password = Base64.decode(password)
    } catch (error) {
      console.error('本地数据解析失败~', error)
    }
  } else {
    isRemember.value = false
  }
})

/*---------------------------------------------------*/

<el-checkbox v-model="isRemember">记住我</el-checkbox>

2、layout 架子

<!-- 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'
</script>

<template>
  <!-- 容器 -->
  <el-container class="layout-container">
    <!-- 左侧 -->
    <el-aside width="200px">
      <!-- logo -->
      <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">
          <!-- 一级 -->
          <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>陈升</strong></div>
        <!-- 最右 -->
        <el-dropdown placement="bottom-end">
          <span class="el-dropdown__box">
            <el-avatar :src="avatar" />
            <el-icon><CaretBottom /></el-icon>
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item command="profile" :icon="User"
                >基本资料</el-dropdown-item
              >
              <el-dropdown-item command="avatar" :icon="Crop"
                >更换头像</el-dropdown-item
              >
              <el-dropdown-item command="password" :icon="EditPen"
                >重置密码</el-dropdown-item
              >
              <el-dropdown-item command="logout" :icon="SwitchButton"
                >退出登录</el-dropdown-item
              >
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-header>
      <!-- 主要内容 -->
      <el-main>
        <router-view></router-view>
      </el-main>
      <el-footer>大事件 ©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 {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background-color: #fff;
    .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>

3、用户基本信息获取&渲染

// api/user.js
// 获取用户基本信息
export const userGetInfoService = () => request.get('/my/userinfo')
// stores/modules/user.js
import { userGetInfoService } from '@/api/user'
....
const user = ref({})
const getUser = async () => {
  const res = await userGetInfoService()
  user.value = res.data.data
}
// layout/LayoutContainer.vue
import { useUserStore } from '@/stores'
import { onMounted } from 'vue'

const userStore = useUserStore()
onMounted(() => {
  userStore.getUser()
})

<div>当前用户:<strong>{{userStore.user.nickname || userStore.user.username}}</strong></div>
<el-avatar :src="userStore.user.user_pic || avatar" />

4、退出登录

// stores/modules/user.js
const setUser = (newUser) => (user.value = newUser)
<!-- layout/LayoutContainer -->
<script setup>
....
// 点击下拉项
const router = useRouter()
const onCommand = async (command) => {
  if (command === 'logout') {
    await ElMessageBox.confirm('你确认退出大事件吗?', '温馨提示', {
      type: 'warning',
      confirmButtonText: '确认',
      cancelButtonText: '取消'
    })
    userStore.setToken('')
    userStore.setUser({})
    router.push('/login')
  } else {
    router.push(`/user/${command}`)
  }
}
</script>

<el-dropdown @command="onCommand">....

三、文章分类

1、PageContainer 封装

<!-- components/PageContainer.vue -->
<script setup>
defineProps({
  title: { required: true, type: String }
})
</script>

<template>
  <el-card class="page-container">
    <template #header>
      <div class="header">
        <span>{{ title }}</span>
        <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>
<!-- article/ArticleChannel.vue -->
<script setup></script>

<template>
  <PageContainer title="文章分类">
    <template #extra>
      <el-button type="primary">添加分类</el-button>
    </template>

    主体部分
  </PageContainer>
</template>
<!-- article/ArticleManage.vue -->
<script setup></script>

<template>
  <PageContainer title="文章管理">
    <template #extra>
      <el-button type="primary">添加文章</el-button>
    </template>

    主体部分
  </PageContainer>
</template>

2、文章分类渲染

// api/article.js
import request from '@/utils/request'

// 获取文章分类信息
export const artGetChannelsService = () => request.get('/my/cate/list')
<!-- article/ArticleChannel.vue -->
<script setup>
import { artGetChannelsService } from '@/api/article'
import { ref } from 'vue'

// 获取文章分类数据
const loading = ref(false)
const channelList = ref([])
const getChannelList = async () => {
  loading.value = true
  const res = await artGetChannelsService()
  channelList.value = res.data.data
  loading.value = false
}
getChannelList()

// 编辑
const onEditChannel = (row) => {
  console.log(row)
}
// 删除
const onDelChannel = (row) => {
  console.log(row)
}
</script>

<template>
  <PageContainer title="文章分类">
    ....

    <el-table v-loading="loading" :data="channelList" style="width: 100%">
      <el-table-column label="序号" width="100" type="index"> </el-table-column>
      <el-table-column label="分类名称" prop="cate_name"></el-table-column>
      <el-table-column label="分类别名" prop="cate_alias"></el-table-column>
      <el-table-column label="操作" width="100">
        <template #default="{ row }">
          <el-button
            :icon="Edit"
            circle
            plain
            type="primary"
            @click="onEditChannel(row)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            plain
            type="danger"
            @click="onDelChannel(row)"
          ></el-button>
        </template>
      </el-table-column>
      <template #empty>
        <el-empty description="没有数据" />
      </template>
    </el-table>
  </PageContainer>
</template>

3、文章分类添加/编辑/删除

// api/article.js
// 添加文章分类
export const artAddChannelService = (data) => request.post('/my/cate/add', data)
// 编辑文章分类
export const artEditChannelService = (data) =>
  request.put('/my/cate/info', data)
// 删除文章分类
export const artDelChannelService = (id) =>
  request.delete('/my/cate/del', { params: { id } })

四、文章管理

1、文章列表渲染

<!-- article/ArticleManage.vue -->
<script setup>
import { Delete, Edit } from '@element-plus/icons-vue'
import { ref } from '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 = (row) => {
  console.log(row)
}
</script>

<template>
  <PageContainer title="文章管理">
    <template #extra>
      <el-button type="primary">添加文章</el-button>
    </template>

    <!-- 筛选项 -->
    <el-form inline>
      <el-form-item label="文章分类:">
        <el-select>
          <el-option label="新闻" value="111"></el-option>
          <el-option label="体育" value="222"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="发布状态:">
        <el-select>
          <el-option label="已发布" value="已发布"></el-option>
          <el-option label="草稿" value="草稿"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary">搜索</el-button>
        <el-button>重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 表格区域 -->
    <el-table :data="articleList" style="width: 100%">
      <el-table-column label="文章标题" width="400">
        <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"></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>
  </PageContainer>
</template>

2、中英国际化处理

<!-- App.vue -->
<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>

<template>
  <el-config-provider :locale="zhCn">
    <router-view></router-view>
  </el-config-provider>
</template>

<style scoped></style>

3、文章分类选择

<!-- article/components/ChannelSelect.vue -->
<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 () => {
  const res = await artGetChannelsService()
  channelList.value = res.data.data
}
getChannelList()
</script>

<template>
  <el-select
    :modelValue="modelValue"
    :style="{ width }"
    @update:modelValue="emit('update:modelValue', $event)"
  >
    <el-option
      v-for="item in channelList"
      :key="item.id"
      :label="item.cate_name"
      :value="item.id"
    ></el-option>
  </el-select>
</template>

<style lang="scss" scoped></style>
<!-- article/components/ArticleManage.vue -->
<script setup>
....
import ChannelSelect from './components/ChannelSelect.vue'

// 列表请求参数
const params = ref({
  pagenum: 1,
  pagesize: 5,
  cate_id: '',
  state: ''
})
....
</script>

<template>
	<channel-select v-model="params.cate_id"></channel-select>
	....
    <el-form-item label="发布状态:">
       <el-select v-model="params.state">...</el-select>
    </el-form-item>
</template>

4、渲染文章列表

// api/article.js
// 获取文章列表
export const artGetListService = (params) =>
  request.get('/my/article/list', { params })
// utils/format.js
import { dayjs } from 'element-plus'

export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')
// article/components/ArticleManage.vue
import { artGetListService } from '@/api/article'
import { formatTime } from '@/utils/format'

....
const articleList = ref([]) // 文章列表
const total = ref(0) // 总条数

// 获取用户列表
const getArticleList = async () => {
  const res = await artGetListService(params.value)
  articleList.value = res.data.data
  total.value = res.data.total
}
getArticleList()

/*---------------------------------------------*/

<el-table-column label="发布时间">
   <template #default="{ row }">
     {{ formatTime(row.pub_date) }}
   </template>
</el-table-column>

5、分页

const loading = ref(false)

// 获取用户列表
const getArticleList = async () => {
  loading.value = true
  const res = await artGetListService(params.value)
  loading.value = false
  ....
}
// 分页
const onSizeChange = (size) => {
  params.value.pagenum = 1
  params.value.pagesize = size
  getArticleList()
}
const onCurrentChange = (page) => {
  params.value.pagenum = page
  getArticleList()
}

/*---------------------------------------------*/

<el-table v-loading="loading" ....

<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"
/>

6、搜索&重置

// 搜索和重置
const onSearch = () => {
  params.value.pagenum = 1
  getArticleList()
}
const onReset = () => {
  params.value.pagenum = 1
  params.value.cate_id = ''
  params.value.state = ''
  getArticleList()
}

/*---------------------------------------------------------*/

<el-button @click="onSearch" type="primary">搜索</el-button>
<el-button @click="onReset">重置</el-button>

7、新增/编辑打开抽屉

新建 article/components/ArticleEdit.vue

<script setup>
import { ref } from 'vue'
import ChannelSelect from './ChannelSelect.vue'

// 默认表单(初始化)
const defaultForm = {
  title: '',
  cate_id: '',
  cover_img: '',
  content: '',
  state: ''
}
// 抽屉表单(备份)
const formModel = ref({
  title: '',
  cate_id: '',
  cover_img: '',
  content: '',
  state: ''
})

// 抽屉
const visibleDrawer = ref(false)
const open = async (row) => {
  visibleDrawer.value = true
  if (row?.id) {
    console.log('编辑回显')
  } else {
    console.log('新增回显')
    formModel.value = { ...defaultForm.value }
  }
}

defineExpose({ open })
</script>

<template>
  <!-- 抽屉 -->`
  <el-drawer
    v-model="visibleDrawer"
    :title="formModel.id ? '编辑文章' : '添加文章'"
    direction="rtl"
    size="35%"
  >
    <!-- 发表文章表单 -->
    <el-form ref="formRef" :model="formModel" 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%" />
      </el-form-item>
      <el-form-item label="文章封面" prop="cover_img">文件上传</el-form-item>
      <el-form-item label="文章内容" prop="content">
        <div class="editor">富文本编辑器</div>
      </el-form-item>
      <el-form-item>
        <el-button type="primary">发布</el-button>
        <el-button type="info">草稿</el-button>
      </el-form-item>
    </el-form>
  </el-drawer>
</template>

<style lang="scss" scoped></style>

article/ArticleMange.vue

<script setup>
const articleEditRef = ref() // 抽屉
// 新增文章
const onAddArticle = () => {
  articleEditRef.value.open()
}
// 编辑文章
const onEditArticle = (row) => {
  articleEditRef.value.open(row)
}
</script>

<template>
  ...
  <!-- 抽屉 -->
  <article-edit ref="articleEditRef"></article-edit>
</template>

8、文件上传

article/components/ArticleEdit.vue

<script setup>
import { Plus } from '@element-plus/icons-vue'
....
// 文件上传
const imgUrl = ref('')
const onUploadFile = (uploadFile) => {
  // 外观
  imgUrl.value = URL.createObjectURL(uploadFile.raw)
  // 实际赋值位置
  formModel.value.cover_img = uploadFile.raw
}
</script>

<template>
  ....
  <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>
  ....
</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;
    }
  }
}
</style>

9、富文本编辑器 vue-quill

pnpm add @vueup/vue-quill@latest

article/components/ArticleEdit.vue

<script setup>
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
....
</script>

<template>
        ....
        <div class="editor">
          <quill-editor
            theme="snow"
            v-model:content="formModel.content"
            contentType="html"
          />
        </div>
</template>

<style lang="scss" scoped>
....
.editor {
  width: 100%;
  :deep(.ql-editor) {
    min-height: 200px;
  }
}
</style>

10、添加/编辑文章

api/article.js

// 添加文章
export const artPublishService = (data) => request.post('/my/article/add', data)

// 获取文章详情
export const artGetDetailService = (id) =>
  request.get('my/article/info', { params: { id } })

// 编辑文章
export const artEditService = (data) => request.put('my/article/info', data)

article/ArticleManage.vue

<script setup>
// 添加/修改 成功后的操作
const onSuccess = (type) => {
  if (type === 'add') {
    // 添加完毕,更新跳转到渲染最后一页;编辑完毕,渲染当前页
    const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)
    params.value.pagenum = lastPage
  }
  getArticleList()
}
</script>

<template>
  <article-edit ref="articleEditRef" @success="onSuccess"></article-edit>
</template>

article/ArticleEdit.vue

<script setup>
import {
  artPublishService,
  artGetDetailService,
  artEditService
} from '@/api/article'
import { ElMessage } from 'element-plus'
import { baseURL } from '@/utils/request'
import axios from 'axios'
import { nextTick } from 'vue'

const editorRef = ref()
const open = async (row) => {
  visibleDrawer.value = true
  if (row?.id) {
    const res = await artGetDetailService(row.id)
    formModel.value = res.data.data
    imgUrl.value = baseURL + formModel.value.cover_img
    // 将 在线地址 转换成 file 格式
    formModel.value.cover_img = await imageUrlToFile(
      imgUrl.value,
      formModel.value.cover_img
    )
  } else {
    formModel.value = { ...defaultForm.value }
    imgUrl.value = ''
    nextTick(() => {
      editorRef.value.setHTML('')
    })
  }
}
// 将网络图片地址转换为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 emit = defineEmits(['success'])
// 发布/草稿
const onPublish = async (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) {
    await artEditService(fd)
    ElMessage.success('编辑成功')
    visibleDrawer.value = false
    emit('success', 'edit')
  } else {
    await artPublishService(fd)
    ElMessage.success('添加成功')
    visibleDrawer.value = false
    emit('success', 'add')
  }
}
</script>

<template>
	<quill-editor ref="editorRef" />
	<el-button @click="onPublish('已发布')" type="primary">发布</el-button>
	<el-button @click="onPublish('草稿')" type="info">草稿</el-button>
</template>

11、删除文章

api/article.js

export const artDelService = (id) =>
  request.delete('my/article/info', { params: { id } })

article/ArticleManage.vue

import { ElMessageBox } from 'element-plus'
import { artDelService } from '@/api/article'

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

五、个人中心

1、基本资料

api/user.js

// 更新用户基本信息
export const userUpdateInfoService = ({ id, nickname, email }) =>
  request.put('/my/userinfo', { id, nickname, email })

user/UserProfile.vue

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

// pinia 用户数据
const {
  user: { nickname, email, username, 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 formRef = ref()
const onSumbit = async () => {
  const valid = await formRef.value.validate()
  if (valid) {
    await userUpdateInfoService(userInfo.value)
    await getUser()
    ElMessage.success('修改成功')
  }
}
</script>

<template>
  <page-container title="基本资料">
    <el-row>
      <el-col :span="12">
        <el-form
          ref="formRef"
          :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="onSumbit">提交修改</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </page-container>
</template>

2、更换头像

api/user.js

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

user/UserAvatar.vue

<script setup>
import PageContainer from '@/components/PageContainer.vue'
import { Plus, Upload } from '@element-plus/icons-vue'
import { ref } from '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) => {
  // 基于 FileReader 读取图片做预览
  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.success('更换头像成功')
}
</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
          type="success"
          :icon="Upload"
          size="large"
          @click="onUpdateAvatar"
        >
          上传头像
        </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>

3、重置密码

api/user.js

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

user/UserPassword.vue

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

const formRef = ref()
const pwdForm = ref({
  old_pwd: '',
  new_pwd: '',
  re_pwd: ''
})
const checkOldSame = (rule, value, callback) => {
  if (value === pwdForm.value.old_pwd) {
    callback(new Error('新密码不能与原密码相同'))
  } else {
    callback()
  }
}
const checkNewSame = (rule, value, callback) => {
  if (value !== pwdForm.value.new_pwd) {
    callback(new Error('确认密码与新密码不一致'))
  } else {
    callback()
  }
}
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 userStore = useUserStore()
const router = useRouter()
const onSubmit = async () => {
  const valid = await formRef.value.validate()
  if (valid) {
    await userUpdatePassService(pwdForm.value)
    ElMessage.success('更换密码成功')
    // 清空token和user信息,并且返回登录页
    userStore.setToken('')
    userStore.setUser({})
    router.push('/login')
  }
}
// 重置密码
const onReset = () => {
  formRef.value.resetFields()
}
</script>

<template>
  <page-container title="重置密码">
    <el-row>
      <el-col :span="12">
        <el-form
          ref="formRef"
          :model="pwdForm"
          :rules="rules"
          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 type="primary" @click="onSubmit">修改密码</el-button>
            <el-button @click="onReset">重置</el-button>
          </el-form-item>
        </el-form>
      </el-col>
    </el-row>
  </page-container>
</template>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1282231.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

做销售的时候为什么你的内心会恐惧?

做销售的时候为什么你的内心会恐惧&#xff1f; 做销售的时候&#xff0c;很多人的内心会感到恐惧。这种恐惧可能来自于对自己业绩的担忧&#xff0c;或者是对被拒绝的恐惧。但是&#xff0c;恐惧并不是我们该有的心态。在销售中&#xff0c;我们需要保持自信和冷静&#xff0…

线上超市小程序可以做什么活动_提升用户参与度与购物体验

标题&#xff1a;线上超市小程序&#xff1a;精心策划活动&#xff0c;提升用户参与度与购物体验 一、引言 随着移动互联网的普及&#xff0c;线上购物已经成为人们日常生活的一部分。线上超市作为线上购物的重要组成部分&#xff0c;以其便捷、快速、丰富的商品种类和个性化…

直击2023云栖大会-大模型时代到来:“计算,为了无法计算的价值”

2023年的云栖大会以“计算&#xff0c;为了无法计算的价值”为主题&#xff0c;强调了计算技术在现代社会中的重要性&#xff0c;特别是在大模型时代到来的背景下。 大模型时代指的是以深度学习为代表的人工智能技术的快速发展&#xff0c;这些技术需要大量的计算资源来训练和优…

机器学习决策树ID3算法

1、先去计算总的信息量 2、根据不同指标分别计算对应的信息增益 3、根据算出的信息增益来选择信息增益最大的作为根结点 4、天气中选择一个继续上述过程 5、决策树划分结束

基于Springboot的秒杀系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的秒杀系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xf…

服务器数据恢复—ocfs2文件系统被格式化为其他文件系统如何恢复数据?

服务器故障&#xff1a; 由于工作人员的误操作&#xff0c;将Ext4文件系统误装入到存储中Ocfs2文件系统数据卷上&#xff0c;导致原Ocfs2文件系统被格式化为Ext4文件系统。 由于Ext4文件系统每隔几百兆就会写入文件系统的原始信息&#xff0c;原Ocfs2文件系统数据会遭受一定程度…

电脑监控软件是隐藏安装吗?

电脑监控软件通常可以隐藏安装。 这种类型的软件可能是通过企业管理者下载或拷贝到员工的电脑上的。因为程序包比较小&#xff0c;安装过程也比较简单&#xff0c;所以操作起来也很方便。 企业管理者的这种操作基本上是为了更好管控公司的电脑运行、防止员工恶意泄露公司的机密…

行业分析:2023年智能自动化药房市场现状及发展前景

医药电商是近些年的行业风口&#xff0c;尤其是随着大型互联网平台的介入和互联网医院的兴起&#xff0c;医药电商步入高速增长期。第三方交易服务平台在医药电商的销售额占比为58%&#xff0c;而到了2020年下降至40%。在终端销售额中&#xff0c;大型医院占据了59.7%的份额&am…

【字符串匹配】【KMP算法】Leetcode 28 找出字符串中第一个匹配项的下标☆

【字符串匹配】【KMP算法】Leetcode 28 找出字符串中第一个匹配项的下标 &#xff08;1&#xff09;前缀和后缀&#xff08;2&#xff09;前缀表&#xff08;最长相同的前缀和后缀的长度&#xff09;&#xff08;3&#xff09;匹配过程示意&#xff08;4&#xff09;next数组的…

2024年天津中德应用技术大学专升本专业课报名及考试时间通知

天津中德应用技术大学2024年高职升本科专业课报名确认及考试通知 按照市高招办《2024年天津市高职升本科招生实施办法》&#xff08;津招办高发〔2023〕14号&#xff09;文件要求&#xff0c;天津中德应用技术大学制定了2024年高职升本科专业课考试报名、确认及考试实施方案&a…

【开源】基于Vue.js的人事管理系统

文末获取源码&#xff0c;项目编号&#xff1a; S 079 。 \color{red}{文末获取源码&#xff0c;项目编号&#xff1a;S079。} 文末获取源码&#xff0c;项目编号&#xff1a;S079。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 管理员功能模块2.2 普通员工功能模块…

【JVM系列】Class文件分析

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

IDEA2023找不到 Allow parallel run

我的idea版本&#xff1a;2023.1.4 第一步&#xff1a;点击Edit Configrations 第二步&#xff1a;点击Modify options 第三步&#xff1a;勾选Allow multiple instances 最后点击Apply应用一下 ok,问题解决&#xff01;

直播的种类及类型

随着网络技术和移动设备的普及&#xff0c;直播已经成为人们娱乐、学习、商业交流等众多领域的重要工具。 直播的种类主要有以下几种: 1.视频直播:这是最常见的直播形式&#xff0c;包括电商直播、婚庆直播、培训直播、家居直播等。 2.图文直播:这种直播形式包括PPT互动直播…

22、为什么是卷积?

(本文已加入“计算机视觉入门与调优”专栏,点击专栏查看更多文章信息) 我们先看一看神经网络(或者叫一个AI模型),是如何完成一张图片的推理的。 你肯定听说过阿尔法狗大战柯洁的故事,当时新闻一出,不知大家什么反应,反正我是被震撼到了。机器竟然学到了那么多的棋谱,…

FreeRTOS-任务通知

任务通知 使用队列、信号量、事件组等方法时&#xff0c;无法知道发送方身份。使用任务通知时&#xff0c;可以明确指定&#xff1a;通知哪个任务。 优势 效率更高。 使用任务通知来发送事件、数据给某个任务时&#xff0c;效率更高。比队列、信号量、事件组都有优势。 更节省内…

2024年甘肃省职业院校技能大赛(中职教师组)网络安全竞赛样题卷④

2024年甘肃省职业院校技能大赛&#xff08;中职教师组&#xff09;网络安全竞赛样题卷④ 2024年甘肃省职业院校技能大赛&#xff08;中职教师组&#xff09;网络安全竞赛样题卷④A模块基础设施设置/安全加固&#xff08;本模块200分&#xff09;A-1任务一 登录安全加固&#xf…

父类的@Autowired字段被继承后能否被注入

可以 示例 父类&#xff1a;Animal.class public class Animal {Autowiredprivate PrometheusAlertService prometheusAlertService;public void eat(){System.out.println("eat food");}} 子类&#xff1a;Dog.class Service public class Dog extends Animal …

园区无线覆盖方案(智慧园区综合解决方案)

​ 李经理正苦恼头疼的工业园区数字化改造项目。近年企业快速增长,园区内Argent工业设备激增,IT部门应接不暇。为确保生产系统稳定运行,IT管理团队经过反复摸索,决定进行全面的数字化升级。然而改造之艰巨远超想象——混杂的接入环境、复杂的专线部署、长达数月的建设周期,种种…

接口获取数据控制台打印有值但是展开又没有了

谷歌浏览器只会展现响应式数据最后的结果&#xff0c;证明原来接口是有值的&#xff0c;后面对这个数据进行操作后&#xff0c;最终没有值了。所以对数据进行操作时最好对数据进行一次深拷贝 JSON.parse(JSON.stringify(data))