物业代码生成
This commit is contained in:
143
apps/web-antd/src/store/auth.ts
Normal file
143
apps/web-antd/src/store/auth.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import type { LoginAndRegisterParams } from '@vben/common-ui';
|
||||
import type { UserInfo } from '@vben/types';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { LOGIN_PATH } from '@vben/constants';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { doLogout, getUserInfoApi, loginApi, seeConnectionClose } from '#/api';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useDictStore } from './dict';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
|
||||
const loginLoading = ref(false);
|
||||
|
||||
/**
|
||||
* 异步处理登录操作
|
||||
* Asynchronously handle the login process
|
||||
* @param params 登录表单数据
|
||||
*/
|
||||
async function authLogin(
|
||||
params: LoginAndRegisterParams,
|
||||
onSuccess?: () => Promise<void> | void,
|
||||
) {
|
||||
// 异步处理用户登录操作并获取 accessToken
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const { access_token } = await loginApi(params);
|
||||
|
||||
// 将 accessToken 存储到 accessStore 中
|
||||
accessStore.setAccessToken(access_token);
|
||||
accessStore.setRefreshToken(access_token);
|
||||
|
||||
// 获取用户信息并存储到 accessStore 中
|
||||
userInfo = await fetchUserInfo();
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
userStore.setUserInfo(userInfo);
|
||||
/**
|
||||
* 在这里设置权限
|
||||
*/
|
||||
accessStore.setAccessCodes(userInfo.permissions);
|
||||
|
||||
if (accessStore.loginExpired) {
|
||||
accessStore.setLoginExpired(false);
|
||||
} else {
|
||||
onSuccess
|
||||
? await onSuccess?.()
|
||||
: await router.push(preferences.app.defaultHomePath);
|
||||
}
|
||||
|
||||
if (userInfo?.realName) {
|
||||
notification.success({
|
||||
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
|
||||
duration: 3,
|
||||
message: $t('authentication.loginSuccess'),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
};
|
||||
}
|
||||
|
||||
async function logout(redirect: boolean = true) {
|
||||
try {
|
||||
await seeConnectionClose();
|
||||
await doLogout();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
resetAllStores();
|
||||
accessStore.setLoginExpired(false);
|
||||
|
||||
// 回登陆页带上当前路由地址
|
||||
await router.replace({
|
||||
path: LOGIN_PATH,
|
||||
query: redirect
|
||||
? {
|
||||
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
|
||||
}
|
||||
: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserInfo() {
|
||||
const backUserInfo = await getUserInfoApi();
|
||||
/**
|
||||
* 登录超时的情况
|
||||
*/
|
||||
if (!backUserInfo) {
|
||||
throw new Error('获取用户信息失败.');
|
||||
}
|
||||
const { permissions = [], roles = [], user } = backUserInfo;
|
||||
/**
|
||||
* 从后台user -> vben user转换
|
||||
*/
|
||||
const userInfo: UserInfo = {
|
||||
avatar: user.avatar ?? '',
|
||||
permissions,
|
||||
realName: user.nickName,
|
||||
roles,
|
||||
userId: user.userId,
|
||||
username: user.userName,
|
||||
};
|
||||
userStore.setUserInfo(userInfo);
|
||||
/**
|
||||
* 需要重新加载字典
|
||||
* 比如退出登录切换到其他租户
|
||||
*/
|
||||
const dictStore = useDictStore();
|
||||
dictStore.resetCache();
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
$reset,
|
||||
authLogin,
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
};
|
||||
});
|
109
apps/web-antd/src/store/dict.ts
Normal file
109
apps/web-antd/src/store/dict.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { DictData } from '#/api/system/dict/dict-data-model';
|
||||
|
||||
import { reactive } from 'vue';
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
/**
|
||||
* antd使用 select和radio通用
|
||||
* 本质上是对DictData的拓展
|
||||
*/
|
||||
export interface DictOption extends DictData {
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字典数据转为Options
|
||||
* @param data 字典数据
|
||||
* @param formatNumber 是否需要将value格式化为number类型
|
||||
* @returns options
|
||||
*/
|
||||
export function dictToOptions(
|
||||
data: DictData[],
|
||||
formatNumber = false,
|
||||
): DictOption[] {
|
||||
return data.map((item) => ({
|
||||
...item,
|
||||
label: item.dictLabel,
|
||||
value: formatNumber ? Number(item.dictValue) : item.dictValue,
|
||||
}));
|
||||
}
|
||||
|
||||
export const useDictStore = defineStore('app-dict', () => {
|
||||
/**
|
||||
* select radio checkbox等使用 只能为固定格式{label, value}
|
||||
*/
|
||||
const dictOptionsMap = reactive(new Map<string, DictOption[]>());
|
||||
/**
|
||||
* 添加一个字典请求状态的缓存
|
||||
*
|
||||
* 主要解决多次请求重复api的问题(不能用abortController 会导致除了第一个其他的获取的全为空)
|
||||
* 比如在一个页面 index表单 modal drawer总共会请求三次 但是获取的都是一样的数据
|
||||
* 相当于加锁 保证只有第一次请求的结果能拿到
|
||||
*/
|
||||
const dictRequestCache = reactive(
|
||||
new Map<string, Promise<DictData[] | void>>(),
|
||||
);
|
||||
|
||||
function getDictOptions(dictName: string): DictOption[] {
|
||||
if (!dictName) return [];
|
||||
// 没有key 添加一个空数组
|
||||
if (!dictOptionsMap.has(dictName)) {
|
||||
dictOptionsMap.set(dictName, []);
|
||||
}
|
||||
// 这里拿到的就不可能为空了
|
||||
return dictOptionsMap.get(dictName)!;
|
||||
}
|
||||
|
||||
function resetCache() {
|
||||
dictRequestCache.clear();
|
||||
dictOptionsMap.clear();
|
||||
/**
|
||||
* 不需要清空dictRequestCache 每次请求成功/失败都清空key
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心逻辑
|
||||
*
|
||||
* 不能直接粗暴使用set 会导致之前return的空数组跟现在的数组指向不是同一个地址 数据也就为空了
|
||||
*
|
||||
* 判断是否已经存在key 并且数组长度为0 说明该次要处理的数据是return的空数组 直接push(不修改指向)
|
||||
* 否则 直接set
|
||||
*
|
||||
*/
|
||||
function setDictInfo(
|
||||
dictName: string,
|
||||
dictValue: DictData[],
|
||||
formatNumber = false,
|
||||
) {
|
||||
if (
|
||||
dictOptionsMap.has(dictName) &&
|
||||
dictOptionsMap.get(dictName)?.length === 0
|
||||
) {
|
||||
dictOptionsMap
|
||||
.get(dictName)
|
||||
?.push(...dictToOptions(dictValue, formatNumber));
|
||||
} else {
|
||||
dictOptionsMap.set(dictName, dictToOptions(dictValue, formatNumber));
|
||||
}
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
/**
|
||||
* doNothing
|
||||
*/
|
||||
}
|
||||
|
||||
return {
|
||||
$reset,
|
||||
dictOptionsMap,
|
||||
dictRequestCache,
|
||||
getDictOptions,
|
||||
resetCache,
|
||||
setDictInfo,
|
||||
};
|
||||
});
|
2
apps/web-antd/src/store/index.ts
Normal file
2
apps/web-antd/src/store/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './auth';
|
||||
export * from './notify';
|
149
apps/web-antd/src/store/notify.ts
Normal file
149
apps/web-antd/src/store/notify.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { NotificationItem } from '@vben/layouts';
|
||||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { SvgMessageUrl } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||
|
||||
import { useEventSource } from '@vueuse/core';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
const { apiURL, clientId, sseEnable } = useAppConfig(
|
||||
import.meta.env,
|
||||
import.meta.env.PROD,
|
||||
);
|
||||
|
||||
export const useNotifyStore = defineStore(
|
||||
'app-notify',
|
||||
() => {
|
||||
/**
|
||||
* return才会被持久化 存储全部消息
|
||||
*/
|
||||
const notificationList = ref<NotificationItem[]>([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userId = computed(() => {
|
||||
return userStore.userInfo?.userId || '0';
|
||||
});
|
||||
|
||||
const notifications = computed(() => {
|
||||
return notificationList.value.filter(
|
||||
(item) => item.userId === userId.value,
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* 开始监听sse消息
|
||||
*/
|
||||
function startListeningMessage() {
|
||||
/**
|
||||
* 未开启 不监听
|
||||
*/
|
||||
if (!sseEnable) {
|
||||
return;
|
||||
}
|
||||
const accessStore = useAccessStore();
|
||||
const token = accessStore.accessToken;
|
||||
|
||||
const sseAddr = `${apiURL}/resource/sse?clientid=${clientId}&Authorization=Bearer ${token}`;
|
||||
|
||||
const { data } = useEventSource(sseAddr, [], {
|
||||
autoReconnect: {
|
||||
delay: 1000,
|
||||
onFailed() {
|
||||
console.error('sse重连失败.');
|
||||
},
|
||||
retries: 3,
|
||||
},
|
||||
});
|
||||
|
||||
watch(data, (message) => {
|
||||
if (!message) return;
|
||||
console.log(`接收到消息: ${message}`);
|
||||
|
||||
notification.success({
|
||||
description: message,
|
||||
duration: 3,
|
||||
message: $t('component.notice.received'),
|
||||
});
|
||||
|
||||
notificationList.value.unshift({
|
||||
// avatar: `https://api.multiavatar.com/${random(0, 10_000)}.png`, 随机头像
|
||||
avatar: SvgMessageUrl,
|
||||
date: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
isRead: false,
|
||||
message,
|
||||
title: $t('component.notice.title'),
|
||||
userId: userId.value,
|
||||
});
|
||||
|
||||
// 需要手动置空 vue3在值相同时不会触发watch
|
||||
data.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置全部已读
|
||||
*/
|
||||
function setAllRead() {
|
||||
notificationList.value
|
||||
.filter((item) => item.userId === userId.value)
|
||||
.forEach((item) => {
|
||||
item.isRead = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单条消息已读
|
||||
* @param item 通知
|
||||
*/
|
||||
function setRead(item: NotificationItem) {
|
||||
!item.isRead && (item.isRead = true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空全部消息
|
||||
*/
|
||||
function clearAllMessage() {
|
||||
notificationList.value = notificationList.value.filter(
|
||||
(item) => item.userId !== userId.value,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 只需要空实现即可
|
||||
* 否则会在退出登录清空所有
|
||||
*/
|
||||
function $reset() {
|
||||
// notificationList.value = [];
|
||||
}
|
||||
/**
|
||||
* 显示小圆点
|
||||
*/
|
||||
const showDot = computed(() =>
|
||||
notificationList.value
|
||||
.filter((item) => item.userId === userId.value)
|
||||
.some((item) => !item.isRead),
|
||||
);
|
||||
|
||||
return {
|
||||
$reset,
|
||||
clearAllMessage,
|
||||
notificationList,
|
||||
notifications,
|
||||
setAllRead,
|
||||
setRead,
|
||||
showDot,
|
||||
startListeningMessage,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
pick: ['notificationList'],
|
||||
},
|
||||
},
|
||||
);
|
44
apps/web-antd/src/store/tenant.ts
Normal file
44
apps/web-antd/src/store/tenant.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { TenantOption } from '#/api/core/auth';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { tenantList as tenantListApi } from '#/api/core/auth';
|
||||
|
||||
/**
|
||||
* 用于超级管理员切换租户
|
||||
*/
|
||||
export const useTenantStore = defineStore('app-tenant', () => {
|
||||
// 是否已经选中租户
|
||||
const checked = ref(false);
|
||||
// 是否开启租户功能
|
||||
const tenantEnable = ref(true);
|
||||
const tenantList = ref<TenantOption[]>([]);
|
||||
|
||||
// 初始化 获取租户信息
|
||||
async function initTenant() {
|
||||
const { tenantEnabled, voList } = await tenantListApi();
|
||||
tenantEnable.value = tenantEnabled;
|
||||
tenantList.value = voList;
|
||||
}
|
||||
|
||||
async function setChecked(_checked: boolean) {
|
||||
checked.value = _checked;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
checked.value = false;
|
||||
tenantEnable.value = true;
|
||||
tenantList.value = [];
|
||||
}
|
||||
|
||||
return {
|
||||
$reset,
|
||||
checked,
|
||||
initTenant,
|
||||
setChecked,
|
||||
tenantEnable,
|
||||
tenantList,
|
||||
};
|
||||
});
|
Reference in New Issue
Block a user