feat: 添加平台运行时信息和多租户支持
- 在 `auth.ts` 和 `platform.ts` 中添加获取平台运行时信息的 API - 在 `AppLayout.tsx` 和 `login/index.tsx` 中处理平台运行时信息,并更新用户界面 - 在 `DeviceInfoMapper.java` 中添加忽略租户的更新方法 - 更新日志清理和模块获取的 API 以支持多租户 - 在 `tenants/index.tsx` 和 `logs/index.tsx` 中添加多租户相关的逻辑和 UI 改进dev_na
parent
c0e973e5a9
commit
eba6bf105e
|
|
@ -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> {
|
|||
""")
|
||||
DeviceInfoEntity selectByDeviceCodeIgnoreTenant(@Param("deviceCode") String deviceCode);
|
||||
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Update("""
|
||||
<script>
|
||||
UPDATE biz_device_info
|
||||
SET tenant_id = #{tenantId},
|
||||
user_id = #{userId},
|
||||
terminal_type = #{terminalType},
|
||||
terminal_version = #{terminalVersion},
|
||||
last_online_at = #{lastOnlineAt},
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE device_id = #{deviceId}
|
||||
AND is_deleted = 0
|
||||
</script>
|
||||
""")
|
||||
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 *
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ export interface TokenResponse {
|
|||
accessExpiresInMinutes: number;
|
||||
refreshExpiresInDays: number;
|
||||
availableTenants?: TenantInfo[];
|
||||
tenantMode?: "single" | "multi";
|
||||
multiTenantEnabled?: boolean;
|
||||
currentTenantId?: number;
|
||||
}
|
||||
|
||||
export interface LoginPayload {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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<SysPermission[]>([]);
|
||||
const [availableTenants, setAvailableTenants] = useState<TenantInfo[]>([]);
|
||||
const [currentTenantId, setCurrentTenantId] = useState<number | null>(null);
|
||||
const [platformRuntime, setPlatformRuntime] = useState<PlatformRuntime | null>(() => {
|
||||
const runtimeStr = sessionStorage.getItem("platformRuntime");
|
||||
return runtimeStr ? JSON.parse(runtimeStr) : null;
|
||||
});
|
||||
const [currentUserLabel, setCurrentUserLabel] = useState<string>(() => {
|
||||
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<string>(() => {
|
||||
try {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
return profileStr ? getAvatarUrl(JSON.parse(profileStr) as CachedUserProfile) : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||
const [platformConfig, setPlatformConfig] = useState<SysPlatformConfig | null>(() => {
|
||||
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<string>(() => {
|
||||
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() {
|
|||
<Dropdown menu={{ items: langMenuItems }} placement="bottomRight">
|
||||
<GlobalOutlined style={{ fontSize: "18px", color: "var(--app-text-main)", cursor: "pointer" }} />
|
||||
</Dropdown>
|
||||
{availableTenants.length > 0 && (
|
||||
{platformRuntime?.tenantMode !== "single" && availableTenants.length > 0 && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: availableTenants.map((tenant) => ({
|
||||
|
|
@ -339,7 +367,7 @@ export default function AppLayout() {
|
|||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||
<Space style={{ cursor: "pointer", color: "var(--app-text-main)" }}>
|
||||
<Avatar size="small" src={currentUserAvatarUrl || undefined} icon={currentUserAvatarUrl ? undefined : <UserOutlined />} style={{ backgroundColor: "var(--app-primary-color)" }} />
|
||||
<span style={{ fontWeight: 500 }}>{currentUserDisplayName}</span>
|
||||
<span style={{ fontWeight: 500 }}>{currentUserLabel || t("layout.admin")}</span>
|
||||
</Space>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
|
|
@ -357,33 +385,22 @@ export default function AppLayout() {
|
|||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to="/"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
textDecoration: "none",
|
||||
minWidth: 0
|
||||
}}
|
||||
>
|
||||
<img src={platformConfig?.logoUrl || "/logo.svg"} alt="logo" style={{ width: 32, height: 32, objectFit: "contain" }} />
|
||||
{(!collapsed || isTop) && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
fontWeight: 700,
|
||||
color: "var(--app-primary-color)",
|
||||
letterSpacing: "0.5px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{platformConfig?.projectName || "UnisBase"}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<img src={platformConfig?.logoUrl || "/logo.svg"} alt="logo" style={{ width: 32, height: 32, objectFit: "contain" }} />
|
||||
{(!collapsed || isTop) && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
fontWeight: 700,
|
||||
color: "var(--app-primary-color)",
|
||||
letterSpacing: "0.5px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
}}
|
||||
>
|
||||
{platformConfig?.projectName || "UnisBase"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -411,7 +428,7 @@ export default function AppLayout() {
|
|||
</Sider>
|
||||
)}
|
||||
|
||||
<Layout style={{ flex: 1, minWidth: 0, minHeight: 0 }}>
|
||||
<Layout style={{ flex: 1, minWidth: 0 }}>
|
||||
<Header
|
||||
style={{
|
||||
background: "var(--app-bg-card)",
|
||||
|
|
@ -461,10 +478,8 @@ export default function AppLayout() {
|
|||
|
||||
<Content
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
margin: "24px 24px 12px",
|
||||
padding: location.pathname === "/" || location.pathname === "/home" ? 0 : 24,
|
||||
padding: 24,
|
||||
background: "var(--app-bg-card)",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "var(--app-shadow)",
|
||||
|
|
@ -481,24 +496,29 @@ export default function AppLayout() {
|
|||
<Footer
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
padding: "16px 24px",
|
||||
background: "var(--app-bg-card)",
|
||||
borderTop: "1px solid var(--app-border-color)"
|
||||
padding: "0 24px 16px",
|
||||
background: "transparent"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: 44,
|
||||
borderRadius: 8,
|
||||
border: "1px solid var(--app-border-color)",
|
||||
background: "var(--app-bg-card)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 12,
|
||||
color: "var(--app-text-secondary)",
|
||||
gap: 16,
|
||||
padding: "10px 16px",
|
||||
color: "var(--app-text-main)",
|
||||
fontSize: 13,
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
backdropFilter: "blur(10px)"
|
||||
}}
|
||||
>
|
||||
{platformConfig?.icpInfo ? <span>{platformConfig.icpInfo}</span> : null}
|
||||
{platformConfig?.icpInfo && platformConfig?.copyrightInfo ? <span aria-hidden="true" style={{ color: "var(--app-border-color)" }}>|</span> : null}
|
||||
{platformConfig?.icpInfo && platformConfig?.copyrightInfo ? <span aria-hidden="true" style={{ color: "#d0d5dd" }}>|</span> : null}
|
||||
{platformConfig?.copyrightInfo ? <span>{platformConfig.copyrightInfo}</span> : null}
|
||||
</div>
|
||||
</Footer>
|
||||
|
|
@ -506,4 +526,3 @@ export default function AppLayout() {
|
|||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -317,6 +317,9 @@
|
|||
"cleanCurrent": "清空{{type}}",
|
||||
"cleanConfirmTitle": "确认清空{{type}}?",
|
||||
"cleanConfirmDescription": "将清空当前日志类型的全部记录,不受当前筛选条件影响。",
|
||||
"cleanConfirmDescriptionWithTenant": "将清空{{tenant}}下当前日志类型的全部记录。",
|
||||
"allTenants": "全部租户",
|
||||
"tenantId": "租户 {{id}}",
|
||||
"cleanSuccess": "{{type}}已清空"
|
||||
},
|
||||
"devicesExt": {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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<SysTenant | null>(null);
|
||||
const [runtime, setRuntime] = useState<PlatformRuntime | null>(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") && <Tooltip title={t("common.edit")} key="edit-tip"><EditOutlined key="edit" onClick={() => openEdit(item)} style={{ color: "#1677ff" }} /></Tooltip>,
|
||||
can("sys_tenant:delete") && (
|
||||
runtime?.tenantMode !== "single" && can("sys_tenant:delete") && (
|
||||
<Popconfirm
|
||||
key="delete-pop"
|
||||
title={t("tenantsExt.deleteConfirm", { name: item.tenantName })}
|
||||
|
|
@ -200,7 +205,7 @@ export default function Tenants() {
|
|||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{can("sys_tenant:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{t("common.create")}</Button>}
|
||||
{runtime?.tenantMode !== "single" && can("sys_tenant:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{t("common.create")}</Button>}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
@ -291,7 +296,7 @@ export default function Tenants() {
|
|||
</Col>
|
||||
</Row>
|
||||
<Form.Item label={t("common.status")} name="status" initialValue={1}>
|
||||
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
|
||||
<Select disabled={runtime?.tenantMode === "single"} options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("common.remark")} name="remark">
|
||||
<Input.TextArea rows={3} placeholder={t("tenantsExt.remarkPlaceholder")} />
|
||||
|
|
|
|||
|
|
@ -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<SysLog[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [moduleOptions, setModuleOptions] = useState<string[]>([]);
|
||||
const [tenants, setTenants] = useState<SysTenant[]>([]);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [selectedLog, setSelectedLog] = useState<SysLog | null>(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) => (
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
maxWidth: "100%",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
verticalAlign: "middle"
|
||||
}}
|
||||
}}
|
||||
title={method}
|
||||
>
|
||||
{method}
|
||||
|
|
@ -241,6 +258,17 @@ export default function Logs() {
|
|||
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
|
||||
allowClear
|
||||
/>
|
||||
{isPlatformAdmin && (
|
||||
<Select
|
||||
placeholder={t("users.tenantFilter")}
|
||||
style={{ width: 200 }}
|
||||
value={params.tenantId}
|
||||
onChange={(value) => setParams({ ...params, tenantId: value, moduleName: "", current: 1 })}
|
||||
options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))}
|
||||
suffixIcon={<ShopOutlined aria-hidden="true" />}
|
||||
allowClear
|
||||
/>
|
||||
)}
|
||||
{activeTab === "OPERATION" && (
|
||||
<Select
|
||||
placeholder={t("logsExt.filterModule")}
|
||||
|
|
@ -284,7 +312,7 @@ export default function Logs() {
|
|||
tabBarExtraContent={(
|
||||
<Popconfirm
|
||||
title={t("logsExt.cleanConfirmTitle", { type: activeLogTypeLabel })}
|
||||
description={t("logsExt.cleanConfirmDescription")}
|
||||
description={isPlatformAdmin ? t("logsExt.cleanConfirmDescriptionWithTenant", { tenant: activeTenantName }) : t("logsExt.cleanConfirmDescription")}
|
||||
okText={t("common.confirm")}
|
||||
cancelText={t("common.cancel")}
|
||||
okButtonProps={{ danger: true, loading: cleaning }}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface SysUser extends BaseEntity {
|
|||
orgId?: number;
|
||||
isPlatformAdmin?: boolean;
|
||||
memberships?: any[];
|
||||
roleIds?: number[];
|
||||
roles?: SysRole[];
|
||||
}
|
||||
|
||||
|
|
@ -84,9 +85,7 @@ export interface PermissionNode extends SysPermission {
|
|||
|
||||
export interface DeviceInfo extends BaseEntity {
|
||||
deviceId: number;
|
||||
userId?: number;
|
||||
username?: string;
|
||||
displayName?: string;
|
||||
userId: number;
|
||||
deviceCode: string;
|
||||
deviceName?: string;
|
||||
terminalType?: string;
|
||||
|
|
@ -115,6 +114,7 @@ export interface SysTenant extends BaseEntity {
|
|||
id: number;
|
||||
tenantCode: string;
|
||||
tenantName: string;
|
||||
defaultAdminUsername?: string;
|
||||
expireTime?: string;
|
||||
contactName?: string;
|
||||
contactPhone?: string;
|
||||
|
|
@ -173,6 +173,13 @@ export interface SysPlatformConfig {
|
|||
systemDescription?: string;
|
||||
}
|
||||
|
||||
export interface PlatformRuntime {
|
||||
tenantMode: "single" | "multi";
|
||||
multiTenantEnabled: boolean;
|
||||
defaultTenantId?: number;
|
||||
currentTenantId?: number;
|
||||
}
|
||||
|
||||
export interface SysParamQuery {
|
||||
paramKey?: string;
|
||||
paramType?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue