feat: oauth登录功能
This commit is contained in:
@@ -29,6 +29,14 @@ const coreRoutes: RouteRecordRaw[] = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
redirect: DEFAULT_HOME_PATH,
|
redirect: DEFAULT_HOME_PATH,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: () => import('#/views/_core/social-callback/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: $t('page.core.oauthLogin'),
|
||||||
|
},
|
||||||
|
name: 'OAuthRedirect',
|
||||||
|
path: '/social-callback',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: AuthPageLayout,
|
component: AuthPageLayout,
|
||||||
meta: {
|
meta: {
|
||||||
|
@@ -3,13 +3,14 @@ import { onMounted, ref } from 'vue';
|
|||||||
|
|
||||||
import { AuthenticationLogin } from '@vben/common-ui';
|
import { AuthenticationLogin } from '@vben/common-ui';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { omit } from 'lodash-es';
|
import { omit } from 'lodash-es';
|
||||||
|
|
||||||
import { tenantList, type TenantResp } from '#/api';
|
import { tenantList, type TenantResp } from '#/api';
|
||||||
import { captchaImage, type CaptchaResponse } from '#/api/core/captcha';
|
import { captchaImage, type CaptchaResponse } from '#/api/core/captcha';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
import OauthLogin from './oauth-login.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'Login' });
|
defineOptions({ name: 'Login' });
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
@@ -71,18 +72,6 @@ async function handleAccountLogin(values: LoginForm) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOauthLogin(provider: string) {
|
|
||||||
switch (provider) {
|
|
||||||
case 'gitee': {
|
|
||||||
message.success('todo gitee login');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
message.warn('暂不支持该登录方式');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -97,7 +86,10 @@ function handleOauthLogin(provider: string) {
|
|||||||
password-placeholder="密码"
|
password-placeholder="密码"
|
||||||
username-placeholder="用户名"
|
username-placeholder="用户名"
|
||||||
@captcha-click="loadCaptcha"
|
@captcha-click="loadCaptcha"
|
||||||
@oauth-login="handleOauthLogin"
|
|
||||||
@submit="handleAccountLogin"
|
@submit="handleAccountLogin"
|
||||||
/>
|
>
|
||||||
|
<template #third-party-login>
|
||||||
|
<OauthLogin />
|
||||||
|
</template>
|
||||||
|
</AuthenticationLogin>
|
||||||
</template>
|
</template>
|
||||||
|
45
apps/web-antd/src/views/_core/authentication/oauth-login.vue
Normal file
45
apps/web-antd/src/views/_core/authentication/oauth-login.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { createIconifyIcon } from '@vben/icons';
|
||||||
|
import { $t } from '@vben/locales';
|
||||||
|
|
||||||
|
import { Col, Row, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { accountBindList } from '../oauth-common';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'OAuthLogin',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有action方法才会显示
|
||||||
|
*/
|
||||||
|
const clientList = accountBindList.filter((item) => item.action);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full sm:mx-auto md:max-w-md">
|
||||||
|
<div class="mt-4 flex items-center justify-between">
|
||||||
|
<span class="border-input w-[35%] border-b dark:border-gray-600"></span>
|
||||||
|
<span class="text-muted-foreground text-center text-xs uppercase">
|
||||||
|
{{ $t('authentication.thirdPartyLogin') }}
|
||||||
|
</span>
|
||||||
|
<span class="border-input w-[35%] border-b dark:border-gray-600"></span>
|
||||||
|
</div>
|
||||||
|
<Row class="enter-x flex items-center justify-evenly">
|
||||||
|
<!-- todo 这里在点击登录时要disabled -->
|
||||||
|
<Col v-for="item in clientList" :key="item.key" :span="4" class="my-2">
|
||||||
|
<Tooltip :title="`${item.title}登录`">
|
||||||
|
<span class="flex cursor-pointer items-center justify-center">
|
||||||
|
<component
|
||||||
|
:is="createIconifyIcon(item.avatar)"
|
||||||
|
v-if="item.avatar"
|
||||||
|
:style="{ color: item.color }"
|
||||||
|
class="size-[24px]"
|
||||||
|
@click="item.action"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</template>
|
94
apps/web-antd/src/views/_core/oauth-common.ts
Normal file
94
apps/web-antd/src/views/_core/oauth-common.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { authBinding } from '#/api/core/auth';
|
||||||
|
/**
|
||||||
|
* @description: 菜单
|
||||||
|
* @param key key
|
||||||
|
* @param title 标题
|
||||||
|
* @param description 描述
|
||||||
|
* @param extra 按钮文字
|
||||||
|
* @param avatar 图标
|
||||||
|
* @param color 图标颜色可直接写英文颜色/hex
|
||||||
|
*/
|
||||||
|
export interface ListItem {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
extra?: string;
|
||||||
|
avatar?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 绑定账号
|
||||||
|
* @param source 来源 如gitee github 与后端的social-callback?source=xxx对应
|
||||||
|
* @param bound 是否已经绑定
|
||||||
|
* @param action 账号绑定回调
|
||||||
|
*/
|
||||||
|
export interface BindItem extends ListItem {
|
||||||
|
source: string;
|
||||||
|
bound?: boolean;
|
||||||
|
action?: (source: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* todo tenantId
|
||||||
|
* 绑定授权从userStore.userInfo获取
|
||||||
|
* 登录从localStorage获取
|
||||||
|
* @param source
|
||||||
|
*/
|
||||||
|
async function handleAuthBinding(source: string) {
|
||||||
|
const tenantId = localStorage.getItem('__oauth_tenant_id') ?? '000000';
|
||||||
|
// 这里返回打开授权页面的链接
|
||||||
|
const href = await authBinding(source, tenantId);
|
||||||
|
window.location.href = href;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号绑定 list
|
||||||
|
* 添加账号绑定只需要在这里增加即可
|
||||||
|
* 添加过的项目会在个人主页-绑定账号中显示
|
||||||
|
* action不为空的会在登录页显示
|
||||||
|
*/
|
||||||
|
export const accountBindList: BindItem[] = [
|
||||||
|
{
|
||||||
|
avatar: 'ri:taobao-fill',
|
||||||
|
color: '#ff4000',
|
||||||
|
description: '绑定淘宝账号',
|
||||||
|
key: '1',
|
||||||
|
source: 'taobao',
|
||||||
|
title: '淘宝',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'fa-brands:alipay',
|
||||||
|
color: '#2eabff',
|
||||||
|
description: '绑定支付宝账号',
|
||||||
|
key: '2',
|
||||||
|
source: 'alipay',
|
||||||
|
title: '支付宝',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
avatar: 'ri:dingding-fill',
|
||||||
|
color: '#2eabff',
|
||||||
|
description: '绑定钉钉账号',
|
||||||
|
key: '3',
|
||||||
|
source: 'ding',
|
||||||
|
title: '钉钉',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: () => handleAuthBinding('gitee'),
|
||||||
|
avatar: 'simple-icons:gitee',
|
||||||
|
color: '#c71d23',
|
||||||
|
description: '绑定GITEE账号',
|
||||||
|
key: '4',
|
||||||
|
source: 'gitee',
|
||||||
|
title: 'GITEE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: () => handleAuthBinding('github'),
|
||||||
|
avatar: 'uiw:github',
|
||||||
|
color: '',
|
||||||
|
description: '绑定GITHUB账号',
|
||||||
|
key: '5',
|
||||||
|
source: 'github',
|
||||||
|
title: 'GITHUB',
|
||||||
|
},
|
||||||
|
];
|
@@ -3,13 +3,26 @@ import type { ColumnsType } from 'ant-design-vue/es/table';
|
|||||||
|
|
||||||
import type { SocialInfo } from '#/api/system/social/model';
|
import type { SocialInfo } from '#/api/system/social/model';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, unref } from 'vue';
|
||||||
|
|
||||||
import { Avatar, Modal, Table } from 'ant-design-vue';
|
import { createIconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Avatar,
|
||||||
|
Card,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { authUnbinding } from '#/api';
|
import { authUnbinding } from '#/api';
|
||||||
import { socialList } from '#/api/system/social';
|
import { socialList } from '#/api/system/social';
|
||||||
|
|
||||||
|
import { accountBindList, type BindItem } from '../../oauth-common';
|
||||||
|
|
||||||
const columns: ColumnsType = [
|
const columns: ColumnsType = [
|
||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@@ -36,14 +49,6 @@ const columns: ColumnsType = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const tableData = ref<SocialInfo[]>([]);
|
|
||||||
|
|
||||||
async function reload() {
|
|
||||||
tableData.value = await socialList();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(reload);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解绑账号
|
* 解绑账号
|
||||||
*/
|
*/
|
||||||
@@ -58,10 +63,47 @@ function handleUnbind(record: Record<string, any>) {
|
|||||||
type: 'warning',
|
type: 'warning',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 没有传递action事件则不支持绑定 弹出默认提示
|
||||||
|
*/
|
||||||
|
function defaultTip(title: string) {
|
||||||
|
message.info({ content: `暂不支持绑定${title}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buttonText(item: BindItem) {
|
||||||
|
return item.bound ? '已绑定' : '绑定';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已经绑定的平台
|
||||||
|
*/
|
||||||
|
const boundPlatformsList = ref<string[]>([]);
|
||||||
|
const bindList = computed<BindItem[]>(() => {
|
||||||
|
const list = [...accountBindList];
|
||||||
|
list.forEach((item) => {
|
||||||
|
item.bound = !!unref(boundPlatformsList).includes(item.source);
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData = ref<SocialInfo[]>([]);
|
||||||
|
|
||||||
|
async function reload() {
|
||||||
|
const resp = await socialList();
|
||||||
|
/**
|
||||||
|
* 平台转小写
|
||||||
|
* 已经绑定的平台
|
||||||
|
*/
|
||||||
|
boundPlatformsList.value = resp.map((item) => item.source.toLowerCase());
|
||||||
|
tableData.value = resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(reload);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex flex-col gap-[16px]">
|
||||||
<Table
|
<Table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data-source="tableData"
|
:data-source="tableData"
|
||||||
@@ -74,6 +116,74 @@ function handleUnbind(record: Record<string, any>) {
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
<div>todo: 绑定功能</div>
|
<div class="pb-3">
|
||||||
|
<List
|
||||||
|
:data-source="bindList"
|
||||||
|
:grid="{ gutter: 8, xs: 1, sm: 1, md: 2, lg: 3, xl: 3, xxl: 3 }"
|
||||||
|
>
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<ListItem>
|
||||||
|
<Card>
|
||||||
|
<div class="flex w-full items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<component
|
||||||
|
:is="createIconifyIcon(item.avatar)"
|
||||||
|
v-if="item.avatar"
|
||||||
|
:style="{ color: item.color }"
|
||||||
|
class="size-[40px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h4
|
||||||
|
class="mb-[4px] text-[14px] text-black/85 dark:text-white/85"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</h4>
|
||||||
|
<span class="text-black/45 dark:text-white/45">
|
||||||
|
{{ item.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
:disabled="item.bound"
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="
|
||||||
|
item.action ? item.action() : defaultTip(item.title)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ buttonText(item) }}
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</ListItem>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
<Alert message="说明" type="info">
|
||||||
|
<template #description>
|
||||||
|
<p>
|
||||||
|
需要添加第三方账号在
|
||||||
|
<span class="font-bold">
|
||||||
|
apps\web-antd\src\views\_core\oauth-common.ts
|
||||||
|
</span>
|
||||||
|
中accountBindList按模板添加
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
添加对应模板后会在此处显示绑定, 但只有
|
||||||
|
<span class="font-bold">实现了action才能在登录页显示</span>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/**
|
||||||
|
list item 间距
|
||||||
|
*/
|
||||||
|
:deep(.ant-list-item) {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
81
apps/web-antd/src/views/_core/social-callback/index.vue
Normal file
81
apps/web-antd/src/views/_core/social-callback/index.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { DEFAULT_HOME_PATH } from '@vben/constants';
|
||||||
|
import { useAccessStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { type AuthApi, authCallback } from '#/api';
|
||||||
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
|
import { accountBindList } from '../oauth-common';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const code = route.query.code as string;
|
||||||
|
const state = route.query.state as string;
|
||||||
|
const stateJson = JSON.parse(atob(state));
|
||||||
|
// 来源
|
||||||
|
const source = route.query.source as string;
|
||||||
|
// 租户ID
|
||||||
|
const defaultTenantId = '000000';
|
||||||
|
const tenantId = (stateJson.tenantId as string) ?? defaultTenantId;
|
||||||
|
const domain = stateJson.domain as string;
|
||||||
|
|
||||||
|
const accessStore = useAccessStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 如果域名不相等 则重定向处理
|
||||||
|
const host = window.location.host;
|
||||||
|
if (domain !== host) {
|
||||||
|
const urlFull = new URL(window.location.href);
|
||||||
|
urlFull.host = domain;
|
||||||
|
window.location.href = urlFull.toString();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 已经实现的平台
|
||||||
|
const currentClient = accountBindList.find(
|
||||||
|
(item) => item.source === source && item.action,
|
||||||
|
);
|
||||||
|
if (!currentClient) {
|
||||||
|
message.error({ content: `未找到${source}平台` });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data: AuthApi.OAuthLoginParams = {
|
||||||
|
grantType: 'social',
|
||||||
|
socialCode: code,
|
||||||
|
socialState: state,
|
||||||
|
source,
|
||||||
|
tenantId,
|
||||||
|
};
|
||||||
|
// 没有token为登录 有token是授权
|
||||||
|
if (accessStore.accessToken) {
|
||||||
|
await authCallback(data);
|
||||||
|
message.success(`${source}授权成功`);
|
||||||
|
} else {
|
||||||
|
// todo
|
||||||
|
await authStore.authLogin(data as any);
|
||||||
|
message.success(`${source}登录成功`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 500 你还没有绑定第三方账号,绑定后才可以登录!
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push(DEFAULT_HOME_PATH);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
46
cspell.json
46
cspell.json
@@ -4,49 +4,51 @@
|
|||||||
"language": "en,en-US",
|
"language": "en,en-US",
|
||||||
"allowCompoundWords": true,
|
"allowCompoundWords": true,
|
||||||
"words": [
|
"words": [
|
||||||
"clsx",
|
|
||||||
"esno",
|
|
||||||
"demi",
|
|
||||||
"unref",
|
|
||||||
"taze",
|
|
||||||
"acmr",
|
"acmr",
|
||||||
"antd",
|
"antd",
|
||||||
"lucide",
|
"antdv",
|
||||||
|
"astro",
|
||||||
"brotli",
|
"brotli",
|
||||||
|
"clsx",
|
||||||
"defu",
|
"defu",
|
||||||
|
"demi",
|
||||||
|
"echarts",
|
||||||
|
"ependencies",
|
||||||
|
"esno",
|
||||||
|
"etag",
|
||||||
"execa",
|
"execa",
|
||||||
|
"Gitee",
|
||||||
"iconify",
|
"iconify",
|
||||||
"intlify",
|
"intlify",
|
||||||
|
"lockb",
|
||||||
|
"lucide",
|
||||||
"mkdist",
|
"mkdist",
|
||||||
"mockjs",
|
"mockjs",
|
||||||
|
"nocheck",
|
||||||
"noopener",
|
"noopener",
|
||||||
"noreferrer",
|
"noreferrer",
|
||||||
"nprogress",
|
"nprogress",
|
||||||
|
"nuxt",
|
||||||
"pinia",
|
"pinia",
|
||||||
|
"prefixs",
|
||||||
"publint",
|
"publint",
|
||||||
|
"Qqchat",
|
||||||
"qrcode",
|
"qrcode",
|
||||||
"shadcn",
|
"shadcn",
|
||||||
"sonner",
|
"sonner",
|
||||||
|
"sortablejs",
|
||||||
|
"styl",
|
||||||
|
"taze",
|
||||||
|
"ui-kit",
|
||||||
"unplugin",
|
"unplugin",
|
||||||
|
"unref",
|
||||||
"vben",
|
"vben",
|
||||||
"vbenjs",
|
"vbenjs",
|
||||||
"vueuse",
|
|
||||||
"yxxx",
|
|
||||||
"nuxt",
|
|
||||||
"lockb",
|
|
||||||
"astro",
|
|
||||||
"ui-kit",
|
|
||||||
"styl",
|
|
||||||
"vnode",
|
|
||||||
"nocheck",
|
|
||||||
"prefixs",
|
|
||||||
"vitepress",
|
|
||||||
"antdv",
|
|
||||||
"ependencies",
|
|
||||||
"vite",
|
"vite",
|
||||||
"echarts",
|
"vitepress",
|
||||||
"sortablejs",
|
"vnode",
|
||||||
"etag"
|
"vueuse",
|
||||||
|
"yxxx"
|
||||||
],
|
],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"**/node_modules/**",
|
"**/node_modules/**",
|
||||||
|
@@ -92,6 +92,19 @@ const formState = reactive({
|
|||||||
username: localUsername,
|
username: localUsername,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* oauth登录 需要tenantId参数
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => formState.tenantId,
|
||||||
|
(tenantId) => {
|
||||||
|
localStorage.setItem('__oauth_tenant_id', tenantId);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认选中第一项租户
|
* 默认选中第一项租户
|
||||||
*/
|
*/
|
||||||
|
@@ -5,7 +5,8 @@
|
|||||||
"register": "Register",
|
"register": "Register",
|
||||||
"codeLogin": "Code Login",
|
"codeLogin": "Code Login",
|
||||||
"qrcodeLogin": "Qr Code Login",
|
"qrcodeLogin": "Qr Code Login",
|
||||||
"forgetPassword": "Forget Password"
|
"forgetPassword": "Forget Password",
|
||||||
|
"oauthLogin": "Oauth Login"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
@@ -5,7 +5,8 @@
|
|||||||
"register": "注册",
|
"register": "注册",
|
||||||
"codeLogin": "验证码登录",
|
"codeLogin": "验证码登录",
|
||||||
"qrcodeLogin": "二维码登录",
|
"qrcodeLogin": "二维码登录",
|
||||||
"forgetPassword": "忘记密码"
|
"forgetPassword": "忘记密码",
|
||||||
|
"oauthLogin": "第三方登录"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "概览",
|
"title": "概览",
|
||||||
|
Reference in New Issue
Block a user