物业代码生成

This commit is contained in:
2025-06-18 11:03:42 +08:00
commit 1262d4c745
1881 changed files with 249599 additions and 0 deletions

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

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

View File

@@ -0,0 +1,2 @@
export * from './auth';
export * from './notify';

View 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'],
},
},
);

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