From eba6bf105e1d143112d7adcc74389ba4a4440de2 Mon Sep 17 00:00:00 2001 From: chenhao Date: Thu, 7 May 2026 09:46:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=B9=B3=E5=8F=B0?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E4=BF=A1=E6=81=AF=E5=92=8C=E5=A4=9A?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 `auth.ts` 和 `platform.ts` 中添加获取平台运行时信息的 API - 在 `AppLayout.tsx` 和 `login/index.tsx` 中处理平台运行时信息,并更新用户界面 - 在 `DeviceInfoMapper.java` 中添加忽略租户的更新方法 - 更新日志清理和模块获取的 API 以支持多租户 - 在 `tenants/index.tsx` 和 `logs/index.tsx` 中添加多租户相关的逻辑和 UI 改进 --- .../com/imeeting/mapper/DeviceInfoMapper.java | 28 +++ .../DeviceOnlineManagementServiceImpl.java | 12 +- frontend/src/api/auth.ts | 3 + frontend/src/api/index.ts | 14 +- frontend/src/api/platform.ts | 21 +- frontend/src/layouts/AppLayout.tsx | 187 ++++++++++-------- frontend/src/locales/en-US.json | 3 + frontend/src/locales/zh-CN.json | 3 + frontend/src/pages/auth/login/index.tsx | 31 +-- .../src/pages/organization/tenants/index.tsx | 15 +- frontend/src/pages/system/logs/index.tsx | 58 ++++-- frontend/src/types/index.ts | 13 +- 12 files changed, 254 insertions(+), 134 deletions(-) diff --git a/backend/src/main/java/com/imeeting/mapper/DeviceInfoMapper.java b/backend/src/main/java/com/imeeting/mapper/DeviceInfoMapper.java index 8357312..3825e52 100644 --- a/backend/src/main/java/com/imeeting/mapper/DeviceInfoMapper.java +++ b/backend/src/main/java/com/imeeting/mapper/DeviceInfoMapper.java @@ -7,6 +7,7 @@ import com.imeeting.entity.biz.DeviceInfoEntity; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; import java.util.List; @@ -24,6 +25,33 @@ public interface DeviceInfoMapper extends BaseMapper { """) DeviceInfoEntity selectByDeviceCodeIgnoreTenant(@Param("deviceCode") String deviceCode); + @InterceptorIgnore(tenantLine = "true") + @Update(""" + + """) + int updateConnectionInfoByIdIgnoreTenant(DeviceInfoEntity deviceInfoEntity); + + @InterceptorIgnore(tenantLine = "true") + @Update(""" + UPDATE biz_device_info + SET last_online_at = #{lastOnlineAt}, + updated_at = CURRENT_TIMESTAMP + WHERE device_id = #{deviceId} + AND is_deleted = 0 + """) + int updateLastOnlineAtByIdIgnoreTenant(@Param("deviceId") Long deviceId, + @Param("lastOnlineAt") java.time.LocalDateTime lastOnlineAt); + @InterceptorIgnore(tenantLine = "true") @Select(""" SELECT * diff --git a/backend/src/main/java/com/imeeting/service/biz/impl/DeviceOnlineManagementServiceImpl.java b/backend/src/main/java/com/imeeting/service/biz/impl/DeviceOnlineManagementServiceImpl.java index 74333b4..b16f230 100644 --- a/backend/src/main/java/com/imeeting/service/biz/impl/DeviceOnlineManagementServiceImpl.java +++ b/backend/src/main/java/com/imeeting/service/biz/impl/DeviceOnlineManagementServiceImpl.java @@ -52,13 +52,9 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement existing.setTerminalType(normalizeTerminalType(authContext.getPlatform())); existing.setTerminalVersion(normalize(authContext.getAppVersion())); existing.setLastOnlineAt(now); - if (existing.getUserId() == null && authContext.getUserId() != null) { - existing.setUserId(authContext.getUserId()); - } - if (existing.getTenantId() == null && authContext.getTenantId() != null) { - existing.setTenantId(authContext.getTenantId()); - } - deviceInfoMapper.updateById(existing); + existing.setUserId(authContext.getUserId()); + existing.setTenantId(authContext.getTenantId()); + deviceInfoMapper.updateConnectionInfoByIdIgnoreTenant(existing); } @Override @@ -71,7 +67,7 @@ public class DeviceOnlineManagementServiceImpl implements DeviceOnlineManagement return; } existing.setLastOnlineAt(toLocalDateTime(lastSeenAtMillis)); - deviceInfoMapper.updateById(existing); + deviceInfoMapper.updateLastOnlineAtByIdIgnoreTenant(existing.getDeviceId(), existing.getLastOnlineAt()); } @Override diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index 03de690..f0fd372 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -17,6 +17,9 @@ export interface TokenResponse { accessExpiresInMinutes: number; refreshExpiresInDays: number; availableTenants?: TenantInfo[]; + tenantMode?: "single" | "multi"; + multiTenantEnabled?: boolean; + currentTenantId?: number; } export interface LoginPayload { diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index cdb905a..a931f1e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -218,15 +218,17 @@ export async function fetchLogs(params: any) { const resp = await http.get("/sys/api/logs", { params }); return resp.data.data; } -export async function cleanLogs(logType: string) { - const resp = await http.delete("/sys/api/logs/clean", { params: { logType } }); - return resp.data.data as boolean; -} -export async function fetchLogModules() { - const resp = await http.get("/sys/api/logs/modules"); + +export async function fetchLogModules(params?: { tenantId?: number }) { + const resp = await http.get("/sys/api/logs/modules", { params }); return resp.data.data as string[]; } +export async function cleanLogs(logType: string, tenantId?: number) { + const resp = await http.delete("/sys/api/logs/clean", { params: { logType, tenantId } }); + return resp.data.data as boolean; +} + export * from "./dict"; export * from "./tenant"; export * from "./org"; diff --git a/frontend/src/api/platform.ts b/frontend/src/api/platform.ts index 49790ce..0ae25cb 100644 --- a/frontend/src/api/platform.ts +++ b/frontend/src/api/platform.ts @@ -1,5 +1,18 @@ import http from "./http"; -import { SysPlatformConfig } from "../types"; +import { PlatformRuntime, SysPlatformConfig } from "../types"; + +const ALLOWED_IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/bmp"]); +const ALLOWED_IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".bmp"]; + +function validatePlatformImage(file: File) { + const lowerName = file.name.toLowerCase(); + const extensionAllowed = ALLOWED_IMAGE_EXTENSIONS.some((extension) => lowerName.endsWith(extension)); + const typeAllowed = ALLOWED_IMAGE_TYPES.has(file.type); + + if (!extensionAllowed || !typeAllowed) { + throw new Error("Only PNG, JPG, GIF, or BMP images are allowed"); + } +} /** * 获取公开平台配置 @@ -17,6 +30,11 @@ export async function getAdminPlatformConfig() { return resp.data.data as SysPlatformConfig; } +export async function getPlatformRuntime() { + const resp = await http.get("/sys/api/platform/runtime"); + return resp.data.data as PlatformRuntime; +} + /** * 更新平台配置 */ @@ -30,6 +48,7 @@ export async function updatePlatformConfig(payload: SysPlatformConfig) { * @param file 文件对象 */ export async function uploadPlatformAsset(file: File) { + validatePlatformImage(file); const formData = new FormData(); formData.append("file", file); const resp = await http.post("/sys/api/admin/platform/config/upload", formData, { diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index 2e5bbe3..7c00ec8 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -17,15 +17,15 @@ import { UserOutlined, VideoCameraOutlined } from "@ant-design/icons"; -import { Avatar, Button, Dropdown, Layout, Menu, Space, type MenuProps, App } from 'antd'; +import { Avatar, Button, Dropdown, Layout, Menu, Space, message, type MenuProps } from "antd"; import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { getCurrentUser, listMyPermissions } from "@/api"; +import { getCurrentUser, getPlatformRuntime, listMyPermissions } from "@/api"; import { switchTenant, type TenantInfo } from "@/api/auth"; import { useAuth } from "@/hooks/useAuth"; import { usePermission } from "@/hooks/usePermission"; -import type { SysPermission, SysPlatformConfig } from "@/types"; +import type { PlatformRuntime, SysPermission, SysPlatformConfig } from "@/types"; import ThemeSelector from "@/components/ThemeSelector/ThemeSelector"; import { useThemeStore } from "@/store/themeStore"; @@ -55,22 +55,42 @@ function resolveMenuIcon(icon?: string): ReactNode { type PermissionMenuNode = SysPermission & { children?: PermissionMenuNode[]; }; + type CachedUserProfile = { displayName?: string; username?: string; avatarUrl?: string }; function getAvatarUrl(profile?: CachedUserProfile | null) { return profile?.avatarUrl?.trim() || ""; } -function getDisplayName(profile: CachedUserProfile | null | undefined, fallbackLabel: string) { - return profile?.displayName || profile?.username || localStorage.getItem("displayName") || localStorage.getItem("username") || fallbackLabel; -} export default function AppLayout() { - const { message } = App.useApp(); const { t, i18n } = useTranslation(); const [collapsed, setCollapsed] = useState(false); const [menus, setMenus] = useState([]); const [availableTenants, setAvailableTenants] = useState([]); const [currentTenantId, setCurrentTenantId] = useState(null); + const [platformRuntime, setPlatformRuntime] = useState(() => { + const runtimeStr = sessionStorage.getItem("platformRuntime"); + return runtimeStr ? JSON.parse(runtimeStr) : null; + }); + const [currentUserLabel, setCurrentUserLabel] = useState(() => { + try { + const profileStr = sessionStorage.getItem("userProfile"); + if (profileStr) { + const profile = JSON.parse(profileStr) as CachedUserProfile; + return profile.displayName || profile.username || localStorage.getItem("username") || ""; + } + } catch { + } + return localStorage.getItem("displayName") || localStorage.getItem("username") || ""; + }); + const [currentUserAvatarUrl, setCurrentUserAvatarUrl] = useState(() => { + try { + const profileStr = sessionStorage.getItem("userProfile"); + return profileStr ? getAvatarUrl(JSON.parse(profileStr) as CachedUserProfile) : ""; + } catch { + return ""; + } + }); const [openKeys, setOpenKeys] = useState([]); const [platformConfig, setPlatformConfig] = useState(() => { const configStr = sessionStorage.getItem("platformConfig"); @@ -83,27 +103,6 @@ export default function AppLayout() { const { load: loadPermissions, can } = usePermission(); const { layoutMode } = useThemeStore(); - const [currentUserDisplayName, setCurrentUserDisplayName] = useState(() => { - try { - const profileStr = sessionStorage.getItem("userProfile"); - if (profileStr) { - const profile = JSON.parse(profileStr) as CachedUserProfile; - return getDisplayName(profile, t("layout.admin")); - } - } catch { - // Ignore invalid cached profile and continue with storage fallback. - } - - return getDisplayName(null, t("layout.admin")); - }); - const [currentUserAvatarUrl, setCurrentUserAvatarUrl] = useState(() => { - try { - const profileStr = sessionStorage.getItem("userProfile"); - return profileStr ? getAvatarUrl(JSON.parse(profileStr) as CachedUserProfile) : ""; - } catch { - return ""; - } - }); const fetchInitialData = useCallback(async () => { try { const storedTenants = localStorage.getItem("availableTenants"); @@ -116,11 +115,42 @@ export default function AppLayout() { setCurrentTenantId(Number(activeTenantId)); } + let runtime: PlatformRuntime | null = null; + try { + runtime = await getPlatformRuntime(); + sessionStorage.setItem("platformRuntime", JSON.stringify(runtime)); + setPlatformRuntime(runtime); + if (runtime.currentTenantId) { + setCurrentTenantId(runtime.currentTenantId); + localStorage.setItem("activeTenantId", String(runtime.currentTenantId)); + } + } catch { + sessionStorage.removeItem("platformRuntime"); + setPlatformRuntime(null); + } + + try { + const profile = await getCurrentUser(); + sessionStorage.setItem("userProfile", JSON.stringify(profile)); + if (profile.username) { + localStorage.setItem("username", profile.username); + } + if (profile.displayName) { + localStorage.setItem("displayName", profile.displayName); + } + setCurrentUserLabel(profile.displayName || profile.username || ""); + setCurrentUserAvatarUrl(getAvatarUrl(profile)); + } catch { + const cached = localStorage.getItem("displayName") || localStorage.getItem("username") || ""; + setCurrentUserLabel(cached); + } + const data = await listMyPermissions(); await loadPermissions(); const filtered = data .filter((item) => (item.permType === "menu" || item.permType === "directory") && item.isVisible === 1 && item.status === 1) + .filter((item) => runtime?.tenantMode === "single" ? item.code !== "menu:tenant" && item.code !== "menu:tenants" && item.path !== "/tenants" : true) .sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); setMenus(filtered); } catch { @@ -131,30 +161,23 @@ export default function AppLayout() { useEffect(() => { fetchInitialData(); }, [fetchInitialData]); + useEffect(() => { const syncUserProfile = () => { try { const profileStr = sessionStorage.getItem("userProfile"); - if (!profileStr) { - setCurrentUserDisplayName(getDisplayName(null, t("layout.admin"))); - setCurrentUserAvatarUrl(""); - return; - } + if (!profileStr) return; const profile = JSON.parse(profileStr) as CachedUserProfile; - localStorage.setItem("displayName", profile.displayName || profile.username || ""); - localStorage.setItem("username", profile.username || localStorage.getItem("username") || ""); - setCurrentUserDisplayName(getDisplayName(profile, t("layout.admin"))); + setCurrentUserLabel(profile.displayName || profile.username || localStorage.getItem("username") || ""); setCurrentUserAvatarUrl(getAvatarUrl(profile)); } catch { - setCurrentUserDisplayName(getDisplayName(null, t("layout.admin"))); - setCurrentUserAvatarUrl(""); } }; - syncUserProfile(); window.addEventListener("user-profile-updated", syncUserProfile); return () => window.removeEventListener("user-profile-updated", syncUserProfile); - }, [t]); + }, []); + useEffect(() => { const syncPlatformConfig = () => { const configStr = sessionStorage.getItem("platformConfig"); @@ -177,9 +200,14 @@ export default function AppLayout() { const profile = await getCurrentUser(); sessionStorage.setItem("userProfile", JSON.stringify(profile)); - localStorage.setItem("displayName", profile.displayName || profile.username || ""); - localStorage.setItem("username", profile.username || localStorage.getItem("username") || ""); - window.dispatchEvent(new Event("user-profile-updated")); + if (profile.username) { + localStorage.setItem("username", profile.username); + } + if (profile.displayName) { + localStorage.setItem("displayName", profile.displayName); + } + setCurrentUserLabel(profile.displayName || profile.username || ""); + setCurrentUserAvatarUrl(getAvatarUrl(profile)); message.success(t("common.success")); window.location.reload(); @@ -321,7 +349,7 @@ export default function AppLayout() { - {availableTenants.length > 0 && ( + {platformRuntime?.tenantMode !== "single" && availableTenants.length > 0 && ( ({ @@ -339,7 +367,7 @@ export default function AppLayout() { } style={{ backgroundColor: "var(--app-primary-color)" }} /> - {currentUserDisplayName} + {currentUserLabel || t("layout.admin")} @@ -357,33 +385,22 @@ export default function AppLayout() { flexShrink: 0 }} > - - logo - {(!collapsed || isTop) && ( - - {platformConfig?.projectName || "UnisBase"} - - )} - + logo + {(!collapsed || isTop) && ( + + {platformConfig?.projectName || "UnisBase"} + + )} ); @@ -411,7 +428,7 @@ export default function AppLayout() { )} - +
{platformConfig?.icpInfo ? {platformConfig.icpInfo} : null} - {platformConfig?.icpInfo && platformConfig?.copyrightInfo ? : null} + {platformConfig?.icpInfo && platformConfig?.copyrightInfo ? : null} {platformConfig?.copyrightInfo ? {platformConfig.copyrightInfo} : null}
@@ -506,4 +526,3 @@ export default function AppLayout() { ); } - diff --git a/frontend/src/locales/en-US.json b/frontend/src/locales/en-US.json index 89ac721..05e2721 100644 --- a/frontend/src/locales/en-US.json +++ b/frontend/src/locales/en-US.json @@ -317,6 +317,9 @@ "cleanCurrent": "Clear {{type}}", "cleanConfirmTitle": "Clear {{type}}?", "cleanConfirmDescription": "This will remove all records of the current log type and ignores the active filters.", + "cleanConfirmDescriptionWithTenant": "This will remove all records of the current log type under {{tenant}}.", + "allTenants": "All tenants", + "tenantId": "Tenant {{id}}", "cleanSuccess": "{{type}} cleared" }, "devicesExt": { diff --git a/frontend/src/locales/zh-CN.json b/frontend/src/locales/zh-CN.json index a390822..45be8ae 100644 --- a/frontend/src/locales/zh-CN.json +++ b/frontend/src/locales/zh-CN.json @@ -317,6 +317,9 @@ "cleanCurrent": "清空{{type}}", "cleanConfirmTitle": "确认清空{{type}}?", "cleanConfirmDescription": "将清空当前日志类型的全部记录,不受当前筛选条件影响。", + "cleanConfirmDescriptionWithTenant": "将清空{{tenant}}下当前日志类型的全部记录。", + "allTenants": "全部租户", + "tenantId": "租户 {{id}}", "cleanSuccess": "{{type}}已清空" }, "devicesExt": { diff --git a/frontend/src/pages/auth/login/index.tsx b/frontend/src/pages/auth/login/index.tsx index f96996a..7290f19 100644 --- a/frontend/src/pages/auth/login/index.tsx +++ b/frontend/src/pages/auth/login/index.tsx @@ -82,6 +82,13 @@ export default function Login() { const payload = JSON.parse(atob(data.accessToken.split(".")[1])); localStorage.setItem("activeTenantId", String(payload.tenantId)); } + if (data.tenantMode) { + sessionStorage.setItem("platformRuntime", JSON.stringify({ + tenantMode: data.tenantMode, + multiTenantEnabled: data.multiTenantEnabled !== false, + currentTenantId: data.currentTenantId + })); + } try { const profile = await getCurrentUser(); @@ -106,26 +113,26 @@ export default function Login() { const loginStyle = platformConfig?.loginBgUrl ? { - backgroundImage: `url(${platformConfig.loginBgUrl})`, - backgroundSize: "cover", - backgroundPosition: "center", - position: "relative" as const - } + backgroundImage: `url(${platformConfig.loginBgUrl})`, + backgroundSize: "cover", + backgroundPosition: "center", + position: "relative" as const + } : {}; const leftStyle = platformConfig?.loginBgUrl ? { - ...loginStyle, - background: "rgba(255, 255, 255, 0.2)", - backdropFilter: "blur(10px)" - } + ...loginStyle, + background: "rgba(255, 255, 255, 0.2)", + backdropFilter: "blur(10px)" + } : {}; const rightStyle = platformConfig?.loginBgUrl ? { - background: "rgba(255, 255, 255, 0.85)", - backdropFilter: "blur(20px)" - } + background: "rgba(255, 255, 255, 0.85)", + backdropFilter: "blur(20px)" + } : {}; return ( diff --git a/frontend/src/pages/organization/tenants/index.tsx b/frontend/src/pages/organization/tenants/index.tsx index 0ef627c..4d7e841 100644 --- a/frontend/src/pages/organization/tenants/index.tsx +++ b/frontend/src/pages/organization/tenants/index.tsx @@ -3,13 +3,13 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { DeleteOutlined, EditOutlined, PhoneOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, ShopOutlined, UserOutlined } from "@ant-design/icons"; import dayjs from "dayjs"; -import { createTenant, deleteTenant, listTenants, updateTenant } from "@/api"; +import { createTenant, deleteTenant, getPlatformRuntime, listTenants, updateTenant } from "@/api"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; import PageHeader from "@/components/shared/PageHeader"; import { getStandardPagination } from "@/utils/pagination"; import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName"; -import type { SysTenant } from "@/types"; +import type { PlatformRuntime, SysTenant } from "@/types"; const { Title, Text, Paragraph } = Typography; @@ -24,6 +24,7 @@ export default function Tenants() { const [queryParams, setQueryParams] = useState({ current: 1, size: 12, name: "", code: "" }); const [drawerOpen, setDrawerOpen] = useState(false); const [editing, setEditing] = useState(null); + const [runtime, setRuntime] = useState(null); const [adminAccountTouched, setAdminAccountTouched] = useState(false); const [form] = Form.useForm(); const [searchForm] = Form.useForm(); @@ -44,6 +45,10 @@ export default function Tenants() { loadData(); }, [loadData, queryParams.current, queryParams.size]); + useEffect(() => { + getPlatformRuntime().then(setRuntime).catch(() => setRuntime(null)); + }, []); + const handleSearch = (values: any) => { setQueryParams((prev) => ({ ...prev, ...values, current: 1 })); }; @@ -123,7 +128,7 @@ export default function Tenants() { styles={{ body: { flex: 1, display: 'flex', flexDirection: 'column' } }} actions={[ can("sys_tenant:update") && openEdit(item)} style={{ color: "#1677ff" }} />, - can("sys_tenant:delete") && ( + runtime?.tenantMode !== "single" && can("sys_tenant:delete") && ( - {can("sys_tenant:create") && } + {runtime?.tenantMode !== "single" && can("sys_tenant:create") && } @@ -291,7 +296,7 @@ export default function Tenants() { - ({ label: item.itemLabel, value: Number(item.itemValue) }))} /> diff --git a/frontend/src/pages/system/logs/index.tsx b/frontend/src/pages/system/logs/index.tsx index eb44327..f3a74ba 100644 --- a/frontend/src/pages/system/logs/index.tsx +++ b/frontend/src/pages/system/logs/index.tsx @@ -1,8 +1,8 @@ import { Button, Card, DatePicker, Descriptions, Input, Modal, Popconfirm, Select, Space, Tabs, Tag, Typography, message } from "antd"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { DeleteOutlined, EyeOutlined, InfoCircleOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons"; -import { cleanLogs, fetchLogModules, fetchLogs } from "@/api"; +import { DeleteOutlined, EyeOutlined, InfoCircleOutlined, ReloadOutlined, SearchOutlined, ShopOutlined, UserOutlined } from "@ant-design/icons"; +import { cleanLogs, fetchLogModules, fetchLogs, listTenants } from "@/api"; import { useDict } from "@/hooks/useDict"; import PageHeader from "@/components/shared/PageHeader"; import ListTable from "@/components/shared/ListTable/ListTable"; @@ -19,6 +19,7 @@ export default function Logs() { const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [moduleOptions, setModuleOptions] = useState([]); + const [tenants, setTenants] = useState([]); const [detailModalVisible, setDetailModalVisible] = useState(false); const [selectedLog, setSelectedLog] = useState(null); const [cleaning, setCleaning] = useState(false); @@ -31,6 +32,7 @@ export default function Logs() { startDate: "", endDate: "", operation: "", + tenantId: undefined as number | undefined, sortField: "createdAt", sortOrder: "descend" as any }); @@ -48,6 +50,10 @@ export default function Logs() { }, []); const isPlatformAdmin = Boolean(userProfile?.isPlatformAdmin); + const activeTenantName = useMemo(() => { + if (!isPlatformAdmin || !params.tenantId) return t("logsExt.allTenants"); + return tenants.find((tenant) => tenant.id === params.tenantId)?.tenantName || t("logsExt.tenantId", { id: params.tenantId }); + }, [isPlatformAdmin, params.tenantId, tenants, t]); const activeLogTypeLabel = useMemo(() => { const dictLabel = logTypeDict.find((item) => item.itemValue === activeTab)?.itemLabel; if (dictLabel) return dictLabel; @@ -67,15 +73,25 @@ export default function Logs() { useEffect(() => { loadData(); - }, [activeTab, params.current, params.size, params.sortField, params.sortOrder]); + }, [activeTab, params.current, params.size, params.sortField, params.sortOrder, params.tenantId]); + + useEffect(() => { + if (!isPlatformAdmin) { + setTenants([]); + return; + } + listTenants({ current: 1, size: 1000 }) + .then((result) => setTenants(result?.records || [])) + .catch(() => setTenants([])); + }, [isPlatformAdmin]); useEffect(() => { if (activeTab !== "OPERATION") { setModuleOptions([]); return; } - fetchLogModules().then((items) => setModuleOptions(items || [])).catch(() => setModuleOptions([])); - }, [activeTab]); + fetchLogModules({ tenantId: isPlatformAdmin ? params.tenantId : undefined }).then((items) => setModuleOptions(items || [])).catch(() => setModuleOptions([])); + }, [activeTab, isPlatformAdmin, params.tenantId]); const handleTableChange = (pagination: any, _filters: any, sorter: any) => { setParams({ @@ -103,6 +119,7 @@ export default function Logs() { startDate: "", endDate: "", operation: "", + tenantId: undefined, sortField: "createdAt", sortOrder: "descend" as any }; @@ -113,7 +130,7 @@ export default function Logs() { const handleClean = async () => { setCleaning(true); try { - await cleanLogs(activeTab); + await cleanLogs(activeTab, isPlatformAdmin ? params.tenantId : undefined); message.success(t("logsExt.cleanSuccess", { type: activeLogTypeLabel })); const nextParams = { ...params, current: 1 }; setParams(nextParams); @@ -209,16 +226,16 @@ export default function Logs() { width: 180, ellipsis: true, render: (method: string) => ( - {method} @@ -241,6 +258,17 @@ export default function Logs() { prefix={