feat(projects): 迁移登录完成

This commit is contained in:
Soybean
2022-01-05 01:35:32 +08:00
parent f5a36a05cb
commit b93b80cb4b
54 changed files with 1679 additions and 260 deletions

View File

@@ -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>

View 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>

View File

@@ -0,0 +1,3 @@
import LoginAgreement from './LoginAgreement/index.vue';
export { LoginAgreement };

View 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>

View File

@@ -1,3 +1,4 @@
import SystemLogo from './SystemLogo/index.vue';
import DarkModeSwitch from './DarkModeSwitch/index.vue';
export { SystemLogo };
export { SystemLogo, DarkModeSwitch };

View 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>

View File

@@ -0,0 +1,3 @@
import ImageVerify from './ImageVerify/index.vue';
export { ImageVerify };

View File

@@ -1 +1,3 @@
export * from './common';
export * from './custom';
export * from './business';

View File

@@ -1 +1,3 @@
export * from './system';
export * from './router';
export * from './route';

View 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;
}

View 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
};
}

View File

@@ -1 +1,2 @@
export * from './service';
export * from './regexp';

View 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}$/;

View File

@@ -0,0 +1,5 @@
import useCountDown from './useCountDown';
import useSmsCode from './useSmsCode';
import useImageVerify from './useImageVerify';
export { useCountDown, useSmsCode, useImageVerify };

View 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
};
}

View 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;
}

View 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
};
}

View File

@@ -1 +1,2 @@
export * from './common';
export * from './business';

View File

@@ -2,5 +2,3 @@ import { EnumLoginModule } from '@/enum';
/** 登录模块 */
export type LoginModuleKey = keyof typeof EnumLoginModule;
export type LoginModuleRegexp = LoginModuleKey;

View File

@@ -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) {
// 添加动态路由

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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));

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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
};
}

View File

@@ -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
View File

@@ -0,0 +1,10 @@
/** 后端返回的用户权益相关类型 */
declare namespace ApiAuth {
/** 返回的token和刷新token */
interface Token {
token: string;
refreshToken: string;
}
/** 返回的用户信息 */
type UserInfo = Auth.UserInfo;
}

View File

@@ -1,7 +1,7 @@
/** 后端返回的路由相关类型 */
declare namespace ApiRoute {
/** 后端返回的路由数据类型 */
interface ResponseRoute {
interface Route {
/** 动态路由 */
routes: AuthRoute.Route[];
/** 路由首页对应的key */

View File

@@ -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;
}
}

View File

@@ -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>);
}
}

View File

@@ -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'>;
}

View File

@@ -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;

View File

@@ -1,4 +1,5 @@
export * from './typeof';
export * from './console';
export * from './color';
export * from './number';
export * from './design-pattern';

View 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
View File

@@ -0,0 +1 @@
export * from './rule';

73
src/utils/form/rule.ts Normal file
View 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() === '';
}

View File

@@ -3,3 +3,4 @@ export * from './storage';
export * from './service';
export * from './auth';
export * from './router';
export * from './form';

View File

@@ -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);
}

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
import OtherLogin from './OtherLogin.vue';
export { OtherLogin };

View 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>

View 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>

View 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>

View File

@@ -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 };

View File

@@ -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>