diff --git a/apps/web-antd/src/api/core/auth.ts b/apps/web-antd/src/api/core/auth.ts index 4071577dd..488c25e8b 100644 --- a/apps/web-antd/src/api/core/auth.ts +++ b/apps/web-antd/src/api/core/auth.ts @@ -24,13 +24,27 @@ export namespace AuthApi { } /** 手机验证码获取接口参数 */ - export interface SmsCodeVO { + export interface SmsCodeParams { mobile: string; scene: number; } /** 手机验证码登录接口参数 */ - export interface SmsLoginVO { + export interface SmsLoginParams { + mobile: string; + code: string; + } + + /** 注册接口参数 */ + export interface RegisterParams { + username: string + password: string + captchaVerification: string + } + + /** 重置密码接口参数 */ + export interface ResetPasswordParams { + password: string; mobile: string; code: string; } @@ -83,4 +97,24 @@ export async function getCaptcha(data: any) { /** 校验验证码 */ export async function checkCaptcha(data: any) { return baseRequestClient.post('/system/captcha/check', data); +} + +/** 获取登录验证码 */ +export const sendSmsCode = (data: AuthApi.SmsCodeParams) => { + return requestClient.post('/system/auth/send-sms-code', data ) +} + +/** 短信验证码登录 */ +export const smsLogin = (data: AuthApi.SmsLoginParams) => { + return requestClient.post('/system/auth/sms-login', data) +} + +/** 注册 */ +export const register = (data: AuthApi.RegisterParams) => { + return requestClient.post('/system/auth/register', data) +} + +/** 通过短信重置密码 */ +export const smsResetPassword = (data: AuthApi.ResetPasswordParams) => { + return requestClient.post('/system/auth/reset-password', data) } \ No newline at end of file diff --git a/apps/web-antd/src/api/infra/job-log/index.ts b/apps/web-antd/src/api/infra/job-log/index.ts new file mode 100644 index 000000000..f2c756d61 --- /dev/null +++ b/apps/web-antd/src/api/infra/job-log/index.ts @@ -0,0 +1,36 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace InfraJobLogApi { + /** 任务日志信息 */ + export interface InfraJobLog { + id?: number; + jobId: number; + handlerName: string; + handlerParam: string; + cronExpression: string; + executeIndex: string; + beginTime: Date; + endTime: Date; + duration: string; + status: number; + createTime?: string; + result: string; + } +} + +/** 查询任务日志列表 */ +export function getJobLogPage(params: PageParam) { + return requestClient.get>('/infra/job-log/page', { params }); +} + +/** 查询任务日志详情 */ +export function getJobLog(id: number) { + return requestClient.get(`/infra/job-log/get?id=${id}`); +} + +/** 导出定时任务日志 */ +export function exportJobLog(params: any) { + return requestClient.download('/infra/job-log/export-excel', { params }); +} diff --git a/apps/web-antd/src/api/infra/job/index.ts b/apps/web-antd/src/api/infra/job/index.ts new file mode 100644 index 000000000..702446921 --- /dev/null +++ b/apps/web-antd/src/api/infra/job/index.ts @@ -0,0 +1,68 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace InfraJobApi { + /** 任务信息 */ + export interface InfraJob { + id?: number; + name: string; + status: number; + handlerName: string; + handlerParam: string; + cronExpression: string; + retryCount: number; + retryInterval: number; + monitorTimeout: number; + createTime?: Date; + } +} + +/** 查询任务列表 */ +export function getJobPage(params: PageParam) { + return requestClient.get>('/infra/job/page', { params }); +} + +/** 查询任务详情 */ +export function getJob(id: number) { + return requestClient.get(`/infra/job/get?id=${id}`); +} + +/** 新增任务 */ +export function createJob(data: InfraJobApi.InfraJob) { + return requestClient.post('/infra/job/create', data); +} + +/** 修改定时任务调度 */ +export function updateJob(data: InfraJobApi.InfraJob) { + return requestClient.put('/infra/job/update', data); +} + +/** 删除定时任务调度 */ +export function deleteJob(id: number) { + return requestClient.delete(`/infra/job/delete?id=${id}`); +} + +/** 导出定时任务调度 */ +export function exportJob(params: any) { + return requestClient.download('/infra/job/export-excel', { params }); +} + +/** 任务状态修改 */ +export function updateJobStatus(id: number, status: number) { + const params = { + id, + status + }; + return requestClient.put('/infra/job/update-status', { params }); +} + +/** 定时任务立即执行一次 */ +export function runJob(id: number) { + return requestClient.put(`/infra/job/trigger?id=${id}`); +} + +/** 获得定时任务的下 n 次执行时间 */ +export function getJobNextTimes(id: number) { + return requestClient.get(`/infra/job/get_next_times?id=${id}`); +} diff --git a/apps/web-antd/src/api/infra/redis/index.ts b/apps/web-antd/src/api/infra/redis/index.ts new file mode 100644 index 000000000..6a2202833 --- /dev/null +++ b/apps/web-antd/src/api/infra/redis/index.ts @@ -0,0 +1,188 @@ +import { requestClient } from '#/api/request'; + +export namespace InfraRedisApi { + /** Redis 监控信息 */ + export interface InfraRedisMonitorInfo { + info: InfraRedisInfo; + dbSize: number; + commandStats: InfraRedisCommandStats[]; + } + + /** Redis 信息 */ + export interface InfraRedisInfo { + io_threaded_reads_processed: string; + tracking_clients: string; + uptime_in_seconds: string; + cluster_connections: string; + current_cow_size: string; + maxmemory_human: string; + aof_last_cow_size: string; + master_replid2: string; + mem_replication_backlog: string; + aof_rewrite_scheduled: string; + total_net_input_bytes: string; + rss_overhead_ratio: string; + hz: string; + current_cow_size_age: string; + redis_build_id: string; + errorstat_BUSYGROUP: string; + aof_last_bgrewrite_status: string; + multiplexing_api: string; + client_recent_max_output_buffer: string; + allocator_resident: string; + mem_fragmentation_bytes: string; + aof_current_size: string; + repl_backlog_first_byte_offset: string; + tracking_total_prefixes: string; + redis_mode: string; + redis_git_dirty: string; + aof_delayed_fsync: string; + allocator_rss_bytes: string; + repl_backlog_histlen: string; + io_threads_active: string; + rss_overhead_bytes: string; + total_system_memory: string; + loading: string; + evicted_keys: string; + maxclients: string; + cluster_enabled: string; + redis_version: string; + repl_backlog_active: string; + mem_aof_buffer: string; + allocator_frag_bytes: string; + io_threaded_writes_processed: string; + instantaneous_ops_per_sec: string; + used_memory_human: string; + total_error_replies: string; + role: string; + maxmemory: string; + used_memory_lua: string; + rdb_current_bgsave_time_sec: string; + used_memory_startup: string; + used_cpu_sys_main_thread: string; + lazyfree_pending_objects: string; + aof_pending_bio_fsync: string; + used_memory_dataset_perc: string; + allocator_frag_ratio: string; + arch_bits: string; + used_cpu_user_main_thread: string; + mem_clients_normal: string; + expired_time_cap_reached_count: string; + unexpected_error_replies: string; + mem_fragmentation_ratio: string; + aof_last_rewrite_time_sec: string; + master_replid: string; + aof_rewrite_in_progress: string; + lru_clock: string; + maxmemory_policy: string; + run_id: string; + latest_fork_usec: string; + tracking_total_items: string; + total_commands_processed: string; + expired_keys: string; + errorstat_ERR: string; + used_memory: string; + module_fork_in_progress: string; + errorstat_WRONGPASS: string; + aof_buffer_length: string; + dump_payload_sanitizations: string; + mem_clients_slaves: string; + keyspace_misses: string; + server_time_usec: string; + executable: string; + lazyfreed_objects: string; + db0: string; + used_memory_peak_human: string; + keyspace_hits: string; + rdb_last_cow_size: string; + aof_pending_rewrite: string; + used_memory_overhead: string; + active_defrag_hits: string; + tcp_port: string; + uptime_in_days: string; + used_memory_peak_perc: string; + current_save_keys_processed: string; + blocked_clients: string; + total_reads_processed: string; + expire_cycle_cpu_milliseconds: string; + sync_partial_err: string; + used_memory_scripts_human: string; + aof_current_rewrite_time_sec: string; + aof_enabled: string; + process_supervised: string; + master_repl_offset: string; + used_memory_dataset: string; + used_cpu_user: string; + rdb_last_bgsave_status: string; + tracking_total_keys: string; + atomicvar_api: string; + allocator_rss_ratio: string; + client_recent_max_input_buffer: string; + clients_in_timeout_table: string; + aof_last_write_status: string; + mem_allocator: string; + used_memory_scripts: string; + used_memory_peak: string; + process_id: string; + master_failover_state: string; + errorstat_NOAUTH: string; + used_cpu_sys: string; + repl_backlog_size: string; + connected_slaves: string; + current_save_keys_total: string; + gcc_version: string; + total_system_memory_human: string; + sync_full: string; + connected_clients: string; + module_fork_last_cow_size: string; + total_writes_processed: string; + allocator_active: string; + total_net_output_bytes: string; + pubsub_channels: string; + current_fork_perc: string; + active_defrag_key_hits: string; + rdb_changes_since_last_save: string; + instantaneous_input_kbps: string; + used_memory_rss_human: string; + configured_hz: string; + expired_stale_perc: string; + active_defrag_misses: string; + used_cpu_sys_children: string; + number_of_cached_scripts: string; + sync_partial_ok: string; + used_memory_lua_human: string; + rdb_last_save_time: string; + pubsub_patterns: string; + slave_expires_tracked_keys: string; + redis_git_sha1: string; + used_memory_rss: string; + rdb_last_bgsave_time_sec: string; + os: string; + mem_not_counted_for_evict: string; + active_defrag_running: string; + rejected_connections: string; + aof_rewrite_buffer_length: string; + total_forks: string; + active_defrag_key_misses: string; + allocator_allocated: string; + aof_base_size: string; + instantaneous_output_kbps: string; + second_repl_offset: string; + rdb_bgsave_in_progress: string; + used_cpu_user_children: string; + total_connections_received: string; + migrate_cached_sockets: string; + } + + /** Redis 命令统计 */ + export interface InfraRedisCommandStats { + command: string; + calls: number; + usec: number; + } +} + +/** 获取 Redis 监控信息 */ +export function getRedisMonitorInfo() { + return requestClient.get('/infra/redis/get-monitor-info'); +} diff --git a/apps/web-antd/src/router/guard.ts b/apps/web-antd/src/router/guard.ts index cca219426..650d110cb 100644 --- a/apps/web-antd/src/router/guard.ts +++ b/apps/web-antd/src/router/guard.ts @@ -1,17 +1,18 @@ import type { Router } from 'vue-router'; import { DEFAULT_HOME_PATH, LOGIN_PATH } from '@vben/constants'; +import { $t } from '@vben/locales'; import { preferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { startProgress, stopProgress } from '@vben/utils'; +import { message } from 'ant-design-vue'; + +import { getSimpleDictDataList } from '#/api/system/dict/data'; import { accessRoutes, coreRouteNames } from '#/router/routes'; import { useAuthStore, useDictStore } from '#/store'; import { generateAccess } from './access'; -import { message } from 'ant-design-vue'; -import { $t } from '@vben/locales'; -import { getSimpleDictDataList } from '#/api/system/dict/data'; /** * 通用守卫配置 @@ -101,12 +102,15 @@ function setupAccessGuard(router: Router) { // 当前登录用户拥有的角色标识列表 let userInfo = userStore.userInfo; if (!userInfo) { - // addy by 芋艿:由于 yudao 是 fetchUserInfo 统一加载用户 + 权限信息,所以将 fetchMenuListAsync + // add by 芋艿:由于 yudao 是 fetchUserInfo 统一加载用户 + 权限信息,所以将 fetchMenuListAsync const loading = message.loading({ content: `${$t('common.loadingMenu')}...`, }); try { - userInfo = (await authStore.fetchUserInfo()).user; + const authPermissionInfo = await authStore.fetchUserInfo(); + if (authPermissionInfo) { + userInfo = authPermissionInfo.user; + } } finally { loading(); } @@ -128,7 +132,7 @@ function setupAccessGuard(router: Router) { userStore.setUserRoles(userRoles); const redirectPath = (from.query.redirect ?? (to.path === DEFAULT_HOME_PATH - ? userInfo.homePath || DEFAULT_HOME_PATH + ? userInfo?.homePath || DEFAULT_HOME_PATH : to.fullPath)) as string; return { diff --git a/apps/web-antd/src/router/routes/modules/infra.ts b/apps/web-antd/src/router/routes/modules/infra.ts index ff06b80c2..0f32b8db6 100644 --- a/apps/web-antd/src/router/routes/modules/infra.ts +++ b/apps/web-antd/src/router/routes/modules/infra.ts @@ -1,6 +1,18 @@ import type { RouteRecordRaw } from 'vue-router'; const routes: RouteRecordRaw[] = [ + { + path: '/infra/job/job-log', + component: () => import('#/views/infra/job/logger/index.vue'), + name: 'InfraJobLog', + meta: { + title: '调度日志', + icon: 'ant-design:history-outlined', + activePath: '/infra/job', + keepAlive: false, + hideInMenu: true, + }, + }, { path: '/codegen', name: 'CodegenEdit', diff --git a/apps/web-antd/src/store/auth.ts b/apps/web-antd/src/store/auth.ts index e4abf9c0a..a66737a27 100644 --- a/apps/web-antd/src/store/auth.ts +++ b/apps/web-antd/src/store/auth.ts @@ -9,7 +9,7 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores'; import { notification } from 'ant-design-vue'; import { defineStore } from 'pinia'; -import { getAuthPermissionInfoApi, loginApi, logoutApi} from '#/api'; +import { type AuthApi, getAuthPermissionInfoApi, loginApi, logoutApi, smsLogin, register } from '#/api'; import { $t } from '#/locales'; export const useAuthStore = defineStore('auth', () => { @@ -22,9 +22,12 @@ export const useAuthStore = defineStore('auth', () => { /** * 异步处理登录操作 * Asynchronously handle the login process + * @param type 登录类型 * @param params 登录表单数据 + * @param onSuccess 登录成功后的回调函数 */ async function authLogin( + type: 'mobile' | 'username' | 'register', params: Recordable, onSuccess?: () => Promise | void, ) { @@ -32,7 +35,9 @@ export const useAuthStore = defineStore('auth', () => { let userInfo: null | UserInfo = null; try { loginLoading.value = true; - const { accessToken, refreshToken } = await loginApi(params); + const { accessToken, refreshToken } = type === 'mobile' ? await smsLogin(params as AuthApi.SmsLoginParams) + : type === 'register' ? await register(params as AuthApi.RegisterParams) + : await loginApi(params); // 如果成功获取到 accessToken if (accessToken) { diff --git a/apps/web-antd/src/utils/tree.ts b/apps/web-antd/src/utils/tree.ts index a7355e980..8797cdad4 100644 --- a/apps/web-antd/src/utils/tree.ts +++ b/apps/web-antd/src/utils/tree.ts @@ -1,56 +1,70 @@ -// TODO @芋艿:1)代码优化;2)是不是抽到公共的? +// todo @芋艿:公用逻辑 +interface TreeNode { + [key: string]: any; + children?: TreeNode[]; +} + /** * 构造树型结构数据 + * * @param {*} data 数据源 * @param {*} id id字段 默认 'id' * @param {*} parentId 父节点字段 默认 'parentId' * @param {*} children 孩子节点字段 默认 'children' */ -export const handleTree = (data: any[], id?: string, parentId?: string, children?: string) => { +export const handleTree = ( + data: TreeNode[], + id: string = 'id', + parentId: string = 'parentId', + children: string = 'children' +): TreeNode[] => { if (!Array.isArray(data)) { - console.warn('data must be an array') - return [] + console.warn('data must be an array'); + return []; } const config = { - id: id || 'id', - parentId: parentId || 'parentId', - childrenList: children || 'children' - } - - const childrenListMap = {} - const nodeIds = {} - const tree: any[] = [] + id, + parentId, + childrenList: children + }; + const childrenListMap: Record = {}; + const nodeIds: Record = {}; + const tree: TreeNode[] = []; + // 1. 数据预处理 + // 1.1 第一次遍历,生成 childrenListMap 和 nodeIds 映射 for (const d of data) { - const parentId = d[config.parentId] - if (childrenListMap[parentId] == null) { - childrenListMap[parentId] = [] + const pId = d[config.parentId]; + if (childrenListMap[pId] === undefined) { + childrenListMap[pId] = []; } - nodeIds[d[config.id]] = d - childrenListMap[parentId].push(d) + nodeIds[d[config.id]] = d; + childrenListMap[pId].push(d); } - + // 1.2 第二次遍历,找出根节点 for (const d of data) { - const parentId = d[config.parentId] - if (nodeIds[parentId] == null) { - tree.push(d) + const pId = d[config.parentId]; + if (nodeIds[pId] === undefined) { + tree.push(d); } } - for (const t of tree) { - adaptToChildrenList(t) - } - - function adaptToChildrenList(o) { - if (childrenListMap[o[config.id]] !== null) { - o[config.childrenList] = childrenListMap[o[config.id]] - } - if (o[config.childrenList]) { - for (const c of o[config.childrenList]) { - adaptToChildrenList(c) + // 2. 构建树结:递归构建子节点 + const adaptToChildrenList = (node: TreeNode): void => { + const nodeId = node[config.id]; + if (childrenListMap[nodeId]) { + node[config.childrenList] = childrenListMap[nodeId]; + // 递归处理子节点 + for (const child of node[config.childrenList]) { + adaptToChildrenList(child); } } + }; + + // 3. 从根节点开始构建完整树 + for (const rootNode of tree) { + adaptToChildrenList(rootNode); } - return tree -} \ No newline at end of file + return tree; +} diff --git a/apps/web-antd/src/views/_core/authentication/code-login.vue b/apps/web-antd/src/views/_core/authentication/code-login.vue index acfd1fd78..d3225ee1c 100644 --- a/apps/web-antd/src/views/_core/authentication/code-login.vue +++ b/apps/web-antd/src/views/_core/authentication/code-login.vue @@ -2,24 +2,102 @@ import type { VbenFormSchema } from '@vben/common-ui'; import type { Recordable } from '@vben/types'; -import { computed, ref } from 'vue'; +import { computed, ref, onMounted } from 'vue'; import { AuthenticationCodeLogin, z } from '@vben/common-ui'; import { $t } from '@vben/locales'; +import { type AuthApi, sendSmsCode } from '#/api'; +import { useAppConfig } from '@vben/hooks'; +import { message } from 'ant-design-vue'; + +import { getTenantSimpleList, getTenantByWebsite } from '#/api/core/auth'; +import { useAccessStore } from '@vben/stores'; +import { useAuthStore } from '#/store'; +const { tenantEnable } = useAppConfig(import.meta.env, import.meta.env.PROD); defineOptions({ name: 'CodeLogin' }); +const authStore = useAuthStore(); +const accessStore = useAccessStore(); + const loading = ref(false); -const CODE_LENGTH = 6; +const CODE_LENGTH = 4; + +const loginRef = ref(); + +/** 获取租户列表,并默认选中 */ +const tenantList = ref([]); // 租户列表 +const fetchTenantList = async () => { + if (!tenantEnable) { + return; + } + try { + // 获取租户列表、域名对应租户 + const websiteTenantPromise = getTenantByWebsite(window.location.hostname); + tenantList.value = await getTenantSimpleList(); + + // 选中租户:域名 > store 中的租户 > 首个租户 + let tenantId: number | null = null; + const websiteTenant = await websiteTenantPromise; + if (websiteTenant?.id) { + tenantId = websiteTenant.id; + } + // 如果没有从域名获取到租户,尝试从 store 中获取 + if (!tenantId && accessStore.tenantId) { + tenantId = accessStore.tenantId; + } + // 如果还是没有租户,使用列表中的第一个 + if (!tenantId && tenantList.value?.[0]?.id) { + tenantId = tenantList.value[0].id; + } + + // 设置选中的租户编号 + accessStore.setTenantId(tenantId); + loginRef.value.getFormApi().setFieldValue('tenantId', tenantId); + } catch (error) { + console.error('获取租户列表失败:', error); + } +}; + +/** 组件挂载时获取租户信息 */ +onMounted(() => { + fetchTenantList(); +}); const formSchema = computed((): VbenFormSchema[] => { return [ + { + component: 'VbenSelect', + componentProps: { + options: tenantList.value.map((item) => ({ + label: item.name, + value: item.id, + })), + placeholder: $t('authentication.tenantTip'), + }, + fieldName: 'tenantId', + label: $t('authentication.tenant'), + rules: z + .number() + .nullable() + .refine((val) => val != null && val > 0, $t('authentication.tenantTip')) + .default(null), + dependencies: { + triggerFields: ['tenantId'], + if: tenantEnable, + trigger(values) { + if (values.tenantId) { + accessStore.setTenantId(values.tenantId); + } + }, + }, + }, { component: 'VbenInput', componentProps: { placeholder: $t('authentication.mobile'), }, - fieldName: 'phoneNumber', + fieldName: 'mobile', label: $t('authentication.mobile'), rules: z .string() @@ -40,6 +118,29 @@ const formSchema = computed((): VbenFormSchema[] => { return text; }, placeholder: $t('authentication.code'), + handleSendCode: async () => { + loading.value = true; + try { + const formApi = loginRef.value?.getFormApi(); + if (!formApi) { + throw new Error('表单未准备好'); + } + // 验证手机号 + await formApi.validateField('mobile'); + const isMobileValid = await formApi.isFieldValid('mobile'); + if (!isMobileValid) { + throw new Error('请输入有效的手机号码'); + } + + // 发送验证码 + const { mobile } = await formApi.getValues(); + const scene = 21; // 场景:短信验证码登录 + await sendSmsCode({ mobile, scene }); + message.success('验证码发送成功'); + } finally { + loading.value = false; + } + } }, fieldName: 'code', label: $t('authentication.code'), @@ -55,13 +156,17 @@ const formSchema = computed((): VbenFormSchema[] => { * @param values 登录表单数据 */ async function handleLogin(values: Recordable) { - // eslint-disable-next-line no-console - console.log(values); + try { + await authStore.authLogin('mobile', values); + } catch (error) { + console.error('Error in handleLogin:', error); + } }