Ver Fonte

feat: 账密登录

shuisheng há 2 anos atrás
pai
commit
e3e92ba6a8

+ 2 - 2
package.json

@@ -35,8 +35,8 @@
     "@vicons/ionicons5": "^0.12.0",
     "axios": "^1.2.1",
     "billd-html-webpack-plugin": "^1.0.5",
-    "billd-scss": "^0.0.7",
-    "billd-utils": "^0.0.15",
+    "billd-scss": "^0.0.8",
+    "billd-utils": "^0.0.19",
     "fabric": "^5.3.0",
     "flv.js": "^1.6.2",
     "js-cookie": "^3.0.5",

+ 8 - 8
pnpm-lock.yaml

@@ -15,11 +15,11 @@ dependencies:
     specifier: ^1.0.5
     version: 1.0.5(@swc/core@1.3.84)(@types/node@18.15.3)(esbuild@0.15.18)(sass@1.59.3)(terser@5.16.6)(webpack-cli@4.10.0)
   billd-scss:
-    specifier: ^0.0.7
-    version: 0.0.7
+    specifier: ^0.0.8
+    version: 0.0.8
   billd-utils:
-    specifier: ^0.0.15
-    version: 0.0.15(typescript@5.1.6)
+    specifier: ^0.0.19
+    version: 0.0.19(typescript@5.1.6)
   fabric:
     specifier: ^5.3.0
     version: 5.3.0
@@ -3661,13 +3661,13 @@ packages:
       - webpack-cli
     dev: false
 
-  /billd-scss@0.0.7:
-    resolution: {integrity: sha512-tB2z5TqDe0VObIU0MkbzicSh8tkfhrO3eN5lu6nWBcRljPyJsgjvoxhuYfXEBsnlYS8UWqQK/aYrc35IXZtUDA==}
+  /billd-scss@0.0.8:
+    resolution: {integrity: sha512-42gyDYnmRICjqrzk3qbqVygpvjpMu/2rMk41x8HyWLOf457EkXyCiAAFrFR2XM3/c6PHWbtD+IhP8MHUV6JeyA==}
     requiresBuild: true
     dev: false
 
-  /billd-utils@0.0.15(typescript@5.1.6):
-    resolution: {integrity: sha512-IkazhLt62GRKANDf9ZRzdMWhKxC6qoL6O+SYdpoF/ul6NW46yaLTnOjEGUsrycXyJvcaL+U+7YinKhnuL8skAQ==}
+  /billd-utils@0.0.19(typescript@5.1.6):
+    resolution: {integrity: sha512-sap3/whp9sAnFqgpCormuPA911hexCzCzuMNMrQCVarEgTXkTQ7vOW4saZxLw5/HauajsXHEBGd4ZNmvQ0hZGw==}
     requiresBuild: true
     dependencies:
       '@babel/core': 7.21.3

+ 81 - 0
src/api/emailUser.ts

@@ -0,0 +1,81 @@
+import request from '@/utils/request';
+
+export function fetchEmailUserList(params) {
+  return request.instance({
+    url: '/email_user/list',
+    method: 'get',
+    params,
+  });
+}
+
+// 发送邮箱登录验证码登录
+export function fetchSendLoginCode(email) {
+  return request.instance({
+    url: '/email_user/send_login_code',
+    method: 'post',
+    data: { email },
+  });
+}
+
+// 邮箱验证码登录
+export function fetchEmailCodeLogin({ email, code }) {
+  return request.instance({
+    url: '/email_user/login',
+    method: 'post',
+    data: { email, code },
+  });
+}
+
+// 发送邮箱注册验证码登录
+export function fetchSendRegisterCode(email) {
+  return request.instance({
+    url: '/email_user/send_register_code',
+    method: 'post',
+    data: { email },
+  });
+}
+
+/** 注册 */
+export function fetchRegister({ email, code }) {
+  return request.instance({
+    url: '/email_user/register',
+    method: 'post',
+    data: { email, code },
+  });
+}
+
+// 绑定邮箱
+export function fetchBindEmail({ email, code }) {
+  return request.instance({
+    url: '/email_user/bind_email',
+    method: 'post',
+    data: { email, code },
+  });
+}
+
+// 发送绑定邮箱验证码
+export function fetchSendBindEmailCode(email) {
+  return request.instance({
+    url: '/email_user/send_bind_code',
+    method: 'post',
+    data: { email },
+  });
+}
+
+// 取消绑定邮箱
+export function fetchCancelBindEmail(code) {
+  return request.instance({
+    url: '/email_user/cancel_bind_email',
+    method: 'post',
+    data: { code },
+  });
+}
+
+// 发送取消绑定邮箱验证码
+export function fetchCancelSendBindEmailCode(email) {
+  return request.instance({
+    url: '/email_user/send_cancel_bind_code',
+    method: 'post',
+    data: { email },
+  });
+}

+ 8 - 0
src/api/user.ts

@@ -1,6 +1,14 @@
 import { IPaging, IUser } from '@/interface';
 import request from '@/utils/request';
 
+export function fetchLogin({ id, password }) {
+  return request.instance({
+    url: '/user/login',
+    method: 'post',
+    data: { id, password },
+  });
+}
+
 export function fetchUserInfo() {
   return request.instance({
     url: '/user/get_user_info',

BIN
src/assets/img/github_logo.webp


BIN
src/assets/img/qq_logo.webp


+ 364 - 0
src/components/LoginModal/index.vue

@@ -0,0 +1,364 @@
+<template>
+  <Teleport to="body">
+    <div
+      class="teleport-login-modal-warp"
+      @click.self.stop="handleClose"
+    >
+      <div class="content">
+        <i
+          class="close"
+          @click="handleClose"
+        ></i>
+        <n-card>
+          <n-tabs
+            :value="currentTab"
+            :default-value="currentTab"
+            size="large"
+            :on-update:value="tabChange"
+          >
+            <n-tab-pane
+              name="pwdlogin"
+              tab="账密登录"
+            >
+              <n-form
+                ref="loginFormRef"
+                label-placement="left"
+                size="large"
+                :model="loginForm"
+                :rules="loginRules"
+              >
+                <n-form-item path="id">
+                  <n-input
+                    v-model:value="loginForm.id"
+                    type="text"
+                    placeholder="请输入账号"
+                  >
+                    <template #prefix>
+                      <n-icon
+                        size="20"
+                        class="lang"
+                      >
+                        <PersonOutline></PersonOutline>
+                      </n-icon>
+                    </template>
+                  </n-input>
+                </n-form-item>
+                <n-form-item path="password">
+                  <n-input
+                    v-model:value="loginForm.password"
+                    type="password"
+                    show-password-on="mousedown"
+                    placeholder="请输入密码"
+                    @focus="onFocus"
+                    @blur="onBlur"
+                    @keyup.enter="handleLoginSubmit"
+                  >
+                    <template #prefix>
+                      <n-icon
+                        size="20"
+                        class="lang"
+                      >
+                        <LockClosedOutline></LockClosedOutline>
+                      </n-icon>
+                    </template>
+                  </n-input>
+                </n-form-item>
+              </n-form>
+              <n-button
+                type="primary"
+                block
+                secondary
+                strong
+                @click="handleLoginSubmit"
+              >
+                登录
+              </n-button>
+            </n-tab-pane>
+            <!-- <n-tab-pane
+              name="codelogin"
+              tab="免密登录"
+            >
+              <n-form
+                ref="registerFormRef"
+                label-placement="left"
+                size="large"
+                :model="registerForm"
+                :rules="registerRules"
+              >
+                <n-form-item path="email">
+                  <n-input
+                    v-model:value="registerForm.email"
+                    placeholder="请输入邮箱"
+                  >
+                    <template #prefix>
+                      <n-icon
+                        size="20"
+                        class="lang"
+                      >
+                        <MailOutline></MailOutline>
+                      </n-icon>
+                    </template>
+                  </n-input>
+                </n-form-item>
+                <n-form-item path="code">
+                  <n-input-group>
+                    <n-input
+                      v-model:value="registerForm.code"
+                      placeholder="请输入验证码"
+                      @keyup.enter="handleRegisterSubmit"
+                    />
+                    <n-button
+                      type="primary"
+                      ghost
+                      :disabled="downCount !== 0"
+                      :loading="sendCodeLoading"
+                      @click="sendCode()"
+                    >
+                      发送{{ downCount !== 0 ? `(${downCount})` : '' }}
+                    </n-button>
+                  </n-input-group>
+                </n-form-item>
+              </n-form>
+              <n-button
+                type="primary"
+                block
+                secondary
+                strong
+                @click="handleRegisterSubmit"
+              >
+                登录
+              </n-button>
+            </n-tab-pane> -->
+          </n-tabs>
+        </n-card>
+        <div class="other-login">
+          <span>第三方登录:</span>
+          <div
+            class="logo-wrap"
+            @click="handleQQLogin()"
+          >
+            <img
+              class="qq-logo"
+              src="@/assets/img/qq_logo.webp"
+            />
+          </div>
+          <div
+            class="logo-wrap"
+            @click="handleGithubLogin"
+          >
+            <img
+              class="qq-logo"
+              src="@/assets/img/github_logo.webp"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </Teleport>
+</template>
+
+<script lang="ts" setup>
+import { LockClosedOutline, PersonOutline } from '@vicons/ionicons5';
+import { ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { fetchSendLoginCode, fetchSendRegisterCode } from '@/api/emailUser';
+import { useQQLogin } from '@/hooks/use-login';
+import { useAppStore } from '@/store/app';
+import { useUserStore } from '@/store/user';
+
+const loginRules = {
+  id: { required: true, message: '请输入账号', trigger: 'blur' },
+  password: { required: true, message: '请输入密码', trigger: 'blur' },
+};
+const registerRules = {
+  email: { required: true, message: '请输入邮箱', trigger: 'blur' },
+  code: { required: true, message: '请输入验证码', trigger: 'blur' },
+};
+
+const router = useRouter();
+const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
+
+const loginForm = ref({
+  id: '',
+  password: '',
+});
+const registerForm = ref({
+  email: '',
+  code: '',
+});
+const loginFormRef = ref(null);
+const registerFormRef = ref(null);
+const currentTab = ref('pwdlogin');
+const sendCodeLoading = ref(false);
+const downCount = ref(0);
+
+const emits = defineEmits(['close']);
+
+function handleGithubLogin() {
+  window.$message.warning('敬请期待!');
+}
+function handleQQLogin() {
+  useQQLogin();
+}
+
+function handleClose() {
+  appStore.showLoginModal = false;
+  emits('close');
+}
+
+const handleLogin = async () => {
+  let token = null;
+  if (currentTab.value === 'codelogin') {
+    token = await userStore.codeLogin({
+      email: registerForm.value.email,
+      code: registerForm.value.code,
+    });
+  } else {
+    token = await userStore.pwdLogin({
+      id: +loginForm.value.id,
+      password: loginForm.value.password,
+    });
+  }
+  if (token) {
+    window.$message.success('登录成功!');
+    userStore.getUserInfo();
+    appStore.showLoginModal = false;
+  }
+};
+const handleRegister = async () => {
+  const { token } = await userStore.register({
+    email: registerForm.value.email,
+    code: registerForm.value.code,
+  });
+  if (token) {
+    window.$message.success('注册成功!');
+  }
+};
+const handleLoginSubmit = (e) => {
+  e.preventDefault();
+  // @ts-ignore
+  loginFormRef.value.validate((errors) => {
+    if (!errors) {
+      handleLogin();
+    }
+  });
+};
+const handleRegisterSubmit = (e) => {
+  e.preventDefault();
+  // @ts-ignore
+  registerFormRef.value.validate((errors) => {
+    if (!errors) {
+      if (currentTab.value === 'register') {
+        handleRegister();
+      } else {
+        handleLogin();
+      }
+    }
+  });
+};
+/** 发送验证码 */
+const sendCode = async () => {
+  if (registerForm.value.email === '')
+    return window.$message.warning('请输入邮箱!');
+  try {
+    sendCodeLoading.value = true;
+    if (currentTab.value === 'codelogin') {
+      await fetchSendLoginCode(registerForm.value.email);
+    } else {
+      await fetchSendRegisterCode(registerForm.value.email);
+    }
+    sendCodeLoading.value = false;
+    window.$message.success('发送成功!');
+    downCount.value = 60;
+    const timer = setInterval(() => {
+      downCount.value -= 1;
+      if (downCount.value === 0) {
+        clearInterval(timer);
+      }
+    }, 1000);
+  } catch (error: any) {
+    sendCodeLoading.value = false;
+    console.log(error);
+  }
+};
+const tabChange = (v) => {
+  currentTab.value = v;
+};
+const focus = ref(false);
+const onFocus = () => {
+  focus.value = true;
+};
+const onBlur = () => {
+  focus.value = false;
+};
+</script>
+
+<style lang="scss" scoped>
+.teleport-login-modal-warp {
+  z-index: 100 !important;
+
+  @extend %maskBg;
+  .content {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    min-width: 350px;
+    border-radius: 5px;
+    background-color: #fff;
+    transform: translate(-50%, -50%);
+    .close {
+      position: absolute;
+      top: 20px;
+      right: 20px;
+      z-index: 1;
+      width: 18px;
+      height: 18px;
+      cursor: pointer;
+
+      @include cross(#ccc, 3px);
+    }
+
+    .top {
+      position: absolute;
+      top: 0;
+      left: 50%;
+      z-index: 100;
+      width: 120px;
+      transform: translate(-50%, -90%);
+      &.close {
+        z-index: 0;
+        transform: translate(-50%, -99%);
+      }
+    }
+
+    .title {
+      margin: 10px 0;
+      text-align: center;
+    }
+
+    .other-login {
+      display: flex;
+      align-items: center;
+      justify-content: space-around;
+      margin: 5px 0;
+      .logo-wrap {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 36px;
+        height: 36px;
+        border-radius: 50%;
+        background-color: #f4f8fb;
+        cursor: pointer;
+        .qq-logo {
+          width: 26px;
+          height: 26px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 1 - 1
src/components/VideoControls/index.vue

@@ -133,7 +133,7 @@ const showLine = ref(false);
 const showSpeed = ref(false);
 
 function handleTip() {
-  window.$message.info('敬请期待~');
+  window.$message.info('敬请期待');
 }
 
 function changeMuted() {

+ 2 - 0
src/constant.ts

@@ -20,6 +20,8 @@ export const COOKIE_KEY = {
   loginInfo: 'loginInfo',
 };
 
+export const lsKeyPrefix = 'billd_live___';
+
 // 全局的localStorage的key
 export const lsKey = {
   lastBuildDate: 'lastBuildDate',

+ 62 - 0
src/hooks/loginModal/index copy.vue

@@ -0,0 +1,62 @@
+<template>
+  <div
+    v-if="show"
+    class="useTip-wrap"
+  >
+    <ModalCpt
+      :title="title"
+      :mask-closable="maskClosable"
+      @close="show = !show"
+    >
+      <div class="container">
+        <img
+          class="qq-logo"
+          src="@/assets/img/qq-logo.webp"
+          alt=""
+          @click="handleQQlogin"
+        />
+        <div>qq登录</div>
+      </div>
+
+      <template #footer></template>
+    </ModalCpt>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue';
+
+import ModalCpt from '@/components/Modal/index.vue';
+import { useQQLogin } from '@/hooks/use-login';
+
+export default defineComponent({
+  components: { ModalCpt },
+  setup() {
+    const title = ref('登录');
+    const show = ref(false);
+    const maskClosable = ref(true);
+    function handleQQlogin() {
+      show.value = !show.value;
+      useQQLogin();
+    }
+    return {
+      title,
+      show,
+      maskClosable,
+      handleQQlogin,
+    };
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.useTip-wrap {
+  .container {
+    text-align: center;
+    .qq-logo {
+      cursor: pointer;
+      width: 60px;
+    }
+  }
+}
+</style>

+ 4 - 1
src/hooks/use-login.ts

@@ -6,6 +6,7 @@ import { fullLoading } from '@/components/FullLoading';
 import { QQ_CLIENT_ID, QQ_OAUTH_URL, QQ_REDIRECT_URI } from '@/constant';
 import LoginModalCpt from '@/hooks/loginModal/index.vue';
 import { PlatformEnum } from '@/interface';
+import { useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
 import { clearLoginInfo, setLoginInfo } from '@/utils/cookie';
 import { getToken } from '@/utils/localStorage/user';
@@ -51,9 +52,11 @@ export async function handleLogin(e) {
 export function loginTip(show = false) {
   const token = getToken();
   instance.show = show;
+  const appStore = useAppStore();
   if (!token) {
     window.$message.warning('请先登录!');
-    instance.show = true;
+    // instance.show = true;
+    appStore.showLoginModal = true;
     return false;
   }
   return true;

+ 4 - 2
src/layout/pc/head/index.vue

@@ -232,7 +232,7 @@
         <div
           v-if="!userStore.userInfo"
           class="qqlogin"
-          @click="useQQLogin()"
+          @click="appStore.showLoginModal = true"
         >
           <div class="btn">登录</div>
         </div>
@@ -278,13 +278,15 @@ import Dropdown from '@/components/Dropdown/index.vue';
 import VPIconChevronDown from '@/components/icons/VPIconChevronDown.vue';
 import VPIconExternalLink from '@/components/icons/VPIconExternalLink.vue';
 import { APIFOX_URL, bilibiliCollectiondetail } from '@/constant';
-import { loginTip, useQQLogin } from '@/hooks/use-login';
+import { loginTip } from '@/hooks/use-login';
 import { IArea, LiveRoomTypeEnum } from '@/interface';
 import { routerName } from '@/router';
+import { useAppStore } from '@/store/app';
 import { useUserStore } from '@/store/user';
 
 const router = useRouter();
 const userStore = useUserStore();
+const appStore = useAppStore();
 const githubStar = ref('');
 const dropdownDoc = ref(false);
 const dropdownSys = ref(false);

+ 4 - 0
src/layout/pc/index.vue

@@ -7,15 +7,19 @@
     </router-view>
     <ModalCpt></ModalCpt>
     <SidebarCpt></SidebarCpt>
+    <LoginModal v-if="appStore.showLoginModal"></LoginModal>
   </div>
 </template>
 
 <script lang="ts" setup>
+import { useAppStore } from '@/store/app';
+
 import HeadCpt from './head/index.vue';
 import ModalCpt from './modal/index.vue';
 import SidebarCpt from './sidebar/index.vue';
 
 document.body.style.minWidth = '1200px';
+const appStore = useAppStore();
 </script>
 
 <style lang="scss" scoped>

+ 1 - 8
src/shims-vue.d.ts

@@ -5,12 +5,5 @@ declare module '*.vue' {
   export default component;
 }
 interface Window {
-  $message: {
-    info: any;
-    success: any;
-    warning: any;
-    error: any;
-    loading: any;
-    default: any;
-  };
+  $message: import('naive-ui/es/message/src/MessageProvider').MessageApiInjection;
 }

+ 2 - 0
src/store/app/index.ts

@@ -37,6 +37,7 @@ export type AppRootState = {
   }[];
   liveLine: LiveLineEnum;
   liveRoomInfo?: ILiveRoom;
+  showLoginModal: boolean;
 };
 
 export const useAppStore = defineStore('app', {
@@ -53,6 +54,7 @@ export const useAppStore = defineStore('app', {
       allTrack: [],
       liveLine: LiveLineEnum.hls,
       liveRoomInfo: undefined,
+      showLoginModal: false,
     };
   },
   actions: {

+ 3 - 2
src/store/cache/index.ts

@@ -1,5 +1,6 @@
 import { defineStore } from 'pinia';
 
+import { lsKeyPrefix } from '@/constant';
 import { AppRootState } from '@/store/app';
 
 export type PiniaCacheRootState = {
@@ -8,9 +9,9 @@ export type PiniaCacheRootState = {
   'resource-list': AppRootState['allTrack'];
 };
 
-export const usePiniaCacheStore = defineStore('pinia-cache', {
+export const usePiniaCacheStore = defineStore(`${lsKeyPrefix}pinia-cache`, {
   persist: {
-    key: 'pinia-cache',
+    key: `${lsKeyPrefix}pinia-cache`,
   },
   state: (): PiniaCacheRootState => {
     return {

+ 56 - 15
src/store/user/index.ts

@@ -1,21 +1,22 @@
 import { defineStore } from 'pinia';
 
-import { fetchUserInfo } from '@/api/user';
+import { fetchEmailCodeLogin, fetchRegister } from '@/api/emailUser';
+import { fetchLogin, fetchUserInfo } from '@/api/user';
 import { IRole, IUser } from '@/interface';
-import { clearToken, setToken } from '@/utils/localStorage/user';
+import cache from '@/utils/cache';
 
-type RootState = {
-  userInfo?: IUser;
-  token?: string;
-  roles?: IRole[];
+type UserRootState = {
+  userInfo: IUser | null;
+  token: string | null;
+  roles: IRole[] | null;
 };
 
 export const useUserStore = defineStore('user', {
-  state: (): RootState => {
+  state: (): UserRootState => {
     return {
-      userInfo: undefined,
-      token: undefined,
-      roles: [],
+      token: null,
+      roles: null,
+      userInfo: null,
     };
   },
   actions: {
@@ -23,17 +24,57 @@ export const useUserStore = defineStore('user', {
       this.userInfo = res;
     },
     setToken(res) {
-      setToken(res);
+      cache.setStorageExp('token', res, 24);
       this.token = res;
     },
     setRoles(res) {
       this.roles = res;
     },
     logout() {
-      clearToken();
-      this.token = undefined;
-      this.userInfo = undefined;
-      this.roles = [];
+      cache.clearStorage('token');
+      this.token = null;
+      this.userInfo = null;
+      this.roles = null;
+    },
+    async pwdLogin({ id, password }) {
+      try {
+        const { data: token } = await fetchLogin({
+          id,
+          password,
+        });
+        this.setToken(token);
+        return token;
+      } catch (error: any) {
+        // 错误返回401,全局的响应拦截会打印报错信息
+        return null;
+      }
+    },
+    async codeLogin({ email, code }) {
+      try {
+        const { data: token } = await fetchEmailCodeLogin({
+          email,
+          code,
+        });
+        this.setToken(token);
+        return token;
+      } catch (error: any) {
+        // 错误返回401,全局的响应拦截会打印报错信息
+        return null;
+      }
+    },
+    async register({ email, code }) {
+      try {
+        // @ts-ignore
+        const { data: token } = await fetchRegister({
+          email,
+          code,
+        });
+        this.setToken(token);
+        return { token };
+      } catch (error: any) {
+        window.$message.error(error.message);
+        return error;
+      }
     },
     async getUserInfo() {
       try {

+ 3 - 1
src/utils/cache.ts

@@ -1,3 +1,5 @@
 import { CacheModel } from 'billd-utils';
 
-export default new CacheModel();
+import { lsKeyPrefix } from '@/constant';
+
+export default new CacheModel(lsKeyPrefix);

+ 1 - 0
src/views/area/index.vue

@@ -10,6 +10,7 @@
       {{ item.name }}
     </div>
   </div>
+
   <router-view></router-view>
 </template>
 

+ 1 - 1
src/views/pull/index.vue

@@ -260,7 +260,7 @@ onMounted(() => {
 });
 
 function handlePay() {
-  window.$message.info('敬请期待~');
+  window.$message.info('敬请期待');
 }
 
 function handleRefresh() {