兰宝车间质量管理系统-前端
三个三
2023-06-20 aae6bb75059718ca4ae0b3361a1a7ea48faca87e
!16 扩展第三方登录授权功能
* add 第三方授权
* add 第三方授权登录
已添加4个文件
已修改6个文件
433 ■■■■ 文件已修改
.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/social/auth.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/icons/svg/gitee.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/socialLogin/index.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/permission.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/index.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/thirdParty.vue 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -1,4 +1,5 @@
.DS_Store
.history
node_modules/
dist/
npm-debug.log*
src/api/login.ts
@@ -60,6 +60,24 @@
    timeout: 20000
  });
}
/**
 * ç¬¬ä¸‰æ–¹ç™»å½•
 * @param source ç¬¬ä¸‰æ–¹ç™»å½•类型
 * */
export function socialLogin(source: string, code: any, state: any): AxiosPromise<any> {
  const data = {
    code,
    state
  };
  return request({
    url: '/auth/social-login/' + source,
    method: 'get',
    headers: {
      isToken: true
    },
    params: data
  });
}
// èŽ·å–ç”¨æˆ·è¯¦ç»†ä¿¡æ¯
export function getInfo(): AxiosPromise<UserInfo> {
src/api/system/social/auth.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
import request from '@/utils/request';
// ç»‘定账号
export function authBinding(source: string) {
  return request({
    url: '/auth/binding/' + source,
    method: 'get',
    headers: {
      isToken: true
    }
  });
}
// è§£ç»‘账号
export function authUnlock(authId: string) {
  return request({
    url: '/auth/unlock/' + authId,
    method: 'delete',
    headers: {
      isToken: true
    }
  });
}
//获取授权列表
export function getAuthList() {
  return request({
    url: '/system/social/list',
    method: 'get',
    headers: {
      isToken: true
    }
  });
}
src/assets/icons/svg/gitee.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686919908144" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2521" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 992C246.895625 992 32 777.104375 32 512S246.895625 32 512 32s480 214.895625 480 480-214.895625 480-480 480z m242.9521875-533.3278125h-272.56875a23.7121875 23.7121875 0 0 0-23.71125 23.7121875l-0.024375 59.255625c0 13.08 10.6078125 23.7121875 23.6878125 23.7121875h165.96c13.104375 0 23.7121875 10.6078125 23.7121875 23.6878125v11.855625a71.1121875 71.1121875 0 0 1-71.1121875 71.1121875h-225.215625a23.7121875 23.7121875 0 0 1-23.6878125-23.7121875V423.1278125a71.1121875 71.1121875 0 0 1 71.0878125-71.1121875h331.824375a23.7121875 23.7121875 0 0 0 23.6878125-23.71125l0.0721875-59.2565625a23.7121875 23.7121875 0 0 0-23.68875-23.7121875H423.08a177.76875 177.76875 0 0 0-177.76875 177.7921875V754.953125c0 13.1034375 10.60875 23.7121875 23.713125 23.7121875h349.63125a159.984375 159.984375 0 0 0 159.984375-159.984375V482.36a23.7121875 23.7121875 0 0 0-23.7121875-23.6878125z" fill="#515151" p-id="2522"></path></svg>
src/layout/components/socialLogin/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
<template>
  <div v-loading="loading" class="social-login"></div>
</template>
<script setup lang="ts">
import { socialLogin } from '@/api/login';
import { setToken } from '@/utils/auth';
const route = useRoute();
const router = useRouter();
/**
 * æŽ¥æ”¶Route传递的参数
 * @param {Object} route.query.
 */
const code = route.query.code;
const state = route.query.state;
const source = route.query.source as string;
const loading = ref(true);
await socialLogin(source, code, state)
  .then(async (res) => {
    if (res.code !== 200) {
      ElMessage.error(res.msg);
      router.go(-2);
      return;
    }
    loading.value = false;
    setToken(res.msg);
    ElMessage.success('登录成功');
    router.go(-2);
  })
  .catch(() => {
    loading.value = false;
  });
</script>
src/permission.ts
@@ -10,7 +10,7 @@
import usePermissionStore from '@/store/modules/permission';
NProgress.configure({ showSpinner: false });
const whiteList = ['/login', '/register'];
const whiteList = ['/login', '/register', '/social-login'];
router.beforeEach(async (to, from, next) => {
  NProgress.start();
src/router/index.ts
@@ -38,6 +38,11 @@
    ]
  },
  {
    path: '/social-login',
    hidden: true,
    component: () => import('@/layout/components/socialLogin/index.vue')
  },
  {
    path: '/login',
    component: () => import('@/views/login.vue'),
    hidden: true
src/views/login.vue
@@ -4,7 +4,7 @@
      <h3 class="title">RuoYi-Vue-Plus多租户管理系统</h3>
      <el-form-item prop="tenantId" v-if="tenantEnabled">
        <el-select v-model="loginForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
          <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
          <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
        </el-select>
      </el-form-item>
@@ -36,6 +36,20 @@
          <router-link class="link-type" :to="'/register'">立即注册</router-link>
        </div>
      </el-form-item>
      <div style="display: flex;justify-content: flex-end;flex-direction: row;">
        <el-button circle>
          <svg-icon icon-class="qq" @click="doSocialLogin('QQ')" />
        </el-button>
        <el-button circle>
          <svg-icon icon-class="wechat" @click="doSocialLogin('Wechat')" />
        </el-button>
        <el-button circle>
          <svg-icon icon-class="gitee" @click="doSocialLogin('gitee')" />
        </el-button>
        <el-button circle>
          <svg-icon icon-class="github" @click="doSocialLogin('github')" />
        </el-button>
      </div>
    </el-form>
    <!--  åº•部  -->
    <div class="el-login-footer">
@@ -46,30 +60,31 @@
<script setup lang="ts">
import { getCodeImg, getTenantList } from '@/api/login';
import { authBinding } from '@/api/system/social/auth';
import Cookies from 'js-cookie';
import { encrypt, decrypt } from '@/utils/jsencrypt';
import { useUserStore } from '@/store/modules/user';
import { LoginData, TenantVO } from '@/api/types';
import { FormRules } from 'element-plus';
import { ElForm, FormRules } from 'element-plus';
import { to } from 'await-to-js';
const userStore = useUserStore();
const router = useRouter();
const loginForm = ref<LoginData>({
    tenantId: "000000",
    username: 'admin',
    password: 'admin123',
    rememberMe: false,
    code: '',
    uuid: ''
  tenantId: "000000",
  username: 'admin',
  password: 'admin123',
  rememberMe: false,
  code: '',
  uuid: ''
});
const loginRules: FormRules = {
    tenantId: [{ required: true, trigger: "blur", message: "请输入您的租户编号" }],
    username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
    password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
    code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
  tenantId: [{ required: true, trigger: "blur", message: "请输入您的租户编号" }],
  username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
  password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
  code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
};
const codeUrl = ref('');
@@ -88,64 +103,64 @@
const tenantList = ref<TenantVO[]>([]);
const handleLogin = () => {
    loginRef.value.validate(async (valid:boolean, fields: any) => {
        if (valid) {
            loading.value = true;
            // å‹¾é€‰äº†éœ€è¦è®°ä½å¯†ç è®¾ç½®åœ¨ cookie ä¸­è®¾ç½®è®°ä½ç”¨æˆ·åå’Œå¯†ç 
            if (loginForm.value.rememberMe) {
                Cookies.set("tenantId", loginForm.value.tenantId, { expires: 30 });
                Cookies.set('username', loginForm.value.username, { expires: 30 });
                Cookies.set('password', String(encrypt(loginForm.value.password)), { expires: 30 });
                Cookies.set('rememberMe', String(loginForm.value.rememberMe), { expires: 30 });
            } else {
                // å¦åˆ™ç§»é™¤
                Cookies.remove("tenantId");
                Cookies.remove('username');
                Cookies.remove('password');
                Cookies.remove('rememberMe');
            }
            // è°ƒç”¨action的登录方法
            // prittier-ignore
            const [err] = await to(userStore.login(loginForm.value));
            if (!err) {
                await router.push({ path: redirect.value || '/' });
            } else {
                loading.value = false;
                // é‡æ–°èŽ·å–éªŒè¯ç 
                if (captchaEnabled.value) {
                    await getCode();
                }
            }
        } else {
            console.log('error submit!', fields);
  loginRef.value.validate(async (valid: boolean, fields: any) => {
    if (valid) {
      loading.value = true;
      // å‹¾é€‰äº†éœ€è¦è®°ä½å¯†ç è®¾ç½®åœ¨ cookie ä¸­è®¾ç½®è®°ä½ç”¨æˆ·åå’Œå¯†ç 
      if (loginForm.value.rememberMe) {
        Cookies.set("tenantId", loginForm.value.tenantId, { expires: 30 });
        Cookies.set('username', loginForm.value.username, { expires: 30 });
        Cookies.set('password', String(encrypt(loginForm.value.password)), { expires: 30 });
        Cookies.set('rememberMe', String(loginForm.value.rememberMe), { expires: 30 });
      } else {
        // å¦åˆ™ç§»é™¤
        Cookies.remove("tenantId");
        Cookies.remove('username');
        Cookies.remove('password');
        Cookies.remove('rememberMe');
      }
      // è°ƒç”¨action的登录方法
      // prittier-ignore
      const [err] = await to(userStore.login(loginForm.value));
      if (!err) {
        await router.push({ path: redirect.value || '/' });
      } else {
        loading.value = false;
        // é‡æ–°èŽ·å–éªŒè¯ç 
        if (captchaEnabled.value) {
          await getCode();
        }
    });
      }
    } else {
      console.log('error submit!', fields);
    }
  });
};
/**
 * èŽ·å–éªŒè¯ç 
 */
const getCode = async () => {
    const res = await getCodeImg();
    const { data } = res;
    captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
    if (captchaEnabled.value) {
        codeUrl.value = 'data:image/gif;base64,' + data.img;
        loginForm.value.uuid = data.uuid;
    }
  const res = await getCodeImg();
  const { data } = res;
  captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
  if (captchaEnabled.value) {
    codeUrl.value = 'data:image/gif;base64,' + data.img;
    loginForm.value.uuid = data.uuid;
  }
};
const getCookie = () => {
    const tenantId = Cookies.get("tenantId");
    const username = Cookies.get('username');
    const password = Cookies.get('password');
    const rememberMe = Cookies.get('rememberMe');
    loginForm.value = {
        tenantId: tenantId === undefined ? loginForm.value.tenantId : tenantId,
        username: username === undefined ? loginForm.value.username : username,
        password: password === undefined ? loginForm.value.password : (decrypt(password) as string),
        rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
    };
  const tenantId = Cookies.get("tenantId");
  const username = Cookies.get('username');
  const password = Cookies.get('password');
  const rememberMe = Cookies.get('rememberMe');
  loginForm.value = {
    tenantId: tenantId === undefined ? loginForm.value.tenantId : tenantId,
    username: username === undefined ? loginForm.value.username : username,
    password: password === undefined ? loginForm.value.password : (decrypt(password) as string),
    rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
  };
}
@@ -153,20 +168,35 @@
 * èŽ·å–ç§Ÿæˆ·åˆ—è¡¨
 */
const initTenantList = async () => {
    const { data } = await getTenantList();
    tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
    if (tenantEnabled.value) {
        tenantList.value = data.voList;
        if (tenantList.value != null && tenantList.value.length !== 0) {
            loginForm.value.tenantId = tenantList.value[0].tenantId;
        }
  const { data } = await getTenantList();
  tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
  if (tenantEnabled.value) {
    tenantList.value = data.voList;
    if (tenantList.value != null && tenantList.value.length !== 0) {
      loginForm.value.tenantId = tenantList.value[0].tenantId;
    }
  }
}
/**
 * ç¬¬ä¸‰æ–¹ç™»å½•
 * @param type
 */
const doSocialLogin = (type: string) => {
  authBinding(type).then((res: any) => {
    if (res.code === 200) {
      window.location.href = res.msg;
  } else {
      ElMessage.error(res.msg);
    }
  });
};
onMounted(() => {
    getCode();
    initTenantList();
    getCookie();
  getCode();
  initTenantList();
  getCookie();
});
</script>
@@ -179,6 +209,7 @@
  background-image: url("../assets/images/login-background.jpg");
  background-size: cover;
}
.title {
  margin: 0px auto 30px auto;
  text-align: center;
@@ -190,32 +221,39 @@
  background: #ffffff;
  width: 400px;
  padding: 25px 25px 5px 25px;
  .el-input {
    height: 40px;
    input {
      height: 40px;
    }
  }
  .input-icon {
    height: 39px;
    width: 14px;
    margin-left: 0px;
  }
}
.login-tip {
  font-size: 13px;
  text-align: center;
  color: #bfbfbf;
}
.login-code {
  width: 33%;
  height: 40px;
  float: right;
  img {
    cursor: pointer;
    vertical-align: middle;
  }
}
.el-login-footer {
  height: 40px;
  line-height: 40px;
@@ -224,10 +262,11 @@
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial,serif;
  font-family: Arial, serif;
  font-size: 12px;
  letter-spacing: 1px;
}
.login-code-img {
  height: 40px;
  padding-left: 12px;
src/views/system/user/profile/index.vue
@@ -55,6 +55,9 @@
            <el-tab-pane label="修改密码" name="resetPwd">
              <resetPwd />
            </el-tab-pane>
            <el-tab-pane label="第三方应用" name="thirdParty">
              <thirdParty :auths="state.auths" />
            </el-tab-pane>
          </el-tabs>
        </el-card>
      </el-col>
@@ -66,13 +69,16 @@
import userAvatar from "./userAvatar.vue";
import userInfo from "./userInfo.vue";
import resetPwd from "./resetPwd.vue";
import thirdParty from "./thirdParty.vue";
import { getAuthList } from "@/api/system/social/auth";
import { getUserProfile } from "@/api/system/user";
const activeTab = ref("userinfo");
const state = ref<{ user: any; roleGroup: string;  postGroup: string}>({
const state = ref<{ user: any; roleGroup: string;  postGroup: string; auths:any[]}>({
    user: {},
    roleGroup: '',
    postGroup: ''
    postGroup: '',
    auths: [],
});
const userForm = ref({});
@@ -85,7 +91,13 @@
    state.value.postGroup = res.data.postGroup;
};
const getAuths = async () => {
    const res = await getAuthList();
    state.value.auths = res.data;
};
onMounted(() => {
    getUser();
    getAuths();
})
</script>
src/views/system/user/profile/thirdParty.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
<template>
  <div>
    <el-table :data="auths" style="width: 100%; height: 100%; font-size: 10px">
      <el-table-column label="序号" width="50" type="index"></el-table-column>
      <el-table-column label="绑定账号平台" width="140" align="center" prop="source" show-overflow-tooltip />
      <el-table-column label="头像" width="120" align="center" prop="avatar">
        <template v-slot="scope">
          <img :src="scope.row.avatar" style="width: 45px; height: 45px" />
        </template>
      </el-table-column>
      <el-table-column label="系统账号" width="180" align="center" prop="userName" :show-overflow-tooltip="true" />
      <el-table-column label="绑定时间" width="180" align="center" prop="createTime" />
      <el-table-column label="操作" width="80" align="center" class-name="small-padding fixed-width">
        <template v-slot="scope">
          <el-button size="small" type="text" @click="unlockAuth(scope.row)">解绑</el-button>
        </template>
      </el-table-column>
    </el-table>
    <div id="git-user-binding">
      <h4 class="provider-desc">你可以绑定以下第三方帐号</h4>
      <div id="authlist" class="user-bind">
        <a class="third-app" href="#" @click="authUrl('gitee');" title="使用 Gitee è´¦å·æŽˆæƒç™»å½•">
          <div class="git-other-login-icon">
            <svg-icon icon-class="gitee" />
          </div>
          <span class="app-name">Gitee</span>
        </a>
        <a class="third-app" href="#" @click="authUrl('github');" title="使用 GitHub è´¦å·æŽˆæƒç™»å½•">
          <div class="git-other-login-icon">
            <svg-icon icon-class="github" />
          </div>
          <span class="app-name">Github</span>
        </a>
        <a class="third-app" href="#" title="功能开发中...">
          <div class="git-other-login-icon">
            <svg-icon icon-class="wechat" />
          </div>
          <span class="app-name">WeiXin</span>
        </a>
        <a class="third-app" href="#" title="功能开发中...">
          <div class="git-other-login-icon">
            <svg-icon icon-class="qq" />
          </div>
          <span class="app-name">QQ</span>
        </a>
      </div>
    </div>
  </div>
</template>
<script lang="ts" setup>
import { authUnlock, authBinding } from "@/api/system/social/auth";
import { PropType } from "vue";
const props = defineProps({
  auths: {
    type: Object as PropType<any>,
  }
});
const auths = computed(() => props.auths);
const unlockAuth = (row: any) => {
  ElMessageBox.confirm('您确定要解除"' + row.source + '"的账号绑定吗?')
    .then(() => {
      return authUnlock(row.id);
    }).then((res: any) => {
      if (res.code === 200) {
        ElMessage.success("解绑成功");
      } else {
        ElMessage.error(res.msg);
      }
    }).catch(() => { });
};
const authUrl = (source: string) => {
  authBinding(source).then((res: any) => {
    if (res.code === 200) {
      window.location.href = res.msg;
    } else {
      ElMessage.error(res.msg);
    }
  });
};
</script>
<style type="text/css">
.user-bind .third-app {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-box-orient: vertical;
  -webkit-box-direction: normal;
  -ms-flex-direction: column;
  flex-direction: column;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  min-width: 80px;
  float: left;
}
.user-bind {
  font-size: 1rem;
  text-align: start;
  height: 50px;
  margin-top: 10px;
}
.git-other-login-icon>img {
  height: 32px;
}
a {
  text-decoration: none;
  cursor: pointer;
  color: #005980;
}
.provider-desc {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
    "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Liberation Sans",
    "PingFang SC", "Microsoft YaHei", "Hiragino Sans GB", "Wenquanyi Micro Hei",
    "WenQuanYi Zen Hei", "ST Heiti", SimHei, SimSun, "WenQuanYi Zen Hei Sharp",
    sans-serif;
  font-size: 1.071rem;
}
td>img {
  height: 20px;
  width: 20px;
  display: inline-block;
  border-radius: 50%;
  margin-right: 5px;
}
</style>