vue3实战Easy云盘(一):创建项目+登录注册页面+构建框架页+上传头像/修改密码/退出登录

一、创建项目

1.创建项目 

 2.安装各种依赖

npm install 
@highlightjs/vue-plugin
@moefe/vue-aplayer
aplayer 
axios 
docx-preview 
dplayer 
element-plus 
highlight.js 
js-md5 
sass 
sass-loader 
spark-md5 
vue-clipboard3 
vue-cookies 
vue-pdf-embed 
vue-router 
vue3-pdfjs 
xlsx 
--save

3.修改端口号

vite.config.js

server: {
    port: 1024,
    hmr: true,
    proxy: {
      '/api': {
        target: 'http://localhost:7090',
        changeOrigin: true,
        pathRewrite: {
          '^api': '/api',
        },
      },
    },
  },

4.引入各项安装

main.js


import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
//引入element plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//图标图标在附件中
import '@/assets/icon/iconfont.css'
import '@/assets/base.scss'
// 引入cookies
import { VueCookies } from 'vue-cookies'
const app = createApp(App)

app.use(router)
app.use(ElementPlus)
app.mount('#app')

index.html

<script src="/easypan-front/public/hls.min.js"></script>
    <title>uu云盘</title>

二、登录注册页面

可以免费下载图片: undraw

(需要前后端源码的可以评论或者私信),这一部分记录的不全面

src/views/Login.vue

<template>
  <div class="login-body">
    <div class="bg"></div>
    <div class="login-panel">
      <el-form 
        class="login-register"
        :model="formData" 
        :rules="rules" 
        ref="formDataRef"  
        @submit.prevent>
        <div class="login-title">uu云盘</div>
        <!-- input输入 -->
        <el-form-item prop="email">
          <el-input
           clearable
           size="large"
           placeholder="请输入邮箱"
           v-model.trim="formData.email"
           maxLength="150"
           >
            <!-- 插入图标 -->
             <template #prefix>
              <span class="iconfont icon-account"></span>
             </template>
          </el-input>
        </el-form-item>
        <!--登录密码 -->
         <el-form-item  prop="password" v-if="opType==1">
          <el-input
           type="password"
           size="large"
           v-model="formData.password" 
           placeholder="请输入密码" 
           show-password
          >
          <!-- 插入图标 -->
            <template #prefix>
              <span class="iconfont icon-password"></span>
            </template>
          </el-input>
        </el-form-item>

        <!-- 注册 -->
        <div v-if="opType==0||opType==2">

          <!-- 邮箱验证码 -->
          <el-form-item prop="emailCode">
            <div class="send-email-panel">
              <el-input v-model.trim="formData.emailCode" placeholder="请输入邮箱验证码" size="large" clearable>
                <template #prefix>
                  <span class="iconfont icon-checkcode"></span>
                </template>
              </el-input>
            <el-button class="send-mail-btn" type="primary" size="large" @click="getEmailCode">获取验证码</el-button>
            </div>
            <!-- 气泡框 -->
            <el-popover placement="left" :width="500" trigger="click">
              <div>
                <p>1、在垃圾箱中查找邮箱验证码</p>
                <p>2、在邮箱中头像->设置->反垃圾->白名单->设置邮件地址白名单</p>
                <p>
                  3、将邮箱【laoluo@wuhancoder.com】添加到白名单不知道怎么设置?
                </p>
              </div>
              <template #reference>
                <span class="a-link" :style="{ 'font-size': '14px' }"
                  >未收到邮箱验证码?</span
                >
              </template>
            </el-popover>
          </el-form-item>

          <!-- 昵称,注册时0才有昵称 -->
          <el-form-item prop="nickName" v-if="opType == 0">
              <el-input
                size="large"
                clearable
                placeholder="请输入昵称"
                v-model.trim="formData.nickName"
                maxLength="20"
              >
                <template #prefix>
                  <span class="iconfont icon-account"></span>
                </template>
              </el-input>
          </el-form-item>

          <!-- 输入密码 -->
          <!-- 注册密码,找回密码 -->
          <el-form-item prop="registerPassword">
            <el-input
              type="password"
              size="large"
              placeholder="请输入密码"
              v-model.trim="formData.registerPassword"
              show-password
            >
              <template #prefix>
                <span class="iconfont icon-password"></span>
              </template>
            </el-input>
          </el-form-item>
          
          <!-- 再次输入密码 -->
          <el-form-item prop="reRegisterPassword">
              <el-input
                type="password"
                size="large"
                placeholder="请再次输入密码"
                v-model.trim="formData.reRegisterPassword"
                show-password
              >
                <template #prefix>
                  <span class="iconfont icon-password"></span>
                </template>
              </el-input>
            </el-form-item>

        </div>

        <!-- 验证码 -->
        <el-form-item prop="checkCode">
          <div class="check-code-panel">
            <el-input
              size="large"
              placeholder="请输入验证码"
              v-model="formData.checkCode"
              @keyup.enter="doSubmit"
            >
            <!-- 插入图标 -->
            <template #prefix>
              <span class="iconfont icon-checkcode"></span>
            </template>
            </el-input>
            <img :src="checkCodeUrl" class="check-code" @click="changeCheckCode(0)">
          </div>
        </el-form-item>
        <!-- 登录 -->
        <el-form-item v-if="opType==1">
          <div class="rememberme-panel">
            <el-checkbox v-model="formData.rememberMe">记住我</el-checkbox>
          </div>
          <div class="no-account">
            <a href="javascript:void(0)" class="a-link" @click="showPanel(2)">忘记密码?</a>
            <a href="javascript:void(0)" class="a-link" @click="showPanel(0)">没有账号?</a>
          </div>
        </el-form-item>
        <!-- 找回密码时2想起密码,去登陆?点击去登陆1 -->
        <el-form-item v-if="opType == 2">
            <a href="javascript:void(0)" class="a-link" @click="showPanel(1)">去登陆?</a>
        </el-form-item>
        <!-- 注册时0,想起已有账号?点击去登陆1 -->
        <el-form-item v-if="opType == 0">
            <a href="javascript:void(0)" class="a-link" @click="showPanel(1)">已有账号?</a>
        </el-form-item>
        <!-- 注册登录重置按钮 -->
        <el-form-item>
          <el-button class="op-btn" type="primary" size="large" @click="doSubmit">
            <span v-if="opType == 0">注册</span>
            <span v-if="opType == 1">登录</span>
            <span v-if="opType == 2">重置密码</span>
          </el-button>
        </el-form-item>
        <!-- qq登录 -->
        <div class="login-btn-qq" v-if="opType == 1">
            快捷登录<img src="@/assets/qq.png" @click="qqLogin" />
          </div>
      </el-form>
    </div>
    <Dialog
      :show="dialogConfig4SendMailCode.show"
      :title="dialogConfig4SendMailCode.title"
      :buttons="dialogConfig4SendMailCode.buttons"
      width="500px"
      :showCancel="false"
      @close="dialogConfig4SendMailCode.show = false">
      <el-form
          :model="formData4SendMailCode"
          :rules="rules"
          ref="formData4SendMailCodeRef"
          label-width="80px"
        >
          <!--展示邮箱-->
          <el-form-item label="邮箱">
            {{ formData.email }}
          </el-form-item>

          <!--验证码输入-->
          <el-form-item label="验证码" prop="checkCode">
            <div class="check-code-panel">
              <el-input
                size="large"
                placeholder="请输入验证码"
                v-model.trim="formData4SendMailCode.checkCode"
              >
                <template #prefix>
                  <span class="iconfont icon-checkcode"></span>
                </template>
              </el-input>
              <img
                :src="checkCodeUrl4SendMailCode"
                class="check-code"
                @click="changeCheckCode(1)"
              />
            </div>
          </el-form-item>
        </el-form>
  </Dialog>
  </div>
</template>

<script setup>
import { ref, reactive, getCurrentInstance,nextTick,onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import md5 from "js-md5";
// import axios from 'axios';

const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();
// 定义api
const api = {
  checkCode:'/api/checkCode',
  sendMailCode: "/sendEmailCode",
  register: "/register",
  login: "/login",
  resetPwd: "/resetPwd",
  qqlogin: "/qqlogin",
}
// 操作类型 0:注册   1:登录   2:重置密码
const opType = ref(1)
const showPanel=(type)=>{
  opType.value=type
  resetForm();
}
onMounted(() => {
  showPanel(1);
});
// 校验再次输入的密码
const checkRePassword = (rule, value, callback) => {
  if (value !== formData.value.registerPassword) {
    callback(new Error(rule.message));
  } else {
    callback();
  }
};
// 登录页面
const formData = ref({});
const formDataRef = ref();
// 校验规则
const rules = {
  email: [
    { required: true, message: "请输入邮箱" },
    { validator: proxy.Verify.email, message: "请输入正确的邮箱" },
  ],
  password: [{ required: true, message: "请输入密码" }],
  emailCode: [{ required: true, message: "请输入邮箱验证码" }],
  nickName: [{ required: true, message: "请输入昵称" }],
  registerPassword: [
    { required: true, message: "请输入密码" },
    {
      validator: proxy.Verify.password,
      message: "密码只能是数字,字母,特殊字符 8-18位",
    },
  ],
  reRegisterPassword: [
    { required: true, message: "请再次输入密码" },
    {
      validator: checkRePassword,
      message: "两次输入的密码不一致",
    },
  ],
  checkCode: [{ required: true, message: "请输入图片验证码" }],
};
// 连接后台,显示验证码
const checkCodeUrl = ref(api.checkCode)
const checkCodeUrl4SendMailCode = ref(api.checkCode)
// 验证码变化
const changeCheckCode=(type)=>{
  if(type==0){
    checkCodeUrl.value =
    api.checkCode + "?type=" + type + "&time=" + new Date().getTime();
  }else{
    checkCodeUrl4SendMailCode.value =
      api.checkCode + "?type=" + type + "&time=" + new Date().getTime();

  }
}
// 注册界面 发送邮箱验证码 定义属性
const formData4SendMailCode = ref({});
const formData4SendMailCodeRef = ref();
const dialogConfig4SendMailCode = reactive({
  show: false,
  title: "发送邮箱验证码",
  buttons: [
    {
      type: "primary",
      text: "发送验证码",
      click: (e) => {
        sendEmailCode();
        // submitForm()
      },
    },
  ],
});
// 获取邮箱验证码
const getEmailCode = () => {
  formDataRef.value.validateField("email", (valid) => {
    if (!valid) {
      return;
    }
    dialogConfig4SendMailCode.show = true;
    // 清空验证码
    nextTick(() => {
      changeCheckCode(1);
      formData4SendMailCodeRef.value.resetFields();
      formData4SendMailCode.value = {
        email: formData.value.email,
      };
    });
  })
}
// 发送邮箱验证码
// 0:注册 1:找回密码
const sendEmailCode = () => {
  formData4SendMailCodeRef.value.validate(async (valid) => {
    if (!valid) {
      return;
    }
    const params = Object.assign({}, formData4SendMailCode.value);
    params.type = opType.value == 0 ? 0 : 1;
    let result = await proxy.Request({
      url: api.sendMailCode,
      params: params,
      errorCallback: () => {
        changeCheckCode(1);
      },
    });
    if (!result) {
      return;
    }
    proxy.Message.success("验证码发送成功,请登录邮箱查看");
    dialogConfig4SendMailCode.show = false;
  })
}

// 重置表单(清空表单)
const resetForm = () => {
  nextTick(() => {
    changeCheckCode(0);
    formDataRef.value.resetFields();
    formData.value = {};

    // 登录
    if (opType.value == 1) {
      const cookieLoginInfo = proxy.VueCookies.get("loginInfo");
      if (cookieLoginInfo) {
        formData.value = cookieLoginInfo;
      }
    }
  });
};
// 登录、注册、重置、提交表单
const doSubmit = () => {
  formDataRef.value.validate(async (valid) => {
    if (!valid) {
      return;
    }
    let params = {};
    Object.assign(params, formData.value);
    // 注册
    if (opType.value == 0 || opType.value == 2) {
      params.password = params.registerPassword;
      delete params.registerPassword;
      delete params.reRegisterPassword;
    }

    // 登录
    if (opType.value == 1) {
      let cookieLoginInfo = proxy.VueCookies.get("loginInfo");
      let cookiePassword =
        cookieLoginInfo == null ? null : cookieLoginInfo.password;
      // 现在的密码和原来的密码不相等的情况下,对当前密码进行md5加密
      if (params.password !== cookiePassword) {
        params.password = md5(params.password);
      }
    }

    // 发送http请求
    let url = null;
    if (opType.value == 0) {
      url = api.register;
    } else if (opType.value == 1) {
      url = api.login;
    } else if (opType.value == 2) {
      url = api.resetPwd;
    }

    let result = await proxy.Request({
      url: url,
      params: params,
      errorCallback: () => {
        changeCheckCode(0);
      },
    });
    if (!result) {
      return;
    }

    // 注册返回
    if (opType.value == 0) {
      proxy.Message.success("注册成功,请登录");
      showPanel(1);
    } else if (opType.value == 1) {
      // 检查是否点击 “记住我”
      if (params.rememberMe) {
        const loginInfo = {
          email: params.email,
          password: params.password,
          rememberMe: params.rememberMe,
        };
        // 将存储七天
        proxy.VueCookies.set("loginInfo", loginInfo, "7d");
      } else {
        proxy.VueCookies.remove("loginInfo");
      }
      proxy.Message.success("登录成功");
      // 存储cookie
      proxy.VueCookies.set("userInfo", result.data, 0);
      // 重定向到原始页面
      const redirectUrl = route.query.redirectUrl || "/";
      router.push(redirectUrl);
    } else if (opType.value == 2) {
      // 重置密码
      proxy.Message.success("重置密码成功,请登录");
      showPanel(1);
    }
  });
};
// qq登录
const qqLogin = async () => {
  let result = await proxy.Request({
    url: api.qqlogin,
    params: {
      callbackUrl: route.query.redirectUrl || "",
    },
  });
  if (!result) return;
  proxy.VueCookies.remove("userInfo");
  document.location.href = result.data;
};

</script>

<style lang="scss" scoped>
.login-body {
  height: calc(100vh);
  // 把背景图像扩展至足够大,以使背景图像完全覆盖背景区域。
  background-size: cover;
  background: url("../assets/login_bg.jpg");
  display: flex;

  .bg {
    flex: 1;
    background-size: cover;
    background-position: center;
    background-size: 800px;
    background-repeat: no-repeat;
    background-image: url("../assets/login_img.png");
  }

  .login-panel {
    width: 430px;
    margin-right: 15%;
    margin-top: calc((100vh - 500px) / 2);

    .login-register {
      padding: 25px;
      background: #fff;
      border-radius: 5px;

      .login-title {
        text-align: center;
        font-size: 18px;
        font-weight: bold;
        margin-bottom: 20px;
      }

      .send-email-panel {
        display: flex;
        width: 100%;
        justify-content: space-between;

        .send-mail-btn {
          margin-left: 5px;
        }
      }

      .rememberme-panel {
        width: 100%;
      }

      .no-account {
        width: 100%;
        display: flex;
        justify-content: space-between;
      }

      .op-btn {
        width: 100%;
      }
    }
  }

  .check-code-panel {
    width: 100%;
    display: flex;

    .check-code {
      margin-left: 5px;
      cursor: pointer;
    }
  }

  .login-btn-qq {
    margin-top: 20px;
    text-align: center;
    display: flex;
    align-items: center;
    justify-content: center;

    img {
      cursor: pointer;
      margin-left: 10px;
      width: 20px;
    }
  }
}
</style>

三、构建框架页 

(1)构建基本框架

src/views/Framework.vue

<template>
    <div class="framework">
        <!-- 头部 -->
        <div class="header">
            <!-- 左上角logo -->
            <div class="logo">
                <span class="iconfont icon-pan"></span>
                <div class="name">uu云盘</div>
            </div>
            <!-- 右侧消息弹框 -->
            <div class="right-panel">
                <!-- 气泡框 -->
                <el-popover :width="800" trigger="click" :v-model:visible="showUploader" :offset="20" transition="none"
                    :hide-after="0" :popper-style="{ padding: '0px' }">
                    <template #reference>
                        <span class="iconfont icon-transfer"></span>
                    </template>
                    <template #default>
                        <Uploader ref="uploaderRef" @uploadCallback="uploadCallbackHandler"></Uploader>
                    </template>
                </el-popover>
                <!-- 下拉框 -->
                <el-dropdown>
                    <!-- 用户信息 -->
                    <div class="user-info">
                        <!-- 头像 -->
                        <div class="avatar"></div>
                        <!-- 昵称 -->
                        <span class="nick-name">{{ userInfo.nickName }}</span>
                    </div>
                    <template #dropdown>
                        <el-dropdown-menu>
                            <el-dropdown-item>修改头像</el-dropdown-item>
                            <el-dropdown-item>修改密码</el-dropdown-item>
                            <el-dropdown-item>退出</el-dropdown-item>
                        </el-dropdown-menu>
                    </template>
                </el-dropdown>
            </div>
        </div>
        <!-- 主体 -->
        <div class="body">
            <!-- 最左侧菜单栏 一级目录 -->
            <div class="left-sider">
                <div class="menu-list">
                    <div :class="['menu-item',item.menuCode==currentMenu.menuCode?'active':'']" v-for="item in menus" @click="jump(item)">
                        <!-- 一级菜单图标 -->
                        <div :class="['iconfont','icon-'+item.icon]"></div>
                        <!-- 一级菜单名字 -->
                        <div class="text">{{ item.name }}</div>
                    </div>
                </div>
                <!-- 二级菜单目录 -->
                <div class="menu-sub-list">
                    <div :class="[' menu-item-sub',currentPath==sub.path?'active':'']" v-for="sub in currentMenu.children" @click="jump(sub)">
                        <!-- 图标 -->
                        <span :class="['iconfont', 'icon-' + sub.icon]" v-if="sub.icon"></span>
                        <!-- 名字 -->
                        <span class="text">{{ sub.name }}</span>
                    </div>
                    <div class="tips" v-if="currentMenu && currentMenu.tips">{{ currentMenu.tips }}</div>
                    <!-- 下方空间使用 -->
                    <div class="space-info">
                        <div>空间使用</div>
                        <div class="percent"></div>
                    </div>
                </div>
            </div>
            <!-- 中间主体内容 -->
            <div class="body-content">
                <router-view v-slot="{ Component }">
                    <component :is="Component"></component>
                </router-view>
            </div>
        </div>
    </div>
</template>

<script setup>
import {
    ref,
    reactive,
    getCurrentInstance,
    watch,
    nextTick,
    computed,
} from "vue";
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
const route = useRoute();
const {proxy} = getCurrentInstance();
const userInfo = ref({
    nickName:'张三'
});
// 菜单栏
const menus = [
    {
        icon: "cloude",
        name: "首页",
        menuCode: "main",
        path: "/main/all",
        allShow: true,
        children: [
            {
                icon: "all",
                name: "全部",
                category: "all",
                path: "/main/all",
            },
            {
                icon: "video",
                name: "视频",
                category: "video",
                path: "/main/video",
            },
            {
                icon: "music",
                name: "音频",
                category: "music",
                path: "/main/music",
            },
            {
                icon: "image",
                name: "图片",
                category: "image",
                path: "/main/image",
            },
            {
                icon: "doc",
                name: "文档",
                category: "doc",
                path: "/main/doc",
            },
            {
                icon: "more",
                name: "其他",
                category: "others",
                path: "/main/others",
            },
        ],
    },
    {
        path: "/myshare",
        icon: "share",
        name: "分享",
        menuCode: "share",
        allShow: true,
        children: [
            {
                name: "分享记录",
                path: "/myshare",
            },
        ],
    },
    {
        path: "/recycle",
        icon: "del",
        name: "回收站",
        menuCode: "recycle",
        tips: "回收站为你保存10天内删除的文件",
        allShow: true,
        children: [
            {
                name: "删除的文件",
                path: "/recycle",
            },
        ],
    },
    {
        path: "/settings/fileList",
        icon: "settings",
        name: "设置",
        menuCode: "settings",
        allShow: false,
        children: [
            {
                name: "用户文件",
                path: "/settings/fileList",
            },
            {
                name: "用户管理",
                path: "/settings/userList",
            },
            {
                path: "/settings/sysSetting",
                name: "系统设置",
            },
        ],
    },
];
const currentMenu = ref({});
const currentPath = ref();

// 点击一级菜单栏跳转事件回调
const jump = (data) =>{
    // 判断如果没有路径或者点击的还是当前的路径就不跳转
    if(!data.path||data.menuCode==currentMenu.value.menuCode){
        return;
    }
    // 否则就跳转到data.path
    router.push(data.path);
}
// 设置当前菜单栏
const setMenu = (menuCode, path) => {
    const menu = menus.find((item) => {
        return item.menuCode === menuCode;
    });
    currentMenu.value = menu;
    currentPath.value = path;
};
// 监听
watch(
    () => route,
    (newVal, oldVal) => {
        if (newVal.meta.menuCode) {
            setMenu(newVal.meta.menuCode, newVal.path);
        }
    },
    { immediate: true, deep: true }
);
</script>

<style lang="scss" scoped>
.header {
    box-shadow: 0 3px 10px 0 rgb(0 0 0 / 6%);
    height: 56px;
    padding-left: 24px;
    padding-right: 24px;
    position: relative;
    z-index: 200;
    display: flex;
    align-items: center;
    justify-content: space-between;

    .logo {
        display: flex;
        align-items: center;

        .icon-pan {
            font-size: 40px;
            color: #1296db;
        }

        .name {
            font-weight: bold;
            margin-left: 5px;
            font-size: 25px;
            color: #05a1f5;
        }
    }

    .right-panel {
        display: flex;
        align-items: center;

        .icon-transfer {
            cursor: pointer;
        }

        .user-info {
            margin-right: 10px;
            display: flex;
            align-items: center;
            cursor: pointer;

            // 头像
            .avatar {
                margin: 0px 5px 0px 15px;
            }

            // 昵称
            .nick-name {
                color: #05a1f5;
            }
        }
    }
}

.body {
    display: flex;

    .left-sider {
        border-right: 1px solid #f1f2f4;
        display: flex;

        .menu-list {
            height: calc(100vh - 56px);
            width: 80px;
            box-shadow: 0 3px 10px 0 rgb(0 0 0 / 6%);
            border-right: 1px solid #f1f2f4;

            .menu-item {
                text-align: center;
                font-size: 14px;
                font-weight: bold;
                padding: 20px 0px;
                cursor: pointer;

                &:hover {
                    background: #f3f3f3;
                }

                .iconfont {
                    font-weight: normal;
                    font-size: 28px;
                }
            }

            .active {
                .iconfont {
                    color: #06a7ff;
                }

                .text {
                    color: #06a7ff;
                }
            }
        }

        .menu-sub-list {
            width: 200px;
            padding: 20px 10px 0px;
            position: relative;

            .menu-item-sub {
                text-align: center;
                line-height: 40px;
                border-radius: 5px;
                cursor: pointer;

                &:hover {
                    background: #f3f3f3;
                }

                .iconfont {
                    font-size: 14px;
                    margin-right: 20px;
                }

                .text {
                    font-size: 13px;
                }
            }

            .active {
                background: #eef9fe;

                .iconfont {
                    color: #05a1f5;
                }

                .text {
                    color: #05a1f5;
                }
            }

            .tips {
                margin-top: 10px;
                color: #888888;
                font-size: 13px;
            }

            .space-info {
                position: absolute;
                bottom: 10px;
                width: 100%;
                padding: 0px 5px;

                .percent {
                    padding-right: 10px;
                }

                .space-use {
                    margin-top: 5px;
                    color: #7e7e7e;
                    display: flex;
                    justify-content: space-around;

                    .use {
                        flex: 1;
                    }

                    .iconfont {
                        cursor: pointer;
                        margin-right: 20px;
                        color: #05a1f5;
                    }
                }
            }
        }
    }

    .body-content {
        flex: 1;
        width: 0;
        padding-left: 20px;
    }
}
</style>

(2)添加路由
./src/router/index.js

{
      path: '/',
      name: 'Framework',
      component: ()=> import('@/views/Framework.vue'),
      children: [{
                    path: '/',
                    redirect: "/main/all"
                },
                {
                    path: '/main/:category',
                    name: '首页',
                    meta: {
                        needLogin: true,
                        menuCode: "main"
                    },
                    component: () =>
                        import ("@/views/main/Main.vue")
                },
                {
                    path: '/myshare',
                    name: '我的分享',
                    meta: {
                        needLogin: true,
                        menuCode: "share"
                    },
                    component: () =>
                        import ("@/views/share/Share.vue")
                },
                {
                    path: '/recycle',
                    name: '回收站',
                    meta: {
                        needLogin: true,
                        menuCode: "recycle"
                    },
                    component: () =>
                        import ("@/views/recycle/Recycle.vue")
                },
                {
                    path: '/settings/sysSetting',
                    name: '系统设置',
                    meta: {
                        needLogin: true,
                        menuCode: "settings"
                    },
                    component: () =>
                        import ("@/views/admin/SysSettings.vue")
                },
                {
                    path: '/settings/userList',
                    name: '用户管理',
                    meta: {
                        needLogin: true,
                        menuCode: "settings"
                    },
                    component: () =>
                        import ("@/views/admin/UserList.vue")
                },
                {
                    path: '/settings/fileList',
                    name: '用户文件',
                    meta: {
                        needLogin: true,
                        menuCode: "settings"
                    },
                    component: () =>
                        import ("@/views/admin/FileList.vue")
                },
            ]
    },

(3)添加如下组件


(4)main.js引入使用
import Avatar from '@/components/Avatar.vue'
(5)./src/components/Avatar.vue

<template>
  <!-- 头像组件 -->
  <span class="avatar">
    <img :src="avatar && avatar != ''
        ? avatar
        : `${proxy.globalInfo.avatarUrl}${userId}?${timestamp}`
      " v-if="userId" />
  </span>
</template>

<script setup>
import { getCurrentInstance } from "vue";
// 获取当前组件实例
const { proxy } = getCurrentInstance();

const props = defineProps({
  userId: {
    type: String,
  },
  avatar: {
    type: String,
  },
  timestamp: {
    type: Number,
    default: 0,
  },
  width: {
    type: Number,
    default: 40,
  },
});
</script>

<style lang="scss" scoped>
.avatar {
  display: flex;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  overflow: hidden;

  img {
    width: 100%;
    object-fit: cover;
  }
}
</style>

四、上传头像

(1)封装UpdateAvatar.vue组件
./src/views/UpdateAvatar.vue

<template>
  <div>
    <!-- 修改头像弹出框 -->
    <Dialog
      :show="dialogConfig.show"
      :title="dialogConfig.title"
      :buttons="dialogConfig.buttons"
      width="500px"
      :showCancel="true"
      @close="dialogConfig.show = false"
    >
      <el-form
        :model="formData"
        ref="formDataRef"
        label-width="80px"
        @submit.prevent
      >
        <!--显示昵称-->
        <el-form-item label="昵称">
          {{ formData.nickName }}
        </el-form-item>

        <!--显示头像-->
        <el-form-item label="头像">
          <AvatarUpload v-model="formData.avatar"></AvatarUpload>
        </el-form-item>
      </el-form>
    </Dialog>
  </div>
</template>




<script setup>
// 引入头像上传组件
import AvatarUpload from "@/components/AvatarUpload.vue";
import { ref, reactive, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";

const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();

const api = {
  updateUserAvatar: "updateUserAvatar",
};

const formData = ref({});
const formDataRef = ref();

const show = (data) => {
  formData.value = Object.assign({}, data);
  formData.value.avatar = { userId: data.userId, qqAvatar: data.avatar };
  dialogConfig.value.show = true;
};
// 子组件暴露自己的属性
// 父组件需要调用子组件的方法父组件需要调用子组件的方法,
// 或者访问子组件的变量
defineExpose({ show });

// 定义弹出框的属性
const dialogConfig = ref({
  show: false,
  title: "修改头像",
  buttons: [
    {
      type: "primary",
      text: "确定",
      click: (e) => {
        submitForm();
      },
    },
  ],
});

// 1、在子组件中调用defineEmits并定义要发射给父组件的方法
// 2、使用defineEmits会返回一个方法,使用一个变量emit(变量名随意)去接收
// 3、在子组件要触发的方法中,调用emit并传入发射给 父组件的方法(updateAvatar)
const emit = defineEmits(["updateAvatar"]);
// 点击确定 提交的回调
const submitForm = async () => {
  // 如果上传的不是文件,将关闭弹出框
  if (!(formData.value.avatar instanceof File)) {
    dialogConfig.value.show = false;
  }
  let result = await proxy.Request({
    url: api.updateUserAvatar,
    params: {
      avatar: formData.value.avatar,
    },
  });
  if (!result) {
    return;
  }
  dialogConfig.value.show = false;
  const cookeUserInfo = proxy.VueCookies.get("userInfo");
  delete cookeUserInfo.avatar;
  proxy.VueCookies.set("userInfo", cookeUserInfo, 0);
  //   在子组件要触发的方法中,调用emit并传入发射给 父组件的方法(updateAvatar)
  emit("updateAvatar");
};
</script>

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

(2)封装全局组件AvatarUpload(上传图片)
./src/commponents/AvatarUpload.vue

<template>
  <!-- 头像上传 -->
  <div class="avatar-upload">
    <div class="avatar-show">
      <template v-if="localFile">
        <img :src="localFile" />
      </template>
      <template v-else>
        <img
          :src="`${modelValue.qqAvatar}`"
          v-if="modelValue && modelValue.qqAvatar"
        />
        <img :src="`/api/getAvatar/${modelValue.userId}`" v-else />
      </template>
    </div>
    <div class="select-btn">
      <el-upload
        name="file"
        :show-file-list="false"
        accept=".png,.PNG,.jpg,.JPG,.jpeg,.JPEG,.gif,.GIF,.bmp,.BMP"
        :multiple="false"
        :http-request="uploadImage"
      >
        <el-button type="primary">选择</el-button>
      </el-upload>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";

const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();

const timestamp = ref("");

// defineProps是一个函数 定义后props可直接在模板中使用,或者在setup其他地方使用
const props = defineProps({
  modelValue: {
    type: Object,
    default: null,
  },
});

// 本地图片
const localFile = ref(null);
// 子组件向父组件传值
const emit = defineEmits();
// 上传图片
const uploadImage = async (file) => {
  file = file.file;
  let img = new FileReader();
  img.readAsDataURL(file);
  img.onload = ({ target }) => {
    localFile.value = target.result;
  };
  emit("update:modelValue", file);
};
</script>

<style lang="scss">
.avatar-upload {
  display: flex;
  justify-content: center;
  align-items: end;
  .avatar-show {
    background: rgb(245, 245, 245);
    width: 150px;
    height: 150px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    position: relative;
    .iconfont {
      font-size: 50px;
      color: #ddd;
    }
    img {
      width: 100%;
      height: 100%;
    }
    .op {
      position: absolute;
      color: #0e8aef;
      top: 80px;
    }
  }
  .select-btn {
    margin-left: 10px;
    vertical-align: bottom;
  }
}
</style>

(3)Framework.vue中引入使用

<!-- 修改头像组件 -->
<UpdateAvatar ref="updateAvatarRef" @updateAvatar="reloadAvatar"></UpdateAvatar>
import UpdateAvatar from "./UpdateAvatar.vue";

(4)添加点击事件
 

<el-dropdown-item @click="updateAvatar">修改头像</el-dropdown-item>
......
// 修改头像
const updateAvatarRef = ref();
// 利用defineExpose,父组件调用子组件的函数,
// 将用户信息利用show传递给子组件的show函数,使得子组件更新信息
const updateAvatar = () => {
    updateAvatarRef.value.show(userInfo.value);
};
// 重新加载最新头像,利用子组件的 emit("updateAvatar"); 传递回来的信息,
const reloadAvatar = () => {
    userInfo.value = proxy.VueCookies.get("userInfo");
    timestamp.value = new Date().getTime();
};

 五、修改密码

(1)封装UpdatePassword.vue组件

./src/views/UpdatePassword.vue

<template>
  <div>
    <!-- 修改头像弹出框 -->
    <Dialog
      :show="dialogConfig.show"
      :title="dialogConfig.title"
      :buttons="dialogConfig.buttons"
      width="500px"
      :showCancel="true"
      @close="dialogConfig.show = false"
    >
      <el-form
        :model="formData"
        :rules="rules"
        ref="formDataRef"
        label-width="80px"
        @submit.prevent
      >
        <!--输入新密码-->
        <el-form-item label="新密码" prop="password">
          <el-input
            type="password"
            size="large"
            placeholder="请输入密码"
            v-model.trim="formData.password"
            show-password
          >
            <template #prefix>
              <span class="iconfont icon-password"></span>
            </template>
          </el-input>
        </el-form-item>

        <!-- 再次输入密码 -->
        <!--输入新密码-->
        <el-form-item label="确认密码" prop="rePassword">
          <el-input
            type="password"
            size="large"
            placeholder="请再次输入密码"
            v-model.trim="formData.rePassword"
            show-password
          >
            <template #prefix>
              <span class="iconfont icon-password"></span>
            </template>
          </el-input>
        </el-form-item>
      </el-form>
    </Dialog>
  </div>
</template>

<script setup>
import AvatarUpload from "@/components/AvatarUpload.vue";
import { ref, reactive, getCurrentInstance, nextTick } from "vue";

const { proxy } = getCurrentInstance();
const api = {
  updatePassword: "updatePassword",
};

const formData = ref({});
const formDataRef = ref();

// 校验再次输入的密码
const checkRePassword = (rule, value, callback) => {
  if (value !== formData.value.rePassword) {
    callback(new Error(rule.message));
  } else {
    callback();
  }
};
const rules = {
  password: [
    { required: true, message: "请输入密码" },
    {
      validator: proxy.Verify.password,

      message: "密码只能是数字,字母,特殊字符 8-18 位",
    },
  ],
  rePassword: [
    { required: true, message: "请再次输入密码" },
    {
      validator: checkRePassword,
      message: "两次输入的密码不一致",
    },
  ],
};

const show = () => {
  dialogConfig.value.show = true;
  nextTick(() => {
    formDataRef.value.resetFields();
    formData.value = {};
  });
};
// 子组件暴露自己的属性
// 父组件需要调用子组件的方法父组件需要调用子组件的方法,
// 或者访问子组件的变量
defineExpose({ show });

const dialogConfig = ref({
  show: false,
  title: "修改密码",
  buttons: [
    {
      type: "primary",
      text: "确定",
      click: (e) => {
        submitForm();
      },
    },
  ],
});

const submitForm = async () => {
  formDataRef.value.validate(async (valid) => {
    if (!valid) {
      return;
    }
    let result = await proxy.Request({
      url: api.updatePassword,
      params: {
        password: formData.value.password,
      },
    });
    if (!result) {
      return;
    }
    dialogConfig.value.show = false;
    proxy.Message.success("密码修改成功");
  });
};
</script>

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

(2)在Framework.vue引入,使用组件

<!-- 修改密码组件 -->
    <UpdatePassword ref="updatePasswordRef"></UpdatePassword>
import UpdatePassword from "./UpdatePassword.vue";

(3)添加点击事件,点击事件回调

<el-dropdown-item @click="updatePassword">修改密码</el-dropdown-item>
// 修改密码
const updatePasswordRef = ref();
const updatePassword = () => {
    updatePasswordRef.value.show();
};

六、退出登录

(1)封装全局组件
./src/utils/Confirm.js

import { ElMessageBox } from 'element-plus'

const confirm = (message, okfun) => {
    ElMessageBox.confirm(message, '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'info',
    }).then(() => {
        okfun()
    }).catch(() => {})
}

export default confirm;

(2)main.js引入和配置

import Confirm from './utils/Confirm';
app.config.globalProperties.Confirm=Confirm

(3)添加api接口

const api = {
    // getUseSpace: "/getUseSpace",
    logout: "/logout",
};

(4)添加点击事件
 

<el-dropdown-item @click="logout">退出</el-dropdown-item>
// 退出登录
const logout = () => {
    proxy.Confirm(`你确定要删除退出吗`, async () => {
        let result = await proxy.Request({
            url: api.logout,
        });
        if (!result) {
            return;
        }
        proxy.VueCookies.remove("userInfo");
        router.push("/login");
    });
};

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

动态IP避坑指南,怎样挑选合适的动态IP?

在现今这个数字化、网络化的时代&#xff0c;动态IP的使用越来越广泛&#xff0c;无论是为了保护网络安全、提高网络访问速度&#xff0c;还是为了实现某些特定的网络功能&#xff0c;动态IP都发挥着不可或缺的作用。然而&#xff0c;如何挑选一个合适的动态IP&#xff0c;避免…

git知识总结

要知道 本地回退后&#xff0c;反悔了&#xff0c;可以恢复。前提是已经提交了&#xff0c;提交了就丢不了。 git reflog git reset --hard commitId 以前git push不让推&#xff0c;就是没有对应关系。第一次推要setxxx参数。 前奏 设置用户名和邮箱&#xff0c;设置错…

docker 部署SSM项目(包含打包)

一&#xff1a;SSM项目打包 1.这个一定要勾选防止静态资源没打包上 2.第二步 3.第三步 4.更改名字(注意部署到线上的时候这里如果用docker或者window部署的话需要带这个项目名&#xff0c;不然会出现找不到接口的情况) ![在这里插入图片描述](https://img-blog.csdnimg.cn/dir…

C#中数组与列表,集合等的联系

C#中&#xff0c;所有数组都自动继承于System.Array这个抽象类&#xff0c;数组都为引用类型&#xff0c; 所有对数组的更新都会导致源数组的元素值的篡改。 而所有集合的根都来自可枚举接口IEnumerable 数组有三种样式&#xff1a; 数组的Rank&#xff08;秩&#xff09;属…

企业微信hook接口协议,ipad协议http,同步消息记录

同步消息记录 参数名必选类型说明uuid是String每个实例的唯一标识&#xff0c;根据uuid操作具体企业微信limit是int每次返回大小seq是int查询下标 请求示例 {"uuid":"ecb033af-6fcd-4ec2-880e-41f070b65eaf","limit":1000, "seq":1…

访客管理系统对于校园安全的重要性

校园访客办理计划是针对校园安全需求规划的安全办理体系&#xff0c;主要用于对校园外来人员的科学办理。要做好校园安全作业&#xff0c;把风险分子拒之门外尤为要害。校园访客办理计划实现访客实名制&#xff0c;并结合公安网、黑名单功用&#xff0c;对风险人员进行提前预警…

浅析vue3自定义指令

vue3中可以像下面这样使用自定义指令。 这里我们只是定义了一个vFoucs变量&#xff0c;vue怎么知道这是一个指令呢&#xff1f; 这是因为约定大于配置&#xff0c;vue3中有这样一个约定&#xff08;截图来自官方文档&#xff09;&#xff1a; 注意这里说的是驼峰命令&#x…

【class6】人工智能初步(选择一个合适的监督学习算法。)

【昨日内容复习】 进行监督学习时&#xff0c;第一个步骤是提取数据集的文本特征和对应的标签。 提取文本特征的具体步骤如下&#xff1a; STEP1. 构造词袋模型&#xff0c;提取数据集中的文本特征 STEP2. 使用toarray()函数&#xff0c;将X转换为一个NumPy数组&#xff0c;方…

初始化linux数据盘(3TB)分区-格式化-挂载目录

场景说明&#xff1a;某云给我们服务器加载了一块3TB的硬盘扩容&#xff08;没有直接扩&#xff0c;原因是原来的盘做的是mbr&#xff08;什么年代了&#xff0c;谁干的&#xff09;的分区&#xff0c;最大识别2TB&#xff09; 确认磁盘 输入命令lsblk 查看数据盘信息 &#…

PyQt5的多窗口设计

文章目录 步骤一步骤2步骤3步骤4步骤5完整的代码如下 步骤一 1.首先新建一个窗口&#xff0c;把对象名改为ParentWindow&#xff0c;然后保存&#xff0c;把.ui的名字改为Main_window.ui 步骤2 再新建一个窗口&#xff0c;把对象名改为ChildWindow1&#xff0c;然后保存&…

Python 旋转立方体

文章目录 效果图运行环境完整代码实现思路1. 导入库和定义常量2. 创建Cube类3. 实现Cube类的draw方法4. 实现主函数 效果图 运行环境 python版本&#xff1a;python3.x 依赖包&#xff1a; $ pip install pygame $ pip install numpy完整代码 import numpy as np # 导入 N…

【复试分数线】工科985历年分数线汇总(第三弹)

国家线 可以看作是考研上岸最最最基础的门槛。真正决定你能不能进入复试的还要看院线&#xff08;复试分数线&#xff09;&#xff01;今天我将分析考信号的5所工科类985近三年复试分数线&#xff0c;大家可以参考&#xff01;&#xff01; 分别是&#xff1a;①北京航空航天大…

k8s coredns配置

1.coredns可根据集群具体数量修改pod数&#xff0c;官方推荐比例为5/1&#xff0c;即有15台服务器最好是3个pod。 2.coredns会继承pod所在主机的dns解析,修改了主机的dns解析之后&#xff0c;coredns有一段时间的缓存&#xff0c;重启coredns才会在集群内部立刻生效该解析。 …

黑马甄选离线数仓项目day01(项目介绍)

课程介绍 项目名称 黑马甄选数仓形式 离线数仓开发业务类型 电商业务 电商介绍 B2B B2C C2C 项目属于 新零售电商 新零售 线上(网站,app,小程序&#xff09; 线下&#xff08;实体体验店&#xff09; 物流&#xff08;自营物流&#xff09; 项目行业 果蔬生鲜领域 商业模式 B…

[图解]实现领域驱动设计译文暴露的问题03

0 00:00:02,960 --> 00:00:04,940 前面我们讲了 1 00:00:05,260 --> 00:00:06,810 第①句话的 2 00:00:07,090 --> 00:00:09,750 第&#xff08;1&#xff09;个问题和第&#xff08;2&#xff09;个问题 3 00:00:13,920 --> 00:00:16,930 共享父类的对象&#…

【代码随想录算法训练Day5】今天休息,复盘总结

Day5 休息日 时机恰到好处&#xff0c;刚好学习完了数组和链表&#xff0c;从代码随想录里扒了两张总结图来&#xff0c;这就是这几天里我们一起解决的问题&#xff0c;如果以后忘了&#xff0c;还有问题&#xff0c;先回到这里&#xff0c;只看思维导图&#xff0c;还能想起来…

WordPress 管理员密码重置方法汇总

最近明月碰到一个 WordPress 站长求助咨询&#xff0c;说是自己 WordPress 站点的管理员密码被恶意篡改了&#xff0c;对 WordPress 了解的都知道这一般都是恶意代码造成的&#xff0c;问题大多出在使用了所谓的破解版、去授权版的插件或者主题被植入了恶意代码、后门木马。明月…

【目标检测论文解读复现NO.38】基于改进YOLOv8模型的轻量化板栗果实识别方法

前言 此前出了目标改进算法专栏&#xff0c;但是对于应用于什么场景&#xff0c;需要什么改进方法对应与自己的应用场景有效果&#xff0c;并且多少改进点能发什么水平的文章&#xff0c;为解决大家的困惑&#xff0c;此系列文章旨在给大家解读最新目标检测算法论文&#xff0c…

革新机器人任务规划:TREE-PLANNER引领高效、准确的机器人动作生成新趋势

DeepVisionary 每日深度学习前沿科技推送&顶会论文分享&#xff0c;与你一起了解前沿深度学习信息&#xff01; 引言 任务规划在机器人技术中扮演着至关重要的角色。它涉及到为机器人设计一系列中级动作&#xff08;技能&#xff09;&#xff0c;使其能够完成复杂的高级任…

网络基础(三)——网络层

目录 IP协议 1、基本概念 2、协议头格式 2.1、报头和载荷如何有效分离 2.2、如果超过了MAC的规定&#xff0c;IP应该如何做呢&#xff1f; 2.3、分片会有什么影响 3、网段划分 4、特殊的ip地址 5、ip地址的数量限制 6、私有ip地址和公网ip地址 7、路由 IP协议 网络…