mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-11-11 19:23:41 +08:00
feat(projects): 迁移登录完成
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="theme.naiveThemeOverrides" class="h-full">
|
||||
<n-config-provider :theme="theme.naiveTheme" :theme-overrides="theme.naiveThemeOverrides" class="h-full">
|
||||
<slot></slot>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
48
src/components/business/LoginAgreement/index.vue
Normal file
48
src/components/business/LoginAgreement/index.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="w-full text-14px">
|
||||
<n-checkbox v-model:checked="checked">我已经仔细阅读并接受</n-checkbox>
|
||||
<n-button :text="true" type="primary" @click="handleClickProtocol">《用户协议》</n-button>
|
||||
<n-button :text="true" type="primary" @click="handleClickPolicy">《隐私权政策》</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { NCheckbox, NButton } from 'naive-ui';
|
||||
|
||||
interface Props {
|
||||
/** 是否勾选 */
|
||||
value?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:value', value: boolean): void;
|
||||
/** 点击协议 */
|
||||
(e: 'click-protocol'): void;
|
||||
/** 点击隐私政策 */
|
||||
(e: 'click-policy'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: true
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const checked = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(newValue: boolean) {
|
||||
emit('update:value', newValue);
|
||||
}
|
||||
});
|
||||
|
||||
function handleClickProtocol() {
|
||||
emit('click-protocol');
|
||||
}
|
||||
function handleClickPolicy() {
|
||||
emit('click-policy');
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
3
src/components/business/index.ts
Normal file
3
src/components/business/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import LoginAgreement from './LoginAgreement/index.vue';
|
||||
|
||||
export { LoginAgreement };
|
||||
39
src/components/common/DarkModeSwitch/index.vue
Normal file
39
src/components/common/DarkModeSwitch/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="text-18px hover:text-primary cursor-pointer" @click="handleSwitch">
|
||||
<icon-mdi-moon-waning-crescent v-if="darkMode" />
|
||||
<icon-mdi-white-balance-sunny v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
/** 暗黑模式 */
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:dark', darkMode: boolean): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
dark: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const darkMode = computed({
|
||||
get() {
|
||||
return props.dark;
|
||||
},
|
||||
set(newValue: boolean) {
|
||||
emit('update:dark', newValue);
|
||||
}
|
||||
});
|
||||
|
||||
function handleSwitch() {
|
||||
darkMode.value = !darkMode.value;
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
@@ -1,3 +1,4 @@
|
||||
import SystemLogo from './SystemLogo/index.vue';
|
||||
import DarkModeSwitch from './DarkModeSwitch/index.vue';
|
||||
|
||||
export { SystemLogo };
|
||||
export { SystemLogo, DarkModeSwitch };
|
||||
|
||||
39
src/components/custom/ImageVerify/index.vue
Normal file
39
src/components/custom/ImageVerify/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<canvas ref="domRef" width="152" height="40" class="cursor-pointer" @click="getImgCode"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useImageVerify } from '@/hooks';
|
||||
|
||||
interface Props {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:code', code: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
code: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify();
|
||||
|
||||
watch(
|
||||
() => props.code,
|
||||
newValue => {
|
||||
setImgCode(newValue);
|
||||
}
|
||||
);
|
||||
watch(imgCode, newValue => {
|
||||
emit('update:code', newValue);
|
||||
});
|
||||
|
||||
defineExpose({ getImgCode });
|
||||
</script>
|
||||
<style scoped></style>
|
||||
3
src/components/custom/index.ts
Normal file
3
src/components/custom/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ImageVerify from './ImageVerify/index.vue';
|
||||
|
||||
export { ImageVerify };
|
||||
@@ -1 +1,3 @@
|
||||
export * from './common';
|
||||
export * from './custom';
|
||||
export * from './business';
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export * from './system';
|
||||
export * from './router';
|
||||
export * from './route';
|
||||
|
||||
32
src/composables/common/route.ts
Normal file
32
src/composables/common/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { unref, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { router } from '@/router';
|
||||
import { useRouteStore } from '@/store';
|
||||
|
||||
/**
|
||||
* 路由查询参数
|
||||
* @param inSetup - 是否在vue页面/组件的setup里面调用,在axios里面无法使用useRouter和useRoute
|
||||
*/
|
||||
export function useRouteQuery(inSetup: boolean = true) {
|
||||
const { getRouteName } = useRouteStore();
|
||||
|
||||
const route = getRouteInstance(inSetup);
|
||||
|
||||
/** 登录后跳转的地址 */
|
||||
const loginRedirect = computed(() => {
|
||||
let url: string | undefined;
|
||||
if (route.name === getRouteName('login')) {
|
||||
url = (route.query?.redirect as string) || '';
|
||||
}
|
||||
return url;
|
||||
});
|
||||
|
||||
return {
|
||||
loginRedirect
|
||||
};
|
||||
}
|
||||
|
||||
function getRouteInstance(inSetup: boolean = true) {
|
||||
const route = inSetup ? useRoute() : unref(router.currentRoute);
|
||||
return route;
|
||||
}
|
||||
90
src/composables/common/router.ts
Normal file
90
src/composables/common/router.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { unref } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import type { RouteLocationRaw } from 'vue-router';
|
||||
import { router as globalRouter } from '@/router';
|
||||
import { useRouteStore } from '@/store';
|
||||
import { LoginModuleKey } from '@/interface';
|
||||
|
||||
/**
|
||||
* 路由跳转
|
||||
* @param inSetup - 是否在vue页面/组件的setup里面调用,在axios里面无法使用useRouter和useRoute
|
||||
*/
|
||||
export function useRouterPush(inSetup: boolean = true) {
|
||||
const { getRouteName } = useRouteStore();
|
||||
|
||||
const router = inSetup ? useRouter() : globalRouter;
|
||||
const route = inSetup ? useRoute() : unref(globalRouter.currentRoute);
|
||||
|
||||
/**
|
||||
* 路由跳转
|
||||
* @param to - 需要跳转的路由
|
||||
* @param newTab - 是否在新的浏览器Tab标签打开
|
||||
*/
|
||||
function routerPush(to: RouteLocationRaw, newTab = false) {
|
||||
if (newTab) {
|
||||
const routerData = router.resolve(to);
|
||||
window.open(routerData.href, '_blank');
|
||||
} else {
|
||||
router.push(to);
|
||||
}
|
||||
}
|
||||
|
||||
/** 返回上一级路由 */
|
||||
function routerBack() {
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转首页
|
||||
* @param newTab - 在新的浏览器标签打开
|
||||
*/
|
||||
function toHome(newTab = false) {
|
||||
routerPush(getRouteName('root'), newTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转登录页面
|
||||
* @param loginModule - 展示的登录模块
|
||||
* @param redirectUrl - 重定向地址(登录成功后跳转的地址),默认undefined表示取当前地址为重定向地址
|
||||
*/
|
||||
function toLogin(loginModule?: LoginModuleKey, redirectUrl?: string) {
|
||||
const module: LoginModuleKey = loginModule || 'pwd-login';
|
||||
const routeLocation: RouteLocationRaw = {
|
||||
name: getRouteName('login'),
|
||||
params: { module }
|
||||
};
|
||||
const redirect = redirectUrl || route.fullPath;
|
||||
Object.assign(routeLocation, { query: { redirect } });
|
||||
routerPush(routeLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录页切换其他模块
|
||||
* @param module - 切换后的登录模块
|
||||
*/
|
||||
function toLoginModule(module: LoginModuleKey) {
|
||||
const { query } = route;
|
||||
routerPush({ name: getRouteName('login'), params: { module }, query });
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后跳转重定向的地址
|
||||
* @param redirect - 重定向地址
|
||||
*/
|
||||
function toLoginRedirect(redirect?: string) {
|
||||
if (redirect) {
|
||||
routerPush(redirect);
|
||||
} else {
|
||||
toHome();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
routerPush,
|
||||
routerBack,
|
||||
toHome,
|
||||
toLogin,
|
||||
toLoginModule,
|
||||
toLoginRedirect
|
||||
};
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './service';
|
||||
export * from './regexp';
|
||||
|
||||
16
src/config/common/regexp.ts
Normal file
16
src/config/common/regexp.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/** 手机号码正则 */
|
||||
export const REGEXP_PHONE =
|
||||
/^[1](([3][0-9])|([4][0,1,4-9])|([5][0-3,5-9])|([6][2,5,6,7])|([7][0-8])|([8][0-9])|([9][0-3,5-9]))[0-9]{8}$/;
|
||||
|
||||
/** 邮箱正则 */
|
||||
export const REGEXP_EMAIL = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/;
|
||||
|
||||
/** 密码正则(密码为8-18位数字/字符/符号的组合) */
|
||||
export const REGEXP_PWD =
|
||||
/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/;
|
||||
|
||||
/** 6位数字验证码正则 */
|
||||
export const REGEXP_CODE_SIX = /^\d{6}$/;
|
||||
|
||||
/** 4位数字验证码正则 */
|
||||
export const REGEXP_CODE_FOUR = /^\d{4}$/;
|
||||
5
src/hooks/business/index.ts
Normal file
5
src/hooks/business/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import useCountDown from './useCountDown';
|
||||
import useSmsCode from './useSmsCode';
|
||||
import useImageVerify from './useImageVerify';
|
||||
|
||||
export { useCountDown, useSmsCode, useImageVerify };
|
||||
52
src/hooks/business/useCountDown.ts
Normal file
52
src/hooks/business/useCountDown.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { useBoolean } from '../common';
|
||||
|
||||
/**
|
||||
* 倒计时
|
||||
* @param second - 倒计时的时间(s)
|
||||
*/
|
||||
export default function useCountDown(second: number) {
|
||||
if (second <= 0 && second % 1 !== 0) {
|
||||
throw Error('倒计时的时间应该为一个正整数!');
|
||||
}
|
||||
const { bool: isComplete, setTrue, setFalse } = useBoolean(false);
|
||||
|
||||
const counts = ref(0);
|
||||
const isCounting = computed(() => Boolean(counts.value));
|
||||
|
||||
let intervalId: any;
|
||||
|
||||
/**
|
||||
* 开始计时
|
||||
* @param updateSecond - 更改初时传入的倒计时时间
|
||||
*/
|
||||
function start(updateSecond: number = second) {
|
||||
if (!counts.value) {
|
||||
setFalse();
|
||||
counts.value = updateSecond;
|
||||
intervalId = setInterval(() => {
|
||||
counts.value -= 1;
|
||||
if (counts.value <= 0) {
|
||||
clearInterval(intervalId);
|
||||
setTrue();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止计时
|
||||
*/
|
||||
function stop() {
|
||||
intervalId = clearInterval(intervalId);
|
||||
counts.value = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
counts,
|
||||
isCounting,
|
||||
start,
|
||||
stop,
|
||||
isComplete
|
||||
};
|
||||
}
|
||||
85
src/hooks/business/useImageVerify.ts
Normal file
85
src/hooks/business/useImageVerify.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
/**
|
||||
* 绘制图形验证码
|
||||
* @param width - 图形宽度
|
||||
* @param height - 图形高度
|
||||
*/
|
||||
export default function useImageVerify(width = 152, height = 40) {
|
||||
const domRef = ref<HTMLCanvasElement>();
|
||||
const imgCode = ref('');
|
||||
|
||||
function setImgCode(code: string) {
|
||||
imgCode.value = code;
|
||||
}
|
||||
|
||||
function getImgCode() {
|
||||
if (!domRef.value) return;
|
||||
imgCode.value = draw(domRef.value, width, height);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getImgCode();
|
||||
});
|
||||
|
||||
return {
|
||||
domRef,
|
||||
imgCode,
|
||||
setImgCode,
|
||||
getImgCode
|
||||
};
|
||||
}
|
||||
|
||||
function randomNum(min: number, max: number) {
|
||||
const num = Math.floor(Math.random() * (max - min) + min);
|
||||
return num;
|
||||
}
|
||||
|
||||
function randomColor(min: number, max: number) {
|
||||
const r = randomNum(min, max);
|
||||
const g = randomNum(min, max);
|
||||
const b = randomNum(min, max);
|
||||
return `rgb(${r},${g},${b})`;
|
||||
}
|
||||
|
||||
function draw(dom: HTMLCanvasElement, width: number, height: number) {
|
||||
let imgCode = '';
|
||||
|
||||
const NUMBER_STRING = '0123456789';
|
||||
|
||||
const ctx = dom.getContext('2d');
|
||||
if (!ctx) return imgCode;
|
||||
|
||||
ctx.fillStyle = randomColor(180, 230);
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
|
||||
imgCode += text;
|
||||
const fontSize = randomNum(18, 41);
|
||||
const deg = randomNum(-30, 30);
|
||||
ctx.font = `${fontSize}px Simhei`;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillStyle = randomColor(80, 150);
|
||||
ctx.save();
|
||||
ctx.translate(30 * i + 23, 15);
|
||||
ctx.rotate((deg * Math.PI) / 180);
|
||||
ctx.fillText(text, -15 + 5, -15);
|
||||
ctx.restore();
|
||||
}
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(randomNum(0, width), randomNum(0, height));
|
||||
ctx.lineTo(randomNum(0, width), randomNum(0, height));
|
||||
ctx.strokeStyle = randomColor(180, 230);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let i = 0; i < 41; i += 1) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = randomColor(150, 200);
|
||||
ctx.fill();
|
||||
}
|
||||
return imgCode;
|
||||
}
|
||||
59
src/hooks/business/useSmsCode.ts
Normal file
59
src/hooks/business/useSmsCode.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { computed } from 'vue';
|
||||
import { REGEXP_PHONE } from '@/config';
|
||||
import { fetchSmsCode } from '@/service';
|
||||
import { useLoading } from '../common';
|
||||
import useCountDown from './useCountDown';
|
||||
|
||||
export default function useSmsCode() {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { counts, start, isCounting } = useCountDown(60);
|
||||
const initLabel = '获取验证码';
|
||||
const countingLabel = (second: number) => `${second}秒后重新获取`;
|
||||
const label = computed(() => {
|
||||
let text = initLabel;
|
||||
if (loading.value) {
|
||||
text = '';
|
||||
}
|
||||
if (isCounting.value) {
|
||||
text = countingLabel(counts.value);
|
||||
}
|
||||
return text;
|
||||
});
|
||||
|
||||
/** 判断手机号码格式是否正确 */
|
||||
function isPhoneValid(phone: string) {
|
||||
let valid = true;
|
||||
if (phone.trim() === '') {
|
||||
window.$message?.error('手机号码不能为空!');
|
||||
valid = false;
|
||||
} else if (!REGEXP_PHONE.test(phone)) {
|
||||
window.$message?.error('手机号码格式错误!');
|
||||
valid = false;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码
|
||||
* @param phone - 手机号
|
||||
*/
|
||||
async function getSmsCode(phone: string) {
|
||||
const valid = isPhoneValid(phone);
|
||||
if (!valid || loading.value) return;
|
||||
startLoading();
|
||||
const { data } = await fetchSmsCode(phone);
|
||||
if (data) {
|
||||
window.$message?.success('验证码发送成功!');
|
||||
start();
|
||||
}
|
||||
endLoading();
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
start,
|
||||
isCounting,
|
||||
getSmsCode,
|
||||
loading
|
||||
};
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './common';
|
||||
export * from './business';
|
||||
|
||||
@@ -2,5 +2,3 @@ import { EnumLoginModule } from '@/enum';
|
||||
|
||||
/** 登录模块 */
|
||||
export type LoginModuleKey = keyof typeof EnumLoginModule;
|
||||
|
||||
export type LoginModuleRegexp = LoginModuleKey;
|
||||
|
||||
@@ -15,7 +15,7 @@ export async function handlePagePermission(
|
||||
|
||||
const permissions = to.meta.permissions || [];
|
||||
const needLogin = Boolean(to.meta?.requiresAuth) || Boolean(permissions.length);
|
||||
const hasPermission = !permissions.length || permissions.includes(auth.role);
|
||||
const hasPermission = !permissions.length || permissions.includes(auth.userInfo.userRole);
|
||||
|
||||
if (!route.isAddedDynamicRoute) {
|
||||
// 添加动态路由
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// import { getLoginModuleRegExp } from '@/utils';
|
||||
import { getLoginModuleRegExp } from '@/utils';
|
||||
import type { LoginModuleKey } from '@/interface';
|
||||
|
||||
/** 固定的路由 */
|
||||
const constantRoutes: AuthRoute.Route[] = [
|
||||
@@ -12,8 +13,14 @@ const constantRoutes: AuthRoute.Route[] = [
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
path: `/login/:module(${getLoginModuleRegExp()})?`,
|
||||
component: 'blank',
|
||||
props: route => {
|
||||
const moduleType = (route.params.module as LoginModuleKey) || 'pwd-login';
|
||||
return {
|
||||
module: moduleType
|
||||
};
|
||||
},
|
||||
meta: {
|
||||
title: '登录',
|
||||
single: true
|
||||
|
||||
@@ -1,15 +1,39 @@
|
||||
import { mockRequest } from '../request';
|
||||
import { userRoutesMiddleware } from '../middleware';
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
* @param phone - 手机号
|
||||
* @returns - 返回boolean值表示是否发送成功
|
||||
*/
|
||||
export function fetchSmsCode(phone: string) {
|
||||
return mockRequest.post<boolean>('/getSmsCode', { phone });
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param phone - 手机号
|
||||
* @param pwdOrCode - 密码或验证码
|
||||
* @param type - 登录方式: pwd - 密码登录; sms - 验证码登录
|
||||
*/
|
||||
export function fetchLogin(phone: string, pwdOrCode: string, type: 'pwd' | 'sms') {
|
||||
if (type === 'pwd') {
|
||||
return mockRequest.post<ApiAuth.Token>('/loginByPwd', { phone, pwd: pwdOrCode });
|
||||
}
|
||||
return mockRequest.post<ApiAuth.Token>('/loginByCode', { phone, code: pwdOrCode });
|
||||
}
|
||||
|
||||
/** 获取用户信息 */
|
||||
export function fetchUserInfo() {
|
||||
return mockRequest.get<ApiAuth.UserInfo>('/getUserInfo');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户路由数据
|
||||
* @param userId - 用户id
|
||||
* @description 后端根据用户id查询到对应的角色类型,并将路由筛选出对应角色的路由数据返回前端
|
||||
*/
|
||||
export async function fetchUserRoutes(userId: string = 'soybean') {
|
||||
const { data } = await mockRequest<ApiRoute.ResponseRoute>(
|
||||
{ url: '/getUserRoutes', method: 'post', data: { userId } },
|
||||
false
|
||||
);
|
||||
const { data } = await mockRequest.post<ApiRoute.Route>('/getUserRoutes', { userId });
|
||||
return userRoutesMiddleware(data);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { transformAuthRouteToVueRoute } from '@/utils';
|
||||
|
||||
export function userRoutesMiddleware(data: ApiRoute.ResponseRoute | null) {
|
||||
export function userRoutesMiddleware(data: ApiRoute.Route | null) {
|
||||
if (!data) return [];
|
||||
|
||||
const routes: RouteRecordRaw[] = data.routes.map(item => transformAuthRouteToVueRoute(item));
|
||||
|
||||
@@ -6,13 +6,6 @@ import CustomAxiosInstance from './instance';
|
||||
|
||||
type RequestMethod = 'get' | 'post' | 'put' | 'delete';
|
||||
|
||||
type RequestResultHook<T = any> = {
|
||||
data: Ref<T | null>;
|
||||
error: Ref<Service.RequestError | null>;
|
||||
loading: Ref<boolean>;
|
||||
network: Ref<boolean>;
|
||||
};
|
||||
|
||||
interface RequestParam {
|
||||
url: string;
|
||||
method?: RequestMethod;
|
||||
@@ -20,9 +13,95 @@ interface RequestParam {
|
||||
axiosConfig?: AxiosRequestConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求
|
||||
* @param axiosConfig - axios配置
|
||||
*/
|
||||
export function createRequest(axiosConfig: AxiosRequestConfig) {
|
||||
const customInstance = new CustomAxiosInstance(axiosConfig);
|
||||
|
||||
/**
|
||||
* 异步promise请求
|
||||
* @param param - 请求参数
|
||||
* - url: 请求地址
|
||||
* - method: 请求方法(默认get)
|
||||
* - data: 请求的body的data
|
||||
* - axiosConfig: axios配置
|
||||
*/
|
||||
async function asyncRequest<T = any>(param: RequestParam): Promise<Service.RequestResult<T>> {
|
||||
const { url } = param;
|
||||
const method = param.method || 'get';
|
||||
const { instance } = customInstance;
|
||||
const res = (await getRequestResponse(
|
||||
instance,
|
||||
method,
|
||||
url,
|
||||
param.data,
|
||||
param.axiosConfig
|
||||
)) as Service.RequestResult<T>;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* get请求
|
||||
* @param url - 请求地址
|
||||
* @param config - axios配置
|
||||
*/
|
||||
function get<T = any>(url: string, config?: AxiosRequestConfig) {
|
||||
return asyncRequest<T>({ url, method: 'get', axiosConfig: config });
|
||||
}
|
||||
|
||||
/**
|
||||
* post请求
|
||||
* @param url - 请求地址
|
||||
* @param data - 请求的body的data
|
||||
* @param config - axios配置
|
||||
*/
|
||||
function post<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
|
||||
return asyncRequest<T>({ url, method: 'post', data, axiosConfig: config });
|
||||
}
|
||||
/**
|
||||
* put请求
|
||||
* @param url - 请求地址
|
||||
* @param data - 请求的body的data
|
||||
* @param config - axios配置
|
||||
*/
|
||||
function put<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
|
||||
return asyncRequest<T>({ url, method: 'put', data, axiosConfig: config });
|
||||
}
|
||||
|
||||
/**
|
||||
* delete请求
|
||||
* @param url - 请求地址
|
||||
* @param config - axios配置
|
||||
*/
|
||||
function handleDelete<T = any>(url: string, config: AxiosRequestConfig) {
|
||||
return asyncRequest<T>({ url, method: 'delete', axiosConfig: config });
|
||||
}
|
||||
|
||||
return {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
delete: handleDelete
|
||||
};
|
||||
}
|
||||
|
||||
type RequestResultHook<T = any> = {
|
||||
data: Ref<T | null>;
|
||||
error: Ref<Service.RequestError | null>;
|
||||
loading: Ref<boolean>;
|
||||
network: Ref<boolean>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建hooks请求
|
||||
* @param axiosConfig - axios配置
|
||||
*/
|
||||
export function createHookRequest(axiosConfig: AxiosRequestConfig) {
|
||||
const customInstance = new CustomAxiosInstance(axiosConfig);
|
||||
|
||||
/**
|
||||
* hooks请求
|
||||
* @param param - 请求参数
|
||||
@@ -30,70 +109,82 @@ export function createRequest(axiosConfig: AxiosRequestConfig) {
|
||||
* - method: 请求方法(默认get)
|
||||
* - data: 请求的body的data
|
||||
* - axiosConfig: axios配置
|
||||
* @param hookMode - 是否启用hook写法
|
||||
*/
|
||||
function request<T = any>(param: RequestParam, hookMode: true): RequestResultHook<T>;
|
||||
function request<T = any>(param: RequestParam, hookMode: false): Promise<Service.RequestResult<T>>;
|
||||
function request<T = any>(
|
||||
param: RequestParam,
|
||||
hookMode: boolean
|
||||
): RequestResultHook<T> | Promise<Service.RequestResult<T>> {
|
||||
function useRequest<T = any>(param: RequestParam): RequestResultHook<T> {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: network, setBool: setNetwork } = useBoolean(window.navigator.onLine);
|
||||
|
||||
startLoading();
|
||||
const data = ref<T | null>(null) as Ref<T | null>;
|
||||
const error = ref<Service.RequestError | null>(null);
|
||||
|
||||
function handleRequestResult(response: any) {
|
||||
const res = response as Service.RequestResult<T>;
|
||||
data.value = res.data;
|
||||
error.value = res.error;
|
||||
endLoading();
|
||||
setNetwork(window.navigator.onLine);
|
||||
}
|
||||
|
||||
const { url } = param;
|
||||
const method = param.method || 'get';
|
||||
const { instance } = customInstance;
|
||||
if (hookMode) {
|
||||
return useRequest(instance, method, url, param.data, param.axiosConfig);
|
||||
}
|
||||
return asyncRequest(instance, method, url, param.data, param.axiosConfig);
|
||||
|
||||
getRequestResponse(instance, method, url, param.data, param.axiosConfig).then(handleRequestResult);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
network
|
||||
};
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
function useRequest<T = any>(
|
||||
instance: AxiosInstance,
|
||||
method: RequestMethod,
|
||||
url: string,
|
||||
bodyData?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): RequestResultHook<T> {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: network, setBool: setNetwork } = useBoolean(window.navigator.onLine);
|
||||
|
||||
startLoading();
|
||||
const data = ref<T | null>(null) as Ref<T | null>;
|
||||
const error = ref<Service.RequestError | null>(null);
|
||||
|
||||
function handleRequestResult(response: any) {
|
||||
const res = response as Service.RequestResult<T>;
|
||||
data.value = res.data;
|
||||
error.value = res.error;
|
||||
endLoading();
|
||||
setNetwork(window.navigator.onLine);
|
||||
/**
|
||||
* get请求
|
||||
* @param url - 请求地址
|
||||
* @param config - axios配置
|
||||
*/
|
||||
function get<T = any>(url: string, config?: AxiosRequestConfig) {
|
||||
return useRequest<T>({ url, method: 'get', axiosConfig: config });
|
||||
}
|
||||
|
||||
getRequestResponse(instance, method, url, bodyData, config).then(handleRequestResult);
|
||||
/**
|
||||
* post请求
|
||||
* @param url - 请求地址
|
||||
* @param data - 请求的body的data
|
||||
* @param config - axios配置
|
||||
*/
|
||||
function post<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
|
||||
return useRequest<T>({ url, method: 'post', data, axiosConfig: config });
|
||||
}
|
||||
/**
|
||||
* put请求
|
||||
* @param url - 请求地址
|
||||
* @param data - 请求的body的data
|
||||
* @param config - axios配置
|
||||
*/
|
||||
function put<T = any>(url: string, data?: any, config?: AxiosRequestConfig) {
|
||||
return useRequest<T>({ url, method: 'put', data, axiosConfig: config });
|
||||
}
|
||||
|
||||
/**
|
||||
* delete请求
|
||||
* @param url - 请求地址
|
||||
* @param config - axios配置
|
||||
*/
|
||||
function handleDelete<T = any>(url: string, config: AxiosRequestConfig) {
|
||||
return useRequest<T>({ url, method: 'delete', axiosConfig: config });
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
network
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
delete: handleDelete
|
||||
};
|
||||
}
|
||||
|
||||
async function asyncRequest<T = any>(
|
||||
instance: AxiosInstance,
|
||||
method: RequestMethod,
|
||||
url: string,
|
||||
bodyData?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<Service.RequestResult<T>> {
|
||||
const res = (await getRequestResponse(instance, method, url, bodyData, config)) as Service.RequestResult<T>;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async function getRequestResponse(
|
||||
instance: AxiosInstance,
|
||||
method: RequestMethod,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ref, computed, reactive } from 'vue';
|
||||
import { ref, computed, reactive, unref } from 'vue';
|
||||
import type { Ref, ComputedRef } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { getUserInfo, getToken } from '@/utils';
|
||||
import { router as globalRouter } from '@/router';
|
||||
import { useRouterPush, useRouteQuery } from '@/composables';
|
||||
import { useLoading } from '@/hooks';
|
||||
import { fetchLogin, fetchUserInfo } from '@/service';
|
||||
import { getUserInfo, getToken, setUserInfo, setToken, setRefreshToken, clearAuthStorage } from '@/utils';
|
||||
|
||||
interface AuthStore {
|
||||
/** 用户信息 */
|
||||
@@ -10,21 +14,94 @@ interface AuthStore {
|
||||
token: Ref<string>;
|
||||
/** 是否登录 */
|
||||
isLogin: ComputedRef<boolean>;
|
||||
/** 用户角色 */
|
||||
role: Ref<Auth.RoleType>;
|
||||
/**
|
||||
* 重置authStore
|
||||
* 是否需要跳转页面(例如当前页面是需要权限的,登出后需要跳转到登录页面)
|
||||
*/
|
||||
resetAuthStore(pushRoute: boolean): void;
|
||||
/** 登录的加载状态 */
|
||||
loginLoding: Ref<boolean>;
|
||||
/**
|
||||
* 登录
|
||||
* @param phone - 手机号
|
||||
* @param pwdOrCode - 密码或验证码
|
||||
* @param type - 登录方式: pwd - 密码登录; sms - 验证码登录
|
||||
*/
|
||||
login(phone: string, pwdOrCode: string, type: 'pwd' | 'sms'): void;
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth-store', () => {
|
||||
const { toLogin, toLoginRedirect } = useRouterPush(false);
|
||||
const { loginRedirect } = useRouteQuery(false);
|
||||
const { loading: loginLoding, startLoading: startLoginLoading, endLoading: endLoginLoading } = useLoading();
|
||||
|
||||
const userInfo: Auth.UserInfo = reactive(getUserInfo());
|
||||
function handleSetUserInfo(data: Auth.UserInfo) {
|
||||
Object.assign(userInfo, data);
|
||||
}
|
||||
|
||||
const token = ref(getToken());
|
||||
function handleSetToken(data: string) {
|
||||
token.value = data;
|
||||
}
|
||||
|
||||
const isLogin = computed(() => Boolean(token.value));
|
||||
const role = ref<Auth.RoleType>('super');
|
||||
|
||||
function resetAuthStore(pushRoute: boolean = true) {
|
||||
const auth = useAuthStore();
|
||||
const route = unref(globalRouter.currentRoute);
|
||||
|
||||
clearAuthStorage();
|
||||
auth.$reset();
|
||||
|
||||
if (pushRoute && route.meta.requiresAuth) {
|
||||
toLogin();
|
||||
}
|
||||
}
|
||||
|
||||
async function login(phone: string, pwdOrCode: string, type: 'pwd' | 'sms') {
|
||||
startLoginLoading();
|
||||
const { data } = await fetchLogin(phone, pwdOrCode, type);
|
||||
if (data) {
|
||||
await loginByToken(data);
|
||||
}
|
||||
endLoginLoading();
|
||||
}
|
||||
|
||||
async function loginByToken(backendToken: ApiAuth.Token) {
|
||||
// 1.先把token存储到缓存中
|
||||
const { token, refreshToken } = backendToken;
|
||||
setToken(token);
|
||||
setRefreshToken(refreshToken);
|
||||
|
||||
// 2.获取用户信息
|
||||
const { data } = await fetchUserInfo();
|
||||
if (data) {
|
||||
// 成功后把用户信息存储到缓存中
|
||||
setUserInfo(data);
|
||||
handleSetToken(token);
|
||||
handleSetUserInfo(data);
|
||||
// 3. 跳转登录后的地址
|
||||
toLoginRedirect(loginRedirect.value);
|
||||
// 4.登录成功弹出欢迎提示
|
||||
window.$notification?.success({
|
||||
title: '登录成功!',
|
||||
content: `欢迎回来,${userInfo.userName}!`,
|
||||
duration: 3000
|
||||
});
|
||||
} else {
|
||||
// 不成功则重置状态
|
||||
resetAuthStore(false);
|
||||
}
|
||||
}
|
||||
|
||||
const authStore: AuthStore = {
|
||||
userInfo,
|
||||
token,
|
||||
isLogin,
|
||||
role
|
||||
resetAuthStore,
|
||||
loginLoding,
|
||||
login
|
||||
};
|
||||
|
||||
return authStore;
|
||||
|
||||
@@ -26,7 +26,7 @@ interface RouteStore {
|
||||
* 获取路由路径
|
||||
* @description getRouteName 和 getRoutePath 优先使用 getRouteName
|
||||
*/
|
||||
getRoutePath(key: AuthRoute.RouteKey): AuthRoute.RoutePath<''> | undefined;
|
||||
getRoutePath(key: AuthRoute.RouteKey): AuthRoute.RoutePath | undefined;
|
||||
/** 获取路由路径 */
|
||||
getRouteTitle(key: AuthRoute.RouteKey): string | undefined;
|
||||
}
|
||||
|
||||
@@ -37,3 +37,27 @@ export function getThemeColors(colors: [ColorType, string][]) {
|
||||
|
||||
return themeColor;
|
||||
}
|
||||
|
||||
/** windicss 暗黑模式 */
|
||||
export function handleWindicssDarkMode() {
|
||||
const DARK_CLASS = 'dark';
|
||||
function getHtmlElement() {
|
||||
return document.querySelector('html');
|
||||
}
|
||||
function addDarkClass() {
|
||||
const html = getHtmlElement();
|
||||
if (html) {
|
||||
html.classList.add(DARK_CLASS);
|
||||
}
|
||||
}
|
||||
function removeDarkClass() {
|
||||
const html = getHtmlElement();
|
||||
if (html) {
|
||||
html.classList.remove(DARK_CLASS);
|
||||
}
|
||||
}
|
||||
return {
|
||||
addDarkClass,
|
||||
removeDarkClass
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import type { Ref, ComputedRef } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useThemeVars } from 'naive-ui';
|
||||
import type { GlobalThemeOverrides } from 'naive-ui';
|
||||
import { useThemeVars, darkTheme, useOsTheme } from 'naive-ui';
|
||||
import type { GlobalThemeOverrides, GlobalTheme } from 'naive-ui';
|
||||
import { kebabCase } from 'lodash-es';
|
||||
import { useBoolean } from '@/hooks';
|
||||
import { getColorPalette } from '@/utils';
|
||||
import { getThemeColors } from './helpers';
|
||||
import { getThemeColors, handleWindicssDarkMode } from './helpers';
|
||||
|
||||
interface OtherColor {
|
||||
/** 信息 */
|
||||
@@ -18,19 +19,31 @@ interface OtherColor {
|
||||
error: string;
|
||||
}
|
||||
|
||||
type BuiltInGlobalTheme = Omit<Required<GlobalTheme>, 'InternalSelectMenu' | 'InternalSelection'>;
|
||||
|
||||
interface ThemeStore {
|
||||
/** 暗黑模式 */
|
||||
darkMode: Ref<boolean>;
|
||||
/** 设置暗黑模式 */
|
||||
setDarkMode(dark: boolean): void;
|
||||
/** 切换/关闭 暗黑模式 */
|
||||
toggleDarkMode(dark: boolean): void;
|
||||
/** 主题颜色 */
|
||||
themeColor: Ref<string>;
|
||||
/** 其他颜色 */
|
||||
otherColor: ComputedRef<OtherColor>;
|
||||
/** naiveUI的主题配置 */
|
||||
naiveThemeOverrides: ComputedRef<GlobalThemeOverrides>;
|
||||
/** naive-ui暗黑主题 */
|
||||
naiveTheme: ComputedRef<BuiltInGlobalTheme | undefined>;
|
||||
}
|
||||
|
||||
type ThemeVarsKeys = keyof Exclude<GlobalThemeOverrides['common'], undefined>;
|
||||
|
||||
export const useThemeStore = defineStore('theme-store', () => {
|
||||
const themeVars = useThemeVars();
|
||||
const { bool: darkMode, setBool: setDarkMode, toggle: toggleDarkMode } = useBoolean();
|
||||
const { addDarkClass, removeDarkClass } = handleWindicssDarkMode();
|
||||
|
||||
const themeColor = ref('#1890ff');
|
||||
const otherColor = computed<OtherColor>(() => ({
|
||||
@@ -62,6 +75,12 @@ export const useThemeStore = defineStore('theme-store', () => {
|
||||
};
|
||||
});
|
||||
|
||||
/** naive-ui暗黑主题 */
|
||||
const naiveTheme = computed(() => (darkMode.value ? darkTheme : undefined));
|
||||
|
||||
/** 操作系统暗黑主题 */
|
||||
const osTheme = useOsTheme();
|
||||
|
||||
/** 添加css vars至html */
|
||||
function addThemeCssVarsToHtml() {
|
||||
if (document.documentElement.style.cssText) return;
|
||||
@@ -82,10 +101,40 @@ export const useThemeStore = defineStore('theme-store', () => {
|
||||
|
||||
init();
|
||||
|
||||
// 监听操作系统主题模式
|
||||
watch(
|
||||
osTheme,
|
||||
newValue => {
|
||||
const isDark = newValue === 'dark';
|
||||
if (isDark) {
|
||||
setDarkMode(true);
|
||||
} else {
|
||||
setDarkMode(false);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
// 监听主题的暗黑模式
|
||||
watch(
|
||||
() => darkMode.value,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
addDarkClass();
|
||||
} else {
|
||||
removeDarkClass();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const themeStore: ThemeStore = {
|
||||
darkMode,
|
||||
setDarkMode,
|
||||
toggleDarkMode,
|
||||
themeColor,
|
||||
otherColor,
|
||||
naiveThemeOverrides
|
||||
naiveThemeOverrides,
|
||||
naiveTheme
|
||||
};
|
||||
|
||||
return themeStore;
|
||||
|
||||
10
src/typings/api/auth.d.ts
vendored
Normal file
10
src/typings/api/auth.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
/** 后端返回的用户权益相关类型 */
|
||||
declare namespace ApiAuth {
|
||||
/** 返回的token和刷新token */
|
||||
interface Token {
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
/** 返回的用户信息 */
|
||||
type UserInfo = Auth.UserInfo;
|
||||
}
|
||||
2
src/typings/api/route.d.ts
vendored
2
src/typings/api/route.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
/** 后端返回的路由相关类型 */
|
||||
declare namespace ApiRoute {
|
||||
/** 后端返回的路由数据类型 */
|
||||
interface ResponseRoute {
|
||||
interface Route {
|
||||
/** 动态路由 */
|
||||
routes: AuthRoute.Route[];
|
||||
/** 路由首页对应的key */
|
||||
|
||||
22
src/typings/business/auth.d.ts
vendored
22
src/typings/business/auth.d.ts
vendored
@@ -1,15 +1,5 @@
|
||||
/** 用户相关模块 */
|
||||
declare namespace Auth {
|
||||
/** 用户信息 */
|
||||
interface UserInfo {
|
||||
/** 用户id */
|
||||
userId: string;
|
||||
/** 用户名 */
|
||||
userName: string;
|
||||
/** 用户手机号 */
|
||||
userPhone: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户角色类型
|
||||
* - super: 超级管理员
|
||||
@@ -18,4 +8,16 @@ declare namespace Auth {
|
||||
* - visitor: 游客
|
||||
*/
|
||||
type RoleType = 'super' | 'admin' | 'test' | 'visitor';
|
||||
|
||||
/** 用户信息 */
|
||||
interface UserInfo {
|
||||
/** 用户id */
|
||||
userId: string;
|
||||
/** 用户名 */
|
||||
userName: string;
|
||||
/** 用户手机号 */
|
||||
userPhone: string;
|
||||
/** 用户角色类型 */
|
||||
userRole: RoleType;
|
||||
}
|
||||
}
|
||||
|
||||
9
src/typings/common/route.d.ts
vendored
9
src/typings/common/route.d.ts
vendored
@@ -24,7 +24,7 @@ declare namespace AuthRoute {
|
||||
: `/${Key}`;
|
||||
|
||||
/** 路由路径 */
|
||||
type RoutePath<Key extends string = string> =
|
||||
type RoutePath<Key extends string = '' | LoginPath> =
|
||||
| '/'
|
||||
| Exclude<KeyToPath<RouteKey>, '/root' | '/redirect'>
|
||||
| Key
|
||||
@@ -65,8 +65,11 @@ declare namespace AuthRoute {
|
||||
order?: number;
|
||||
};
|
||||
|
||||
/** 登录路由路径 */
|
||||
type LoginPath = `/login/:module(${string})?`;
|
||||
|
||||
/** 单个路由的类型结构(后端返回此类型结构的路由) */
|
||||
interface Route<T extends string = ''> {
|
||||
interface Route<T extends string = '' | LoginPath> {
|
||||
/** 路由名称(路由唯一标识) */
|
||||
name: RouteKey;
|
||||
/** 路由路径 */
|
||||
@@ -85,5 +88,7 @@ declare namespace AuthRoute {
|
||||
children?: Route[];
|
||||
/** 路由描述 */
|
||||
meta: RouteMeta;
|
||||
/** 属性 */
|
||||
props?: boolean | Record<string, any> | ((to: any) => Record<string, any>);
|
||||
}
|
||||
}
|
||||
|
||||
2
src/typings/common/util.d.ts
vendored
2
src/typings/common/util.d.ts
vendored
@@ -12,6 +12,4 @@ declare namespace Util {
|
||||
type UnionToTuple<T, U = T> = [T] extends [never]
|
||||
? []
|
||||
: [LastInUnion<T>, ...UnionToTuple<Exclude<U, LastInUnion<T>>>];
|
||||
|
||||
type Inter = UnionToTuple<'1' | '2'>;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ export function getUserInfo() {
|
||||
const emptyInfo: Auth.UserInfo = {
|
||||
userId: '',
|
||||
userName: '',
|
||||
userPhone: ''
|
||||
userPhone: '',
|
||||
userRole: 'visitor'
|
||||
};
|
||||
const userInfo: Auth.UserInfo = getLocal<Auth.UserInfo>(EnumStorageKey['user-info']) || emptyInfo;
|
||||
return userInfo;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './typeof';
|
||||
export * from './console';
|
||||
export * from './color';
|
||||
export * from './number';
|
||||
export * from './design-pattern';
|
||||
|
||||
23
src/utils/common/number.ts
Normal file
23
src/utils/common/number.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 根据数字获取对应的汉字
|
||||
* @param num - 数字(0-10)
|
||||
*/
|
||||
export function getHanByNumber(num: number) {
|
||||
const HAN_STR = '零一二三四五六七八九十';
|
||||
return HAN_STR.charAt(num);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将总秒数转换成 分:秒
|
||||
* @param seconds - 秒
|
||||
*/
|
||||
export function transformToTimeCountDown(seconds: number) {
|
||||
const SECONDS_A_MINUTE = 60;
|
||||
function fillZero(num: number) {
|
||||
return num.toString().padStart(2, '0');
|
||||
}
|
||||
const minuteNum = Math.floor(seconds / SECONDS_A_MINUTE);
|
||||
const minute = fillZero(minuteNum);
|
||||
const second = fillZero(seconds - minuteNum * SECONDS_A_MINUTE);
|
||||
return `${minute}: ${second}`;
|
||||
}
|
||||
1
src/utils/form/index.ts
Normal file
1
src/utils/form/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './rule';
|
||||
73
src/utils/form/rule.ts
Normal file
73
src/utils/form/rule.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Ref } from 'vue';
|
||||
import type { FormItemRule } from 'naive-ui';
|
||||
import { REGEXP_PHONE, REGEXP_PWD, REGEXP_CODE_SIX, REGEXP_EMAIL } from '@/config';
|
||||
|
||||
/** 表单规则 */
|
||||
interface CustomFormRules {
|
||||
/** 手机号码 */
|
||||
phone: FormItemRule[];
|
||||
/** 密码 */
|
||||
pwd: FormItemRule[];
|
||||
/** 验证码 */
|
||||
code: FormItemRule[];
|
||||
/** 邮箱 */
|
||||
email: FormItemRule[];
|
||||
}
|
||||
|
||||
/** 表单规则 */
|
||||
export const formRules: CustomFormRules = {
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号码' },
|
||||
{ pattern: REGEXP_PHONE, message: '手机号码格式错误', trigger: 'input' }
|
||||
],
|
||||
pwd: [
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ pattern: REGEXP_PWD, message: '密码为8-18位数字/字符/符号,至少2种组合', trigger: 'input' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码' },
|
||||
{ pattern: REGEXP_CODE_SIX, message: '验证码格式错误', trigger: 'input' }
|
||||
],
|
||||
email: [{ pattern: REGEXP_EMAIL, message: '邮箱格式错误', trigger: 'blur' }]
|
||||
};
|
||||
|
||||
/** 获取确认密码的表单规则 */
|
||||
export function getConfirmPwdRule(pwd: Ref<string>) {
|
||||
const confirmPwdRule: FormItemRule[] = [
|
||||
{ required: true, message: '请输入确认密码' },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (!isBlankString(value) && value !== pwd.value) {
|
||||
return Promise.reject(rule.message);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
message: '输入的值与密码不一致',
|
||||
trigger: 'input'
|
||||
}
|
||||
];
|
||||
return confirmPwdRule;
|
||||
}
|
||||
|
||||
/** 获取图片验证码的表单规则 */
|
||||
export function getImgCodeRule(imgCode: Ref<string>) {
|
||||
const imgCodeRule: FormItemRule[] = [
|
||||
{ required: true, message: '请输入验证码' },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (!isBlankString(value) && value !== imgCode.value) {
|
||||
return Promise.reject(rule.message);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
message: '验证码不正确',
|
||||
trigger: 'blur'
|
||||
}
|
||||
];
|
||||
return imgCodeRule;
|
||||
}
|
||||
|
||||
/** 是否为空字符串 */
|
||||
function isBlankString(str: string) {
|
||||
return str.trim() === '';
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './storage';
|
||||
export * from './service';
|
||||
export * from './auth';
|
||||
export * from './router';
|
||||
export * from './form';
|
||||
|
||||
@@ -40,12 +40,18 @@ export function transformAuthRouteToVueRoute(item: AuthRoute.Route) {
|
||||
consoleError('路由组件解析失败: ', item);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasProps(item) && !isSingleRoute(item)) {
|
||||
(itemRoute as any).props = item.props;
|
||||
}
|
||||
|
||||
if (isSingleRoute(item)) {
|
||||
itemRoute.children = [
|
||||
{
|
||||
path: '',
|
||||
name: item.name,
|
||||
component: getViewComponent(item.name)
|
||||
component: getViewComponent(item.name),
|
||||
props: hasProps(item) ? item.props : undefined
|
||||
}
|
||||
];
|
||||
} else if (hasChildren(item)) {
|
||||
@@ -67,6 +73,10 @@ function hasChildren(item: AuthRoute.Route) {
|
||||
return Boolean(item.children && item.children.length);
|
||||
}
|
||||
|
||||
function hasProps(item: AuthRoute.Route) {
|
||||
return Boolean(item.props);
|
||||
}
|
||||
|
||||
function isSingleRoute(item: AuthRoute.Route) {
|
||||
return Boolean(item.meta.single);
|
||||
}
|
||||
|
||||
61
src/views/system/login/components/BindWechat/index.vue
Normal file
61
src/views/system/login/components/BindWechat/index.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" placeholder="手机号码" />
|
||||
</n-form-item>
|
||||
<n-form-item path="code">
|
||||
<div class="flex-y-center w-full">
|
||||
<n-input v-model:value="model.code" placeholder="验证码" />
|
||||
<div class="w-18px"></div>
|
||||
<n-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSmsCode">
|
||||
{{ label }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" size="large">
|
||||
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
|
||||
<n-button size="large" :block="true" :round="true" @click="toLoginModule('pwd-login')">返回</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { NForm, NFormItem, NInput, NSpace, NButton } from 'naive-ui';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useSmsCode } from '@/hooks';
|
||||
import { formRules } from '@/utils';
|
||||
|
||||
const { toLoginModule } = useRouterPush();
|
||||
const { label, isCounting, loading: smsLoading, getSmsCode } = useSmsCode();
|
||||
|
||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
imgCode: ''
|
||||
});
|
||||
const rules = {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code
|
||||
};
|
||||
|
||||
function handleSmsCode() {
|
||||
getSmsCode(model.phone);
|
||||
}
|
||||
|
||||
function handleSubmit(e: MouseEvent) {
|
||||
if (!formRef.value) return;
|
||||
e.preventDefault();
|
||||
|
||||
formRef.value.validate(errors => {
|
||||
if (!errors) {
|
||||
window.$message?.success('验证成功');
|
||||
} else {
|
||||
window.$message?.error('验证失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
81
src/views/system/login/components/CodeLogin/index.vue
Normal file
81
src/views/system/login/components/CodeLogin/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" placeholder="手机号码" />
|
||||
</n-form-item>
|
||||
<n-form-item path="code">
|
||||
<div class="flex-y-center w-full">
|
||||
<n-input v-model:value="model.code" placeholder="验证码" />
|
||||
<div class="w-18px"></div>
|
||||
<n-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSmsCode">
|
||||
{{ label }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item path="imgCode">
|
||||
<n-input v-model:value="model.imgCode" placeholder="验证码,点击图片刷新" />
|
||||
<div class="pl-8px">
|
||||
<image-verify v-model:code="imgCode" />
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" size="large">
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:block="true"
|
||||
:round="true"
|
||||
:loading="auth.loginLoding"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
确定
|
||||
</n-button>
|
||||
<n-button size="large" :block="true" :round="true" @click="toLoginModule('pwd-login')">返回</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { NForm, NFormItem, NInput, NSpace, NButton } from 'naive-ui';
|
||||
import type { FormInst } from 'naive-ui';
|
||||
import { ImageVerify } from '@/components';
|
||||
import { useAuthStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useSmsCode } from '@/hooks';
|
||||
import { formRules, getImgCodeRule } from '@/utils';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const { login } = useAuthStore();
|
||||
const { toLoginModule } = useRouterPush();
|
||||
const { label, isCounting, loading: smsLoading, getSmsCode } = useSmsCode();
|
||||
|
||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
imgCode: ''
|
||||
});
|
||||
const imgCode = ref('');
|
||||
const rules = {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code,
|
||||
imgCode: getImgCodeRule(imgCode)
|
||||
};
|
||||
|
||||
function handleSmsCode() {
|
||||
getSmsCode(model.phone);
|
||||
}
|
||||
|
||||
function handleSubmit(e: MouseEvent) {
|
||||
if (!formRef.value) return;
|
||||
e.preventDefault();
|
||||
|
||||
formRef.value.validate(errors => {
|
||||
if (!errors) {
|
||||
const { phone, code } = model;
|
||||
login(phone, code, 'sms');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="absolute-lt wh-full overflow-hidden">
|
||||
<div class="absolute-lt z-1 wh-full overflow-hidden">
|
||||
<div class="absolute -right-300px -top-900px">
|
||||
<corner-top :start-color="lightColor" :end-color="darkColor" />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<n-space :vertical="true">
|
||||
<n-divider class="!mb-0 text-14px text-[#666]">其他登录方式</n-divider>
|
||||
<div class="flex-center">
|
||||
<n-button :text="true">
|
||||
<icon-mdi-wechat class="text-22px text-[#888] hover:text-[#52BF5E]" />
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NSpace, NDivider, NButton } from 'naive-ui';
|
||||
</script>
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,3 @@
|
||||
import OtherLogin from './OtherLogin.vue';
|
||||
|
||||
export { OtherLogin };
|
||||
75
src/views/system/login/components/PwdLogin/index.vue
Normal file
75
src/views/system/login/components/PwdLogin/index.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" placeholder="请输入手机号码" />
|
||||
</n-form-item>
|
||||
<n-form-item path="pwd">
|
||||
<n-input v-model:value="model.pwd" type="password" show-password-on="click" placeholder="请输入密码" />
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" :size="24">
|
||||
<div class="flex-y-center justify-between">
|
||||
<n-checkbox v-model:checked="rememberMe">记住我</n-checkbox>
|
||||
<n-button :text="true">忘记密码?</n-button>
|
||||
</div>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:block="true"
|
||||
:round="true"
|
||||
:loading="auth.loginLoding"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
确定
|
||||
</n-button>
|
||||
<div class="flex-y-center justify-between">
|
||||
<n-button class="flex-1" :block="true" @click="toLoginModule('code-login')">
|
||||
{{ EnumLoginModule['code-login'] }}
|
||||
</n-button>
|
||||
<div class="w-12px"></div>
|
||||
<n-button class="flex-1" :block="true" @click="toLoginModule('register')">
|
||||
{{ EnumLoginModule.register }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
<other-login />
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { NForm, NFormItem, NInput, NSpace, NCheckbox, NButton } from 'naive-ui';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
import { EnumLoginModule } from '@/enum';
|
||||
import { useAuthStore } from '@/store';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { formRules } from '@/utils';
|
||||
import { OtherLogin } from './components';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const { login } = useAuthStore();
|
||||
const { toLoginModule } = useRouterPush();
|
||||
|
||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
pwd: ''
|
||||
});
|
||||
const rules: FormRules = {
|
||||
phone: formRules.phone,
|
||||
pwd: formRules.pwd
|
||||
};
|
||||
const rememberMe = ref(false);
|
||||
|
||||
function handleSubmit(e: MouseEvent) {
|
||||
if (!formRef.value) return;
|
||||
e.preventDefault();
|
||||
|
||||
formRef.value.validate(errors => {
|
||||
if (!errors) {
|
||||
const { phone, pwd } = model;
|
||||
login(phone, pwd, 'pwd');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
75
src/views/system/login/components/Register/index.vue
Normal file
75
src/views/system/login/components/Register/index.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" placeholder="手机号码" />
|
||||
</n-form-item>
|
||||
<n-form-item path="code">
|
||||
<div class="flex-y-center w-full">
|
||||
<n-input v-model:value="model.code" placeholder="验证码" />
|
||||
<div class="w-18px"></div>
|
||||
<n-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSmsCode">
|
||||
{{ label }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item path="pwd">
|
||||
<n-input v-model:value="model.pwd" placeholder="密码" />
|
||||
</n-form-item>
|
||||
<n-form-item path="confirmPwd">
|
||||
<n-input v-model:value="model.confirmPwd" placeholder="确认密码" />
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" size="large">
|
||||
<login-agreement v-model:value="agreement" />
|
||||
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
|
||||
<n-button size="large" :block="true" :round="true" @click="toLoginModule('pwd-login')">返回</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs } from 'vue';
|
||||
import { NForm, NFormItem, NInput, NSpace, NButton } from 'naive-ui';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
import { LoginAgreement } from '@/components';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useSmsCode } from '@/hooks';
|
||||
import { formRules, getConfirmPwdRule } from '@/utils';
|
||||
|
||||
const { toLoginModule } = useRouterPush();
|
||||
const { label, isCounting, loading: smsLoading, start } = useSmsCode();
|
||||
|
||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
pwd: '',
|
||||
confirmPwd: ''
|
||||
});
|
||||
const rules: FormRules = {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code,
|
||||
pwd: formRules.pwd,
|
||||
confirmPwd: getConfirmPwdRule(toRefs(model).pwd)
|
||||
};
|
||||
|
||||
const agreement = ref(false);
|
||||
|
||||
function handleSmsCode() {
|
||||
start();
|
||||
}
|
||||
|
||||
function handleSubmit(e: MouseEvent) {
|
||||
if (!formRef.value) return;
|
||||
e.preventDefault();
|
||||
|
||||
formRef.value.validate(errors => {
|
||||
if (!errors) {
|
||||
if (!agreement.value) return;
|
||||
window.$message?.success('验证成功');
|
||||
} else {
|
||||
window.$message?.error('验证失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
71
src/views/system/login/components/ResetPwd/index.vue
Normal file
71
src/views/system/login/components/ResetPwd/index.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<n-form ref="formRef" :model="model" :rules="rules" size="large" :show-label="false">
|
||||
<n-form-item path="phone">
|
||||
<n-input v-model:value="model.phone" placeholder="手机号码" />
|
||||
</n-form-item>
|
||||
<n-form-item path="code">
|
||||
<div class="flex-y-center w-full">
|
||||
<n-input v-model:value="model.code" placeholder="验证码" />
|
||||
<div class="w-18px"></div>
|
||||
<n-button size="large" :disabled="isCounting" :loading="smsLoading" @click="handleSmsCode">
|
||||
{{ label }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item path="pwd">
|
||||
<n-input v-model:value="model.pwd" placeholder="密码" />
|
||||
</n-form-item>
|
||||
<n-form-item path="confirmPwd">
|
||||
<n-input v-model:value="model.confirmPwd" placeholder="确认密码" />
|
||||
</n-form-item>
|
||||
<n-space :vertical="true" size="large">
|
||||
<n-button type="primary" size="large" :block="true" :round="true" @click="handleSubmit">确定</n-button>
|
||||
<n-button size="large" :block="true" :round="true" @click="toLoginModule('pwd-login')">返回</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, toRefs } from 'vue';
|
||||
import { NForm, NFormItem, NInput, NSpace, NButton, useMessage } from 'naive-ui';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
import { useRouterPush } from '@/composables';
|
||||
import { useSmsCode } from '@/hooks';
|
||||
import { formRules, getConfirmPwdRule } from '@/utils';
|
||||
|
||||
const message = useMessage();
|
||||
const { toLoginModule } = useRouterPush();
|
||||
const { label, isCounting, loading: smsLoading, start } = useSmsCode();
|
||||
|
||||
const formRef = ref<(HTMLElement & FormInst) | null>(null);
|
||||
const model = reactive({
|
||||
phone: '',
|
||||
code: '',
|
||||
pwd: '',
|
||||
confirmPwd: ''
|
||||
});
|
||||
const rules: FormRules = {
|
||||
phone: formRules.phone,
|
||||
code: formRules.code,
|
||||
pwd: formRules.pwd,
|
||||
confirmPwd: getConfirmPwdRule(toRefs(model).pwd)
|
||||
};
|
||||
|
||||
function handleSmsCode() {
|
||||
start();
|
||||
}
|
||||
|
||||
function handleSubmit(e: MouseEvent) {
|
||||
if (!formRef.value) return;
|
||||
e.preventDefault();
|
||||
|
||||
formRef.value.validate(errors => {
|
||||
if (!errors) {
|
||||
message.success('验证成功');
|
||||
} else {
|
||||
message.error('验证失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
@@ -1,3 +1,8 @@
|
||||
import LoginBg from './LoginBg/index.vue';
|
||||
import PwdLogin from './PwdLogin/index.vue';
|
||||
import CodeLogin from './CodeLogin/index.vue';
|
||||
import Register from './Register/index.vue';
|
||||
import ResetPwd from './ResetPwd/index.vue';
|
||||
import BindWechat from './BindWechat/index.vue';
|
||||
|
||||
export { LoginBg };
|
||||
export { LoginBg, PwdLogin, CodeLogin, Register, ResetPwd, BindWechat };
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<div class="relative flex-center wh-full" :style="{ backgroundColor: bgColor }">
|
||||
<n-card :bordered="false" size="large" class="z-20 !w-auto rounded-20px shadow-sm">
|
||||
<dark-mode-switch
|
||||
:dark="theme.darkMode"
|
||||
class="absolute left-48px top-24px z-3 text-20px"
|
||||
@update:dark="setDarkMode"
|
||||
/>
|
||||
<n-card :bordered="false" size="large" class="z-4 !w-auto rounded-20px shadow-sm">
|
||||
<div class="w-360px">
|
||||
<header class="flex-y-center justify-between">
|
||||
<div class="w-70px h-70px rounded-35px overflow-hidden">
|
||||
@@ -9,30 +14,70 @@
|
||||
<n-gradient-text type="primary" :size="28">{{ title }}</n-gradient-text>
|
||||
</header>
|
||||
<main class="pt-24px">
|
||||
<h3 class="text-18px text-primary font-medium">登录</h3>
|
||||
<h3 class="text-18px text-primary font-medium">{{ activeModule.label }}</h3>
|
||||
<div class="pt-24px">
|
||||
<transition>
|
||||
<component :is="activeModule.component" />
|
||||
</transition>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</n-card>
|
||||
<login-bg :theme-color="theme.themeColor" />
|
||||
<login-bg :theme-color="bgThemeColor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import { NCard, NGradientText } from 'naive-ui';
|
||||
import { SystemLogo } from '@/components';
|
||||
import { EnumLoginModule } from '@/enum';
|
||||
import { SystemLogo, DarkModeSwitch } from '@/components';
|
||||
import { useThemeStore } from '@/store';
|
||||
import { useAppInfo } from '@/composables';
|
||||
import { mixColor } from '@/utils';
|
||||
import { LoginBg } from './components';
|
||||
import { getColorPalette, mixColor } from '@/utils';
|
||||
import type { LoginModuleKey } from '@/interface';
|
||||
import { LoginBg, PwdLogin, CodeLogin, Register, ResetPwd, BindWechat } from './components';
|
||||
|
||||
interface Props {
|
||||
/** 登录模块分类 */
|
||||
module: LoginModuleKey;
|
||||
}
|
||||
|
||||
interface LoginModule {
|
||||
key: LoginModuleKey;
|
||||
label: EnumLoginModule;
|
||||
component: Component;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const theme = useThemeStore();
|
||||
const { setDarkMode } = useThemeStore();
|
||||
const { title } = useAppInfo();
|
||||
|
||||
const modules: LoginModule[] = [
|
||||
{ key: 'pwd-login', label: EnumLoginModule['pwd-login'], component: PwdLogin },
|
||||
{ key: 'code-login', label: EnumLoginModule['code-login'], component: CodeLogin },
|
||||
{ key: 'register', label: EnumLoginModule.register, component: Register },
|
||||
{ key: 'reset-pwd', label: EnumLoginModule['reset-pwd'], component: ResetPwd },
|
||||
{ key: 'bind-wechat', label: EnumLoginModule['bind-wechat'], component: BindWechat }
|
||||
];
|
||||
|
||||
const activeModule = computed(() => {
|
||||
const active: LoginModule = { ...modules[0] };
|
||||
const findItem = modules.find(item => item.key === props.module);
|
||||
if (findItem) {
|
||||
Object.assign(active, findItem);
|
||||
}
|
||||
return active;
|
||||
});
|
||||
|
||||
const bgThemeColor = computed(() => (theme.darkMode ? getColorPalette(theme.themeColor, 7) : theme.themeColor));
|
||||
|
||||
const bgColor = computed(() => {
|
||||
const COLOR_WHITE = '#ffffff';
|
||||
const darkMode = false;
|
||||
const ratio = darkMode ? 0.6 : 0.2;
|
||||
const ratio = theme.darkMode ? 0.5 : 0.2;
|
||||
return mixColor(COLOR_WHITE, theme.themeColor, ratio);
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user