Преглед на файлове

Merge remote-tracking branch 'origin/dev_YWQ' into dev_czc

dev_YWQ
congzc преди 1 година
родител
ревизия
d7c53474d6
променени са 24 файла, в които са добавени 1736 реда и са изтрити 1137 реда
  1. +1
    -1
      .env.development
  2. +4
    -1
      .env.production
  3. +0
    -6
      .env.staging
  4. +8
    -0
      .env.test
  5. +3
    -3
      package.json
  6. +52
    -0
      src/apis/login.js
  7. +16
    -0
      src/apis/signSucceed.js
  8. Двоични данни
      src/assets/video/loginVideo.mp4
  9. +19
    -1
      src/lang/package/en.ts
  10. +19
    -2
      src/lang/package/zh-cn.ts
  11. +2
    -8
      src/layout/components/Navbar.vue
  12. +1
    -1
      src/layout/components/TagsView/index.vue
  13. +7
    -2
      src/layout/index.vue
  14. +1
    -0
      src/main.ts
  15. +48
    -48
      src/permission.ts
  16. +7
    -1
      src/router/index.ts
  17. +0
    -2
      src/types/auto-imports.d.ts
  18. +69
    -0
      src/utils/request.js
  19. +12
    -12
      src/utils/request.ts
  20. +414
    -162
      src/views/login/index.vue
  21. +2
    -2
      src/views/redirect/index.vue
  22. +168
    -0
      src/views/signSucceed/index.vue
  23. +3
    -5
      vite.config.ts
  24. +880
    -880
      yarn.lock

+ 1
- 1
.env.development Целия файл

@@ -5,4 +5,4 @@ NODE_ENV='development'

VITE_APP_TITLE = 'Metavatar-PC'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_API = 'https://photo.metavatar.cc/C'

+ 4
- 1
.env.production Целия файл

@@ -1,5 +1,8 @@
## 生产环境

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV='production'

VITE_APP_TITLE = 'Metavatar-PC'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/prod-api'
VITE_APP_BASE_API = 'https://photo.metavatar.cc/C'

+ 0
- 6
.env.staging Целия файл

@@ -1,6 +0,0 @@
## 模拟环境
NODE_ENV='staging'

VITE_APP_TITLE = 'vue3-element-admin'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/prod--api'

+ 8
- 0
.env.test Целия файл

@@ -0,0 +1,8 @@
## 开发环境

# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV='test'

VITE_APP_TITLE = 'Metavatar-PC'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = 'https://photo.metavatar.cc/C'

+ 3
- 3
package.json Целия файл

@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite serve --mode development",
"build:dev": "vite build --mode development &&vue-tsc --noEmit",
"build:dev": "vite build --mode test &&vue-tsc --noEmit",
"build:prod": "vite build --mode production &&vue-tsc --noEmit"
},
"config": {
@@ -27,7 +27,7 @@
"screenfull": "^6.0.0",
"vue": "^3.2.45",
"vue-i18n": "9",
"vue-router": "^4.1.6"
"vue-router": "^4.0.13"
},
"devDependencies": {
"@commitlint/cli": "^17.5.1",
@@ -71,4 +71,4 @@
"repository": "https://gitee.com/youlaiorg/vue3-element-admin.git",
"author": "有来开源组织",
"license": "MIT"
}
}

+ 52
- 0
src/apis/login.js Целия файл

@@ -0,0 +1,52 @@
import request from "@/utils/request.js";

const baseURL = import.meta.env.VITE_APP_BASE_API

/**
* @description 注册
* @params email,password
* @returns data
*/
export function register(data) {
return request({
url: `/api/user/doRegisterByEmail`,
method: 'post',
data
})
}

/**
* @description 登录
* @params email,password,code
* @returns data
*/
export function login(data) {
return request({
url: `/api/user/loginByEmail`,
method: 'post',
data
})
}

/**
/**
* @description:重新发送邮件
* @param {string} data
* @return: data
*/
export function sendRegisterEmail(email) {
return request({
url: `/api/user/sendRegisterEmail`,
method: 'post',
data: {
email
}
})
}

/**
* @description:获取图形验证码
* @return: 验证码地址
*/
export const getCodeImgUrl = baseURL + '/api/user/captcha.jpg'


+ 16
- 0
src/apis/signSucceed.js Целия файл

@@ -0,0 +1,16 @@
import request from "@/utils/request.js";

/**
* @description 通过ticket激活账户
* @params ticket
* @returns data
*/
export function activateByTicket(ticket) {
return request({
url: `/api/user/doActivation`,
method: 'post',
data: {
ticket
}
})
}

Двоични данни
src/assets/video/loginVideo.mp4 Целия файл


+ 19
- 1
src/lang/package/en.ts Целия файл

@@ -7,11 +7,17 @@ export default {

// 登录页面国际化
login: {
title: "Metavatar-PC Sign in",
title: "Metavatar-PC",
username: "Username",
email: "E-mail",
password: "Password",
confirmPassword: "Confirm Password",
login: "Sign in",
register: "Sign up",
loginBtn: "Sign in",
registerBtn: "Sign up",
verifyCode: "Verify Code",
signIn: "Already got an account? Sign in here",
signUp: "Don't have an account? Sign up here",
forgetPwd: "I forgot my password",
},
@@ -79,4 +85,16 @@ export default {
WeChatPay: "WeChatPay",
Alipay: "Alipay",
},
signSucceed: {
title: "Confirm your email",
succeed: "Sign Succeed !",
welcome: "Welcome to Metavatar !",
createSuccess:
"Your account has been created successfully. Enjoy yourself !",
expired:
"Your ticket is expired. You can click the button to resend new email then",
sendSuccess: "The email has been resent, you can check it now",
goLogin: "Login Now",
reSend: "Resend",
},
};

+ 19
- 2
src/lang/package/zh-cn.ts Целия файл

@@ -7,11 +7,17 @@ export default {

// 登录页面国际化
login: {
title: "Metavatar-PC 登录",
title: "Metavatar-PC",
username: "用户名",
email: "电子邮件",
password: "密码",
login: "登 录",
confirmPassword: "确认密码",
login: "登录",
register: "注册",
loginBtn: "登 录",
registerBtn: "注 册",
verifyCode: "验证码",
signIn: "已有账号?现在登录!",
signUp: "没有账号?现在加入!",
forgetPwd: "忘记密码",
},
@@ -79,4 +85,15 @@ export default {
WeChatPay: "微信支付",
Alipay: "支付宝支付",
},
signSucceed: {
title: "确认邮箱",
succeed: "注册成功",
welcome: "欢迎加入Metavatar !",
createSuccess: "您的账户已成功创建",
expired:
"您的通行密钥已过期。稍后,您可以点击下面的按钮重新发送一封新的邮件。",
sendSuccess: "您的邮件已重新发送,请注意查收。",
goLogin: "即将登录",
reSend: "重新发送",
},
};

+ 2
- 8
src/layout/components/Navbar.vue Целия файл

@@ -123,14 +123,8 @@ function logout() {
confirmButtonText: confirm,
cancelButtonText: cancel,
}).then(() => {
userStore
.logout()
.then(() => {
tagsViewStore.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
router.push(`/login`);
localStorage.removeItem("AccessToken");
});
}



+ 1
- 1
src/layout/components/TagsView/index.vue Целия файл

@@ -95,7 +95,7 @@ watch(
route,
() => {
addTags();
moveToCurrentTag();
// moveToCurrentTag();
},
{
//初始化立即执行


+ 7
- 2
src/layout/index.vue Целия файл

@@ -18,7 +18,7 @@
<!--主页面-->
<app-main />

<!-- 设置面板 -->
<!-- 设置面板 -->-
<RightPanel v-if="showSettings">
<settings />
</RightPanel>
@@ -30,12 +30,15 @@
import { computed, watchEffect } from "vue";
import { useWindowSize } from "@vueuse/core";
import { AppMain, Navbar, Settings, TagsView } from "./components/index";
import SignSucceed from "../views/signSucceed/index.vue";
import Sidebar from "./components/Sidebar/index.vue";
import RightPanel from "@/components/RightPanel/index.vue";

import { useAppStore } from "@/store/modules/app";
import { useSettingsStore } from "@/store/modules/settings";

import { useRoute, useRouter } from "vue-router";
const route = useRoute();
const { params } = route;
const { width } = useWindowSize();

/**
@@ -80,6 +83,8 @@ watchEffect(() => {
function handleOutsideClick() {
appStore.closeSideBar(false);
}

onMounted(() => {});
</script>

<style lang="scss" scoped>


+ 1
- 0
src/main.ts Целия файл

@@ -25,6 +25,7 @@ setupDirective(app);
setupStore(app);
// element
import ElementPlus from "element-plus";

app.use(ElementPlus);

app.use(router).use(i18n).mount("#app");

+ 48
- 48
src/permission.ts Целия файл

@@ -9,53 +9,53 @@ NProgress.configure({ showSpinner: false }); // 进度条
const permissionStore = usePermissionStoreHook();

// 白名单路由
const whiteList = ["/login"];
const whiteList = ["/login", "/signSucceed"];

router.beforeEach(async (to, from, next) => {
NProgress.start();
const hasToken = localStorage.getItem("accessToken");
if (hasToken) {
if (to.path === "/login") {
// 如果已登录,跳转首页
next({ path: "/" });
NProgress.done();
} else {
const userStore = useUserStoreHook();
const hasRoles = userStore.roles && userStore.roles.length > 0;
if (hasRoles) {
// 未匹配到任何路由,跳转404
if (to.matched.length === 0) {
from.name ? next({ name: from.name }) : next("/404");
} else {
next();
}
} else {
try {
const { roles } = await userStore.getInfo();
const accessRoutes = await permissionStore.generateRoutes(roles);
accessRoutes.forEach((route) => {
router.addRoute(route);
});
next({ ...to, replace: true });
} catch (error) {
// 移除 token 并跳转登录页
await userStore.resetToken();
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
}
} else {
// 未登录可以访问白名单页面
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
});
// router.beforeEach(async (to, from, next) => {
// NProgress.start();
// // const hasToken = localStorage.getItem("accessToken");
// // if (hasToken) {
// // if (to.path === "/login") {
// // // 如果已登录,跳转首页
// // next({ path: "/" });
// // NProgress.done();
// // } else {
// // const userStore = useUserStoreHook();
// // const hasRoles = userStore.roles && userStore.roles.length > 0;
// // if (hasRoles) {
// // // 未匹配到任何路由,跳转404
// // if (to.matched.length === 0) {
// // from.name ? next({ name: from.name }) : next("/404");
// // } else {
// // next();
// // }
// // } else {
// // try {
// // const { roles } = await userStore.getInfo();
// // const accessRoutes = await permissionStore.generateRoutes(roles);
// // accessRoutes.forEach((route) => {
// // router.addRoute(route);
// // });
// // next({ ...to, replace: true });
// // } catch (error) {
// // // 移除 token 并跳转登录页
// // await userStore.resetToken();
// // next(`/login?redirect=${to.path}`);
// // NProgress.done();
// // }
// // }
// // }
// // } else {
// // // 未登录可以访问白名单页面
// // if (whiteList.indexOf(to.path) !== -1) {
// // next();
// // } else {
// // next(`/login?redirect=${to.path}`);
// // NProgress.done();
// // }
// // }
// });

router.afterEach(() => {
NProgress.done();
});
// router.afterEach(() => {
// NProgress.done();
// });

+ 7
- 1
src/router/index.ts Целия файл

@@ -22,6 +22,11 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import("@/views/login/index.vue"),
meta: { hidden: true },
},
{
path: "/signSucceed",
component: () => import("@/views/signSucceed/index.vue"),
meta: { hidden: true },
},
// 创建视频
{
path: "/",
@@ -78,7 +83,7 @@ export const constantRoutes: RouteRecordRaw[] = [
},
},
],
},// 修改密码
}, // 修改密码
{
path: "/",
component: Layout,
@@ -116,6 +121,7 @@ export const constantRoutes: RouteRecordRaw[] = [
},
],
},

// {
// path: "/dashboard",
// component: Layout,


+ 0
- 2
src/types/auto-imports.d.ts Целия файл

@@ -5,7 +5,6 @@ declare global {
const ElForm: typeof import('element-plus/es')['ElForm']
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const ElTree: typeof import('element-plus/es')['ElTree']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const computed: typeof import('vue')['computed']
@@ -275,7 +274,6 @@ declare module 'vue' {
readonly ElForm: UnwrapRef<typeof import('element-plus/es')['ElForm']>
readonly ElMessage: UnwrapRef<typeof import('element-plus/es')['ElMessage']>
readonly ElMessageBox: UnwrapRef<typeof import('element-plus/es')['ElMessageBox']>
readonly ElTree: UnwrapRef<typeof import('element-plus/es')['ElTree']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>


+ 69
- 0
src/utils/request.js Целия файл

@@ -0,0 +1,69 @@
import axios from 'axios'
import router from '@/router'
import { ElMessage } from 'element-plus'
ElMessage

// 开发环境需以 "/api" 拼接,其他环境需以 "/C" 拼接
const env = process.env.NODE_ENV == 'development' ? "/api" : "/C"
console.log(process.env.NODE_ENV, 'env')

/**
* @description:发送Axios请求
*/
const request = axios.create({
baseURL: env,
timeout: 5000
})

// 请求拦截器
request.interceptors.request.use(
function (config) {
// 给所有授权的请求提供token
const AccessToken = localStorage.getItem("AccessToken")
if (AccessToken) {
config.headers.token = AccessToken
}
return config
},
function (error) {
// 操作
return Promise.reject(error)
}
)

// 响应拦截器
request.interceptors.response.use(
function (response) {
let code = response.data.code
// 当token过期或损坏时
if (code == 1052) {
router.push("/login");
ElMessage.error("登录过期,请重新登录!");
return

// 弹出错误
} else if (code != 200) {
if (code == 1053) {
router.push("/login");
// Toast.fail("请您先登录!");
return

} else if (code == 500) {
ElMessage.error("系统异常,请稍后再试");

// 其他错误
} else {

return Promise.reject(response.data)
}
} else {
// 正常返回
return response.data
}

},
function (error) {
}
)

export default request

+ 12
- 12
src/utils/request.ts Целия файл

@@ -1,11 +1,11 @@
import axios, { InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { useUserStoreHook } from '@/store/modules/user';
import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
import { useUserStoreHook } from "@/store/modules/user";

// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }
headers: { "Content-Type": "application/json;charset=utf-8" },
});

// 请求拦截器
@@ -26,7 +26,7 @@ service.interceptors.request.use(
service.interceptors.response.use(
(response: AxiosResponse) => {
const { code, msg } = response.data;
if (code === '00000') {
if (code === "00000") {
return response.data;
}
// 响应数据为二进制流处理(Excel导出)
@@ -34,23 +34,23 @@ service.interceptors.response.use(
return response;
}

ElMessage.error(msg || '系统出错');
return Promise.reject(new Error(msg || 'Error'));
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Error"));
},
(error: any) => {
if (error.response.data) {
const { code, msg } = error.response.data;
// token 过期,重新登录
if (code === 'A0230') {
ElMessageBox.confirm('当前页面已失效,请重新登录', '提示', {
confirmButtonText: '确定',
type: 'warning'
if (code === "A0230") {
ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
confirmButtonText: "确定",
type: "warning",
}).then(() => {
localStorage.clear();
window.location.href = '/';
window.location.href = "/";
});
} else {
ElMessage.error(msg || '系统出错');
ElMessage.error(msg || "系统出错");
}
}
return Promise.reject(error.message);


+ 414
- 162
src/views/login/index.vue Целия файл

@@ -1,8 +1,9 @@
<template>
<div class="login-container">
<div class="Version">Version 1.0.4 test</div>
<div class="videoBox">
<video
src="../../assets/video/loginVideo.mp4"
src="https://video.metavatar.cc/sv/4aaaaa5e-18832dbb5e6/4aaaaa5e-18832dbb5e6.mp4"
muted
autoplay
loop
@@ -12,54 +13,50 @@
v-if="isLogin"
ref="loginFormRef"
:model="loginData"
:rules="loginRules"
class="login-form"
>
<div class="flex text-white items-center py-4">
<span class="text-2xl flex-1 text-center">{{ $t("login.title") }}</span>
<span class="text-2xl flex-1 text-center">{{
$t("login.title") + " " + $t("login.login")
}}</span>
<lang-select style="color: #fff" />
</div>

<el-form-item prop="username">
<el-form-item prop="email">
<div class="p-2 text-white">
<svg-icon icon-class="user" />
</div>
<el-input
class="flex-1"
ref="username"
ref="email"
size="large"
v-model="loginData.username"
:placeholder="$t('login.username')"
name="username"
v-model="loginData.email"
:placeholder="$t('login.email')"
name="email"
/>
</el-form-item>

<el-tooltip
:disabled="isCapslock === false"
content="Caps lock is On"
placement="right"
>
<el-form-item prop="password">
<span class="p-2 text-white">
<svg-icon icon-class="password" />
</span>
<el-input
class="flex-1"
v-model="loginData.password"
placeholder="密码"
:type="passwordVisible === false ? 'password' : 'input'"
size="large"
name="password"
@keyup="checkCapslock"
@keyup.enter="handleLogin"
<el-form-item prop="password">
<span class="p-2 text-white">
<svg-icon icon-class="password" />
</span>
<el-input
class="flex-1"
v-model="loginData.password"
:placeholder="$t('login.password')"
:type="passwordVisible === false ? 'password' : 'input'"
size="large"
name="password"
@keyup.enter="handleLogin"
/>
<span class="mr-2" @click="passwordVisible = !passwordVisible">
<svg-icon
:icon-class="passwordVisible === false ? 'eye' : 'eye-open'"
class="text-white cursor-pointer"
/>
<span class="mr-2" @click="passwordVisible = !passwordVisible">
<svg-icon
:icon-class="passwordVisible === false ? 'eye' : 'eye-open'"
class="text-white cursor-pointer"
/>
</span>
</el-form-item>
</el-tooltip>
</span>
</el-form-item>

<!-- 验证码 -->
<el-form-item prop="verifyCode">
@@ -70,13 +67,13 @@
v-model="loginData.verifyCode"
auto-complete="off"
:placeholder="$t('login.verifyCode')"
class="w-[60%]"
class="flex-1"
@keyup.enter="handleLogin"
/>

<div class="captcha">
<img :src="captchaBase64" @click="getCaptcha" />
</div>
<span class="captcha">
<img v-if="showCodeImg" :src="verifyCodeUrl" @click="getCode" />
</span>
</el-form-item>

<el-button
@@ -85,11 +82,28 @@
type="primary"
class="loginBtn"
@click.prevent="handleLogin"
>{{ $t("login.login") }}
>
<el-icon
v-if="isLogining"
style="margin-right: 10px"
class="is-loading"
>
<svg
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
data-v-ea893728=""
>
<path
fill="currentColor"
d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32zm0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32zm448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32zm-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32zM195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0zm-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z"
></path>
</svg>
</el-icon>
{{ $t("login.loginBtn") }}
</el-button>

<div :class="currentLan == 'zh-cn' ? 'bottomBtnCN' : 'bottomBtnEN'">
<div style="margin-bottom: 5px">
<div style="margin-bottom: 5px" @click="loginFlag">
<span class="join">{{ $t("login.signUp") }}</span>
</div>
<div>
@@ -101,72 +115,70 @@
<el-form
v-if="!isLogin"
ref="loginFormRef"
:model="loginData"
:model="loginData2"
:rules="loginRules"
class="login-form"
>
<div class="flex text-white items-center py-4">
<span class="text-2xl flex-1 text-center">{{ $t("login.title") }}</span>
<span class="text-2xl flex-1 text-center">{{
$t("login.title") + " " + $t("login.register")
}}</span>
<lang-select style="color: #fff" />
</div>

<el-form-item prop="username">
<el-form-item prop="email">
<div class="p-2 text-white">
<svg-icon icon-class="user" />
</div>
<el-input
class="flex-1"
ref="username"
ref="email"
size="large"
v-model="loginData.username"
:placeholder="$t('login.username')"
name="username"
v-model="loginData2.email"
:placeholder="$t('login.email')"
name="email"
/>
</el-form-item>

<el-tooltip
:disabled="isCapslock === false"
content="Caps lock is On"
placement="right"
>
<el-form-item prop="password">
<span class="p-2 text-white">
<svg-icon icon-class="password" />
</span>
<el-input
class="flex-1"
v-model="loginData.password"
placeholder="密码"
:type="passwordVisible === false ? 'password' : 'input'"
size="large"
name="password"
@keyup="checkCapslock"
@keyup.enter="handleLogin"
<el-form-item prop="password">
<span class="p-2 text-white">
<svg-icon icon-class="password" />
</span>
<el-input
class="flex-1"
v-model="loginData2.password"
:placeholder="$t('login.password')"
:type="passwordVisible === false ? 'password' : 'input'"
size="large"
name="password"
/>
<span class="mr-2" @click="passwordVisible = !passwordVisible">
<svg-icon
:icon-class="passwordVisible === false ? 'eye' : 'eye-open'"
class="text-white cursor-pointer"
/>
<span class="mr-2" @click="passwordVisible = !passwordVisible">
<svg-icon
:icon-class="passwordVisible === false ? 'eye' : 'eye-open'"
class="text-white cursor-pointer"
/>
</span>
</el-form-item>
</el-tooltip>
</span>
</el-form-item>

<!-- 验证码 -->
<el-form-item prop="verifyCode">
<el-form-item prop="confirmPassword">
<span class="p-2 text-white">
<svg-icon icon-class="verify_code" />
<svg-icon icon-class="password" />
</span>
<el-input
v-model="loginData.verifyCode"
auto-complete="off"
:placeholder="$t('login.verifyCode')"
class="w-[60%]"
@keyup.enter="handleLogin"
class="flex-1"
v-model="loginData2.confirmPassword"
:placeholder="$t('login.confirmPassword')"
:type="passwordVisible === false ? 'password' : 'input'"
size="large"
name="confirmPassword"
@keyup.enter="handleRegister"
/>

<div class="captcha">
<img :src="captchaBase64" @click="getCaptcha" />
</div>
<span class="mr-2" @click="passwordVisible = !passwordVisible">
<svg-icon
:icon-class="passwordVisible === false ? 'eye' : 'eye-open'"
class="text-white cursor-pointer"
/>
</span>
</el-form-item>

<el-button
@@ -174,13 +186,30 @@
:loading="loading"
type="primary"
class="loginBtn"
@click.prevent="handleLogin"
>{{ $t("login.login") }}
@click.prevent="handleRegister"
>
<el-icon
v-if="isRegistering"
style="margin-right: 10px"
class="is-loading"
>
<svg
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
data-v-ea893728=""
>
<path
fill="currentColor"
d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32zm0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32zm448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32zm-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32zM195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0zm-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z"
></path>
</svg>
</el-icon>
{{ $t("login.registerBtn") }}
</el-button>

<div :class="currentLan == 'zh-cn' ? 'bottomBtnCN' : 'bottomBtnEN'">
<div style="margin-bottom: 5px" @click="loginFlag">
<span class="join">{{ $t("login.signUp") }}</span>
<span class="join">{{ $t("login.signIn") }}</span>
</div>
<div>
<span class="join">{{ $t("login.forgetPwd") }}</span>
@@ -191,36 +220,36 @@
</template>

<script setup lang="ts">
import router from "@/router";
import LangSelect from "@/components/LangSelect/index.vue";
import SvgIcon from "@/components/SvgIcon/index.vue";

import {
register,
login,
sendRegisterEmail,
getCodeImgUrl,
} from "@/apis/login";
import axios from "axios";
// 状态管理依赖
import { useUserStore } from "@/store/modules/user";

// API依赖
import { LocationQuery, LocationQueryValue, useRoute } from "vue-router";
import { getCaptchaApi } from "@/api/auth";
import { LoginData } from "@/api/auth/types";

import {
LocationQuery,
LocationQueryValue,
useRoute,
useRouter,
} from "vue-router";
import { useI18n } from "vue-i18n";
const { locale } = useI18n();

const { locale } = useI18n();
const currentLan = ref(locale);

const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
/**
* 按钮loading
*/
const loading = ref(false);

/**
* 是否大写锁定
*/
const isCapslock = ref(false);

/**
* 密码是否可见
*/
@@ -229,106 +258,313 @@ const passwordVisible = ref(false);
/**
* 验证码图片Base64字符串
*/
const captchaBase64 = ref();
const verifyCodeUrl = ref();

/**
* 登录/注册
*/
const isLogin = ref(true);

const showCodeImg = ref(true);
/**
* 登录表单引用
*/
const loginFormRef = ref(ElForm);

const loginData = ref<LoginData>({
username: "admin",
password: "123456",
const isLogining = ref(false);
const isRegistering = ref(false);

const loginData = ref({
email: "",
password: "",
verifyCode: "",
});

const loginData2 = ref({
email: "",
password: "",
confirmPassword: "",
});

const loginRules = {
username: [{ required: true, trigger: "blur" }],
const loginRules = ref({
email: [
{
required: true,
trigger: "blur",
validator: emailValidator,
},
],
password: [{ required: true, trigger: "blur", validator: passwordValidator }],
verifyCode: [{ required: true, trigger: "blur" }],
};
confirmPassword: [
{ required: true, trigger: "blur", validator: confirmPasswordValidator },
],
verifyCode: [
{
required: true,
trigger: "blur",
message:
currentLan.value == "zh-cn"
? "请输入验证码"
: "Please input verifyCode",
},
],
});

watch(currentLan, (newValue, oldValue) => {
const loginRule = loginRules.value;
loginRule.verifyCode[0].message =
newValue == "zh-cn" ? "请输入验证码" : "Please input verifyCode";
setTimeout(() => {
loginFormRef.value.resetFields();
getInfo();
}, 100);
// 切换语言刷新验证码
getCode();
});

/**
* @description: 切换登录/注册状态
*/
function loginFlag() {
isLogin.value = !isLogin;
console.log(isLogin.value, "isLogin.value");
isLogin.value = !isLogin.value;
// 切换登录/注册状态刷新验证码
getCode();
setTimeout(() => {
loginFormRef.value.resetFields();
}, 100);
}

/**
* 密码校验器
* @description: 将账户/密码存储在本地
* @param:email,password
*/
function passwordValidator(rule: any, value: any, callback: any) {
if (value.length < 6) {
callback(new Error("The password can not be less than 6 digits"));
function saveInfo(data: any) {
const userInfo = JSON.stringify({
email: data.email,
password: data.password,
});
// 将用户信息加密
const enCodeUserInfo = encodeURI(userInfo);
localStorage.setItem("userInfo", enCodeUserInfo);
}

/**
* @description: 将存储在本地的账户/密码恢复至登录表单
*/
function getInfo() {
const data = localStorage.getItem("userInfo");
// 将用户信息解密
const deCodeUserInfo = decodeURI(data as any);
const userInfo = JSON.parse(deCodeUserInfo);
if (userInfo) {
loginData.value.email = userInfo.email ? userInfo.email : "";
loginData.value.password = userInfo.password ? userInfo.password : "";
}
}

/**
* 邮箱校验器
*/
function emailValidator(rule: any, value: any, callback: any) {
const msg =
currentLan.value == "zh-cn"
? "邮箱不能为空!"
: "The E-amil can not be empty";

const msg2 =
currentLan.value == "zh-cn" ? "邮箱格式不正确!" : "Incorrect email format";

const emailReg =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

if (!value) {
callback(new Error(msg));
} else if (!emailReg.test(value)) {
callback(new Error(msg2));
} else {
callback();
}
}

/**
* 检查输入大小写状态
* 密码校验器
*/
function checkCapslock(e: any) {
const { key } = e;
isCapslock.value = key && key.length === 1 && key >= "A" && key <= "Z";
function passwordValidator(rule: any, value: any, callback: any) {
const msg =
currentLan.value == "zh-cn"
? "密码不能为空!"
: "The password can not be empty";

const msg2 =
currentLan.value == "zh-cn"
? "密码至少为6位!"
: "The password can not be less than 6 digits";
if (!value) {
callback(new Error(msg));
} else if (value.length < 6) {
callback(new Error(msg2));
} else {
callback();
}
}

/**
* 获取验证码
* 确认密码校验器
*/
function getCaptcha() {
getCaptchaApi().then(({ data }) => {
const { verifyCodeBase64, verifyCodeKey } = data;
loginData.value.verifyCodeKey = verifyCodeKey;
captchaBase64.value = verifyCodeBase64;
function confirmPasswordValidator(rule: any, value: any, callback: any) {
const msg =
currentLan.value == "zh-cn"
? "确认密码不能为空!"
: "The Confirm password can not be empty";
const msg2 =
currentLan.value == "zh-cn"
? "两次输入密码不一致!"
: "Confirm Passward entered should match";

if (!value) {
callback(new Error(msg));
} else if (value !== loginData2.value.password) {
callback(new Error(msg2));
} else {
callback();
}
}

// 注册行为
function handleRegister() {
loginFormRef.value.validate((val: any) => {
if (val) {
const data = {
email: loginData2.value.email,
password: loginData2.value.password,
};
isRegistering.value = true;
doRegister(data);
} else {
const msg =
currentLan.value == "zh-cn"
? "请检查信息格式!"
: "Please check the format !";
ElMessage.error(msg);
return;
}
});
}

/**
* 登录
*/
// 登录行为
function handleLogin() {
loginFormRef.value.validate((valid: boolean) => {
if (valid) {
loading.value = true;
userStore
.login(loginData.value)
.then(() => {
const query: LocationQuery = route.query;

const redirect = (query.redirect as LocationQueryValue) ?? "/";

const otherQueryParams = Object.keys(query).reduce(
(acc: any, cur: string) => {
if (cur !== "redirect") {
acc[cur] = query[cur];
}
return acc;
},
{}
);

router.push({ path: redirect, query: otherQueryParams });
})
.catch(() => {
// 验证失败,重新生成验证码
getCaptcha();
})
.finally(() => {
loading.value = false;
});
loginFormRef.value.validate((val: any) => {
if (val) {
const data = {
email: loginData.value.email,
password: loginData.value.password,
code: loginData.value.verifyCode,
};
isLogining.value = true;
doLogin(data);
} else {
const msg =
currentLan.value == "zh-cn"
? "请检查信息格式!"
: "Please check the format !";
ElMessage.error(msg);
return;
}
});
}

function getCode() {
showCodeImg.value = false;
axios({
url: getCodeImgUrl,
method: "get",
}).then(() => {
verifyCodeUrl.value = getCodeImgUrl;
showCodeImg.value = true;
});
}

/**
* @description:注册
* @param {Object} data
* @return: data
*/
async function doRegister(data: object) {
const msg =
currentLan.value == "zh-cn"
? "确认信息已发送至您的邮箱,请注意查收"
: "An email has been send to you, Please pay attention to confirm";
await register(data)
.then((res) => {
console.log(res, "res");
isRegistering.value = false;
ElMessage.success(msg);
// 注册成功将账号密码保存到本地
saveInfo(data);
})
.catch((err) => {
// 注册失败刷新验证码
getCode();
isRegistering.value = false;
console.log(err, "err");
ElMessage.error(err.message);
});
}

/**
* @description:登录
* @param {Object} data
* @return: data
*/
async function doLogin(data: any) {
const msg =
currentLan.value == "zh-cn" ? "登录成功!" : "Welcome to Metavatar!";
await login(data)
.then((res) => {
console.log(res, "res");
localStorage.setItem("AccessToken", res.data.token);
ElMessage.success(msg);
router.push("/createVideo");
// 注册成功将账号密码保存到本地
saveInfo(data);
isLogining.value = false;
})
.catch((err) => {
console.log(err, "err");
// 登录失败刷新验证码
getCode();
ElMessage.error(err.message);
isLogining.value = false;
if (err.code == 2112) {
reSendEmail(data.email);
}
});
}

/**
* @description:重新发送邮件
* @param {string} email
* @return: data
*/
async function reSendEmail(email: string) {
const msg =
currentLan.value == "zh-cn"
? "确认信息已发送至您的邮箱,请注意查收!"
: "An email has been send to you, Please pay attention to confirm !";
await sendRegisterEmail(email)
.then((res) => {
console.log(res, "res");
ElMessage.success(msg);
})
.catch((err) => {
console.log(err, "err");
ElMessage.error(err.message);
});
}

onMounted(() => {
getCaptcha();
getCode();
getInfo();
});
</script>

@@ -340,6 +576,13 @@ onMounted(() => {
overflow: hidden;
// background-color: #2d3a4b;

.Version {
position: absolute;
bottom: 20px;
right: 20px;
color: #ffffff;
}

.login-form {
position: absolute;
top: 46%;
@@ -348,7 +591,7 @@ onMounted(() => {
max-width: 100%;
padding: 20px;
overflow: hidden;
backdrop-filter: blur(5px);
backdrop-filter: blur(3px);
background: linear-gradient(
123.64deg,
rgba(188, 193, 196, 0.1) -22.71%,
@@ -357,6 +600,7 @@ onMounted(() => {
border-radius: 20px;
transform: translateY(-50%);
border: 1px solid #cacaca;
transition: all 0.15s;

.captcha {
position: absolute;
@@ -369,6 +613,9 @@ onMounted(() => {
cursor: pointer;
}
}
&:hover {
backdrop-filter: blur(7px);
}
}
}

@@ -423,10 +670,15 @@ onMounted(() => {
.videoBox {
position: absolute;
top: 0;
z-index: -1;
left: 0;
width: 100%;
height: 100%;
opacity: 0.8;
z-index: -1;
video {
width: 100%;
height: 100%;
object-fit: fill;
}
}

.el-form-item {


+ 2
- 2
src/views/redirect/index.vue Целия файл

@@ -3,7 +3,7 @@
</template>

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { useRoute, useRouter } from "vue-router";

const route = useRoute();
const router = useRouter();
@@ -11,5 +11,5 @@ const router = useRouter();
const { params, query } = route;
const { path } = params;

router.replace({ path: '/' + path, query });
router.replace({ path: "/" + path, query });
</script>

+ 168
- 0
src/views/signSucceed/index.vue Целия файл

@@ -0,0 +1,168 @@
<template>
<div class="content">
<div class="logo" @click="goLogin">
<img src="../../assets/img/left-top-logo.png" alt="" />
</div>

<div class="lanSelect">
<lang-select
class="navItem"
style="color: #ffffff; transform: scale(2)"
/>
</div>
<div v-if="isSuccess == 1">
<div class="welcome">{{ $t("signSucceed.welcome") }}</div>
<div class="success">{{ $t("signSucceed.createSuccess") }}</div>
<div class="login pointer" @click="goLogin">
{{ $t("signSucceed.goLogin") }}
</div>
</div>

<div v-if="isSuccess == 2">
<div class="welcome">{{ $t("signSucceed.welcome") }}</div>
<div class="success">
{{ $t("signSucceed.expired") }}
</div>
<div class="login pointer" @click="reSend">
{{ $t("signSucceed.reSend") }}
</div>
</div>

<div v-if="isSuccess == 3">
<div class="welcome">{{ $t("signSucceed.welcome") }}</div>
<div class="success">{{ $t("signSucceed.sendSuccess") }}</div>
</div>
</div>
</template>

<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
import { activateByTicket } from "@/apis/signSucceed";
import { sendRegisterEmail } from "@/apis/login";
import { useI18n } from "vue-i18n";
import LangSelect from "@/components/LangSelect/index.vue";

const { locale } = useI18n();
const currentLan = ref(locale);
const route = useRoute();
const router = useRouter();
const { params, query } = route;
const isSuccess = ref(0);

function doActivateByTicket() {
const ticket = query.ticket;
activateByTicket(ticket)
.then((res) => {
console.log(res, "res");
localStorage.setItem("AccessToken", res.data.token);
isSuccess.value = 1;
})
.catch((err) => {
console.log(err, "err");
isSuccess.value = 2;
});
}

function goLogin() {
const AccessToken = localStorage.getItem("AccessToken");
if (AccessToken) {
router.push("/createVideo");
} else {
router.push("/login");
}
}

function reSend() {
const email = query.email;
reSendEmail(email as string);
}

/**
* @description:重新发送邮件
* @param {string} email
* @return: data
*/
async function reSendEmail(email: string) {
const msg =
currentLan.value == "zh-cn"
? "确认信息已发送至您的邮箱,请注意查收!"
: "An email has been send to you, Please pay attention to confirm !";
await sendRegisterEmail(email)
.then((res) => {
console.log(res, "res");
ElMessage.success(msg);
isSuccess.value = 3;
})
.catch((err) => {
console.log(err, "err");
if (err.code == 2113) {
ElMessage.success(err.message);
router.push("/login");
return;
}
ElMessage.error(err.message);
router.push("/login");
});
}

onMounted(() => {
console.log(query, "query");
if (query.ticket || query.email) {
doActivateByTicket();
} else {
router.push("/login");
}
});
</script>

<style lang="scss" scoped>
.content {
position: relative;
height: 1000px;
padding: 100px 0 0 40px;
background-color: #000000;
color: #ffffff;
.logo {
position: absolute;
top: 20px;
left: 20px;
width: 200px;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}

.lanSelect {
position: absolute;
top: 40px;
right: 40px;
}

.success {
font-size: 25px;
text-align: center;
margin-bottom: 28px;
}
.welcome {
font-size: 30px;
text-align: center;
margin: 28px;
}
.login {
width: 200px;
height: 50px;
line-height: 50px;
text-align: center;
color: #ffffff;
background: linear-gradient(270deg, #4b61dc, #15d1b8);
border-radius: 10px;
transition: all 0.3s;
margin: auto;
&:hover {
transform: scale(1.1);
}
}
}
</style>

+ 3
- 5
vite.config.ts Целия файл

@@ -42,12 +42,10 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
open: true, // 运行是否自动打开浏览器
proxy: {
// 反向代理解决跨域
[env.VITE_APP_BASE_API]: {
target: "http://vapi.youlai.tech", // 线上接口地址
// target: 'http://localhost:8989', // 本地接口地址
"/api": {
target: env.VITE_APP_BASE_API, // 线上接口地址
changeOrigin: true,
rewrite: (path) =>
path.replace(new RegExp("^" + env.VITE_APP_BASE_API), ""), // 替换 /dev-api 为 target 接口地址
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},


+ 880
- 880
yarn.lock
Файловите разлики са ограничени, защото са твърде много
Целия файл


Зареждане…
Отказ
Запис