性能优化
parent
9a6d9dca8a
commit
19fef2ce3a
|
|
@ -108,10 +108,8 @@ function App() {
|
|||
<Route path="/monitor/cache" element={<CacheMonitorPage />} />
|
||||
<Route path="/monitor/job" element={<JobMonitorPage />} />
|
||||
<Route path="/monitor/logininfor" element={<LoginLogPage />} />
|
||||
<Route path="/system/logininfor" element={<LoginLogPage />} />
|
||||
<Route path="/monitor/online" element={<OnlineUserPage />} />
|
||||
<Route path="/monitor/operlog" element={<OperationLogPage />} />
|
||||
<Route path="/system/operlog" element={<OperationLogPage />} />
|
||||
<Route path="/monitor/server" element={<ServerMonitorPage />} />
|
||||
<Route path="/monitor/cacheList" element={<CacheListPage />} />
|
||||
<Route path="/system/user" element={<UserPage />} />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Avatar, Button, Dropdown, Space } from 'antd';
|
||||
import { Avatar, Button, Dropdown, Space, Tabs } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { DownOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { removeToken } from '../../utils/auth';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import './navbar.css';
|
||||
|
|
@ -11,9 +11,189 @@ interface AppNavbarProps {
|
|||
collapsed: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
interface OpenPageTab {
|
||||
key: string;
|
||||
path: string;
|
||||
pathname: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const OPEN_PAGE_TABS_STORAGE_KEY = 'pms_open_page_tabs_v1';
|
||||
const MAX_OPEN_PAGE_TABS = 12;
|
||||
|
||||
const TAB_TITLE_MAP: Record<string, string> = {
|
||||
'/index': '工作日志',
|
||||
'/dashboard/project-execution': '项目执行表',
|
||||
'/projectBank/projectProgress': '项目执行表',
|
||||
'/projectBank/projectUser': '项目人员表',
|
||||
'/projectBank/userProject': '人员项目表',
|
||||
'/projectBank/userScore': '人员绩效表',
|
||||
'/projectBank/userScoreDetail': '绩效详情',
|
||||
'/user/profile': '个人中心',
|
||||
'/profile': '个人中心',
|
||||
'/monitor/cache': '缓存监控',
|
||||
'/monitor/job': '定时任务',
|
||||
'/monitor/logininfor': '登录日志',
|
||||
'/log/logininfor': '登录日志',
|
||||
'/system/logininfor': '登录日志',
|
||||
'/system/log/logininfor': '登录日志',
|
||||
'/monitor/online': '在线用户',
|
||||
'/monitor/operlog': '操作日志',
|
||||
'/log/operlog': '操作日志',
|
||||
'/system/operlog': '操作日志',
|
||||
'/system/log/operlog': '操作日志',
|
||||
'/monitor/server': '服务监控',
|
||||
'/monitor/cacheList': '缓存列表',
|
||||
'/system/user': '用户管理',
|
||||
'/system/role': '角色管理',
|
||||
'/system/menu': '菜单管理',
|
||||
'/system/dept': '部门管理',
|
||||
'/system/dict': '字典管理',
|
||||
'/system/config': '参数设置',
|
||||
'/project/list': '项目列表',
|
||||
'/project/detail': '项目详情',
|
||||
'/project/demandManage': '需求管理',
|
||||
'/demandManage': '需求管理',
|
||||
'/workAppraisal/manager': '考核任务',
|
||||
'/workAppraisal/normalWorker': '考核评分',
|
||||
'/workAppraisal/managerUser': '考核人员',
|
||||
'/workAppraisal/detail': '绩效详情',
|
||||
'/workAppraisal/taskSet': '任务设置',
|
||||
'/workAppraisal/taskModule': '考核看板',
|
||||
'/workAppraisal/dashboard': '考核看板',
|
||||
'/workAppraisal/moduleDetail': '模块详情',
|
||||
'/workAppraisal/myPerformance': '人员绩效表',
|
||||
};
|
||||
|
||||
const CANONICAL_TAB_PATH_MAP: Record<string, string> = {
|
||||
'/': '/index',
|
||||
'/profile': '/user/profile',
|
||||
'/dashboard/project-execution': '/projectBank/projectProgress',
|
||||
'/log/logininfor': '/monitor/logininfor',
|
||||
'/system/logininfor': '/monitor/logininfor',
|
||||
'/system/log/logininfor': '/monitor/logininfor',
|
||||
'/log/operlog': '/monitor/operlog',
|
||||
'/system/operlog': '/monitor/operlog',
|
||||
'/system/log/operlog': '/monitor/operlog',
|
||||
'/workAppraisal/dashboard': '/workAppraisal/taskModule',
|
||||
'/demandManage': '/project/demandManage',
|
||||
};
|
||||
|
||||
const normalizePathname = (pathname: string) => {
|
||||
const normalizedPathname = pathname === '/' ? '/index' : pathname;
|
||||
return CANONICAL_TAB_PATH_MAP[normalizedPathname] ?? normalizedPathname;
|
||||
};
|
||||
|
||||
const normalizeTabSearch = (pathname: string, search: string) => {
|
||||
if (!search) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const normalizedPathname = normalizePathname(pathname);
|
||||
const params = new URLSearchParams(search);
|
||||
|
||||
if (normalizedPathname === '/user/profile') {
|
||||
params.delete('tab');
|
||||
}
|
||||
|
||||
const normalizedParams = params.toString();
|
||||
return normalizedParams ? `?${normalizedParams}` : '';
|
||||
};
|
||||
|
||||
const getTabTitle = (pathname: string, search: string) => {
|
||||
const normalizedPathname = normalizePathname(pathname);
|
||||
const params = new URLSearchParams(search);
|
||||
const routeTitle = TAB_TITLE_MAP[normalizedPathname] ?? '页面';
|
||||
|
||||
if (normalizedPathname === '/projectBank/projectUser') {
|
||||
const projectName = params.get('projectName');
|
||||
return projectName ? `${routeTitle} · ${projectName}` : routeTitle;
|
||||
}
|
||||
|
||||
if (normalizedPathname === '/index') {
|
||||
const nickName = params.get('nickName');
|
||||
return nickName ? `${routeTitle} · ${nickName}` : routeTitle;
|
||||
}
|
||||
|
||||
if (normalizedPathname === '/project/demandManage' || normalizedPathname === '/demandManage') {
|
||||
const projectName = params.get('projectName');
|
||||
return projectName ? `${routeTitle} · ${projectName}` : routeTitle;
|
||||
}
|
||||
|
||||
return routeTitle;
|
||||
};
|
||||
|
||||
const createOpenPageTab = (pathname: string, search: string): OpenPageTab => {
|
||||
const normalizedPathname = normalizePathname(pathname);
|
||||
const normalizedSearch = normalizeTabSearch(normalizedPathname, search || '');
|
||||
return {
|
||||
key: `${normalizedPathname}${normalizedSearch}`,
|
||||
path: `${normalizedPathname}${search || ''}`,
|
||||
pathname: normalizedPathname,
|
||||
title: getTabTitle(normalizedPathname, search || ''),
|
||||
};
|
||||
};
|
||||
|
||||
const getStoredOpenPageTabs = (canAccessPath: (path: string) => boolean): OpenPageTab[] => {
|
||||
if (typeof window === 'undefined') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(OPEN_PAGE_TABS_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seenKeys = new Set<string>();
|
||||
|
||||
return parsed.reduce<OpenPageTab[]>((tabs, item) => {
|
||||
if (
|
||||
!item
|
||||
|| typeof item !== 'object'
|
||||
|| typeof item.path !== 'string'
|
||||
|| typeof item.pathname !== 'string'
|
||||
) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
const storedPath = String(item.path);
|
||||
const [storedPathname, storedSearch = ''] = storedPath.split('?');
|
||||
const normalizedTab = createOpenPageTab(
|
||||
storedPathname,
|
||||
storedSearch ? `?${storedSearch}` : '',
|
||||
);
|
||||
|
||||
if (!canAccessPath(normalizedTab.pathname) || seenKeys.has(normalizedTab.key)) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
seenKeys.add(normalizedTab.key);
|
||||
tabs.push(normalizedTab);
|
||||
return tabs;
|
||||
}, []);
|
||||
} catch (error) {
|
||||
console.warn('Failed to restore open page tabs:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const isPinnedTab = (tab: OpenPageTab) => tab.pathname === '/index';
|
||||
|
||||
const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
|
||||
const navigate = useNavigate();
|
||||
const { userName } = usePermission();
|
||||
const location = useLocation();
|
||||
const { userName, canAccessPath } = usePermission();
|
||||
const activeTab = React.useMemo(
|
||||
() => createOpenPageTab(location.pathname, location.search),
|
||||
[location.pathname, location.search],
|
||||
);
|
||||
const [openTabs, setOpenTabs] = React.useState<OpenPageTab[]>(() => getStoredOpenPageTabs(canAccessPath));
|
||||
|
||||
const handleLogout = () => {
|
||||
removeToken();
|
||||
|
|
@ -46,6 +226,78 @@ const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
|
|||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!canAccessPath(activeTab.pathname)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenTabs((prev) => {
|
||||
const filteredPrev = prev.filter((item) => canAccessPath(item.pathname));
|
||||
const existingIndex = filteredPrev.findIndex((item) => item.key === activeTab.key);
|
||||
let nextTabs = existingIndex >= 0
|
||||
? filteredPrev.map((item, index) => (index === existingIndex ? activeTab : item))
|
||||
: [...filteredPrev, activeTab];
|
||||
|
||||
if (nextTabs.length > MAX_OPEN_PAGE_TABS) {
|
||||
const removableIndex = nextTabs.findIndex((item) => item.key !== activeTab.key && !isPinnedTab(item));
|
||||
if (removableIndex >= 0) {
|
||||
nextTabs = nextTabs.filter((_item, index) => index !== removableIndex);
|
||||
} else {
|
||||
nextTabs = nextTabs.slice(-MAX_OPEN_PAGE_TABS);
|
||||
}
|
||||
}
|
||||
|
||||
return nextTabs;
|
||||
});
|
||||
}, [activeTab, canAccessPath]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.sessionStorage.setItem(OPEN_PAGE_TABS_STORAGE_KEY, JSON.stringify(openTabs));
|
||||
}, [openTabs]);
|
||||
|
||||
const handleTabChange = (key: string) => {
|
||||
const targetTab = openTabs.find((item) => item.key === key);
|
||||
if (!targetTab) {
|
||||
return;
|
||||
}
|
||||
navigate(targetTab.path);
|
||||
};
|
||||
|
||||
const handleTabRemove = (targetKey: string) => {
|
||||
const targetTab = openTabs.find((item) => item.key === targetKey);
|
||||
if (!targetTab || isPinnedTab(targetTab) || openTabs.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetIndex = openTabs.findIndex((item) => item.key === targetKey);
|
||||
const nextTabs = openTabs.filter((item) => item.key !== targetKey);
|
||||
setOpenTabs(nextTabs);
|
||||
|
||||
if (targetKey === activeTab.key) {
|
||||
const fallbackTab = nextTabs[targetIndex] ?? nextTabs[targetIndex - 1] ?? nextTabs[0];
|
||||
if (fallbackTab) {
|
||||
navigate(fallbackTab.path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tabItems = React.useMemo(
|
||||
() =>
|
||||
openTabs.map((item) => ({
|
||||
key: item.key,
|
||||
closable: !isPinnedTab(item) && openTabs.length > 1,
|
||||
label: (
|
||||
<span className="app-navbar-tab-label" title={item.title}>
|
||||
{item.title}
|
||||
</span>
|
||||
),
|
||||
})),
|
||||
[openTabs],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="app-navbar">
|
||||
<Button
|
||||
|
|
@ -54,6 +306,21 @@ const AppNavbar: React.FC<AppNavbarProps> = ({ collapsed, onToggle }) => {
|
|||
onClick={onToggle}
|
||||
className="app-navbar-toggle"
|
||||
/>
|
||||
<div className="app-navbar-tabs">
|
||||
<Tabs
|
||||
hideAdd
|
||||
size="small"
|
||||
type="editable-card"
|
||||
activeKey={activeTab.key}
|
||||
items={tabItems}
|
||||
onChange={handleTabChange}
|
||||
onEdit={(targetKey, action) => {
|
||||
if (action === 'remove' && typeof targetKey === 'string') {
|
||||
handleTabRemove(targetKey);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="app-navbar-meta">
|
||||
<Dropdown menu={{ items: menuItems, onClick: handleMenuClick }} trigger={['click']}>
|
||||
<Button type="text" className="app-navbar-user">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
.app-navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 74px;
|
||||
padding: 10px 16px;
|
||||
|
|
@ -24,6 +23,67 @@
|
|||
border: 1px solid rgba(219, 228, 243, 0.9);
|
||||
}
|
||||
|
||||
.app-navbar-tabs {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-nav {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-nav::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-nav-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-tab {
|
||||
max-width: 260px;
|
||||
padding: 8px 12px !important;
|
||||
border: 1px solid rgba(219, 228, 243, 0.9) !important;
|
||||
border-radius: 14px !important;
|
||||
background: linear-gradient(180deg, rgba(247, 249, 255, 0.96), rgba(255, 255, 255, 0.96)) !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-tab-active {
|
||||
background: linear-gradient(135deg, rgba(99, 91, 255, 0.12), rgba(79, 70, 229, 0.08)) !important;
|
||||
box-shadow: 0 8px 18px rgba(79, 70, 229, 0.12);
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-tab-btn {
|
||||
color: #30415f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-tab-remove {
|
||||
color: #7d8cab;
|
||||
}
|
||||
|
||||
.app-navbar-tabs .ant-tabs-ink-bar,
|
||||
.app-navbar-tabs .ant-tabs-content-holder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-navbar-tab-label {
|
||||
display: inline-block;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-navbar-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -81,6 +141,10 @@
|
|||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.app-navbar-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-navbar-copy {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ interface PageBackButtonProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
const PageBackButton = ({ text = '返回', fallbackPath = '/', className }: PageBackButtonProps) => {
|
||||
const PageBackButton = ({ text: _text = '返回', fallbackPath = '/', className }: PageBackButtonProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleBack = () => {
|
||||
|
|
@ -21,7 +21,7 @@ const PageBackButton = ({ text = '返回', fallbackPath = '/', className }: Page
|
|||
|
||||
return (
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={handleBack} className={className}>
|
||||
{text}
|
||||
返回
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,15 +38,21 @@ type MenuItemWithChildren = MenuItem & { children?: MenuItem[] };
|
|||
const BACKEND_PATH_TO_APP_PATH: Record<string, string> = {
|
||||
'/project': '/project/list',
|
||||
'/projectBank/userScore': '/workAppraisal/myPerformance',
|
||||
'/logininfor': '/monitor/logininfor',
|
||||
'/operlog': '/monitor/operlog',
|
||||
'/log/logininfor': '/monitor/logininfor',
|
||||
'/log/operlog': '/monitor/operlog',
|
||||
'/system/logininfor': '/monitor/logininfor',
|
||||
'/system/operlog': '/monitor/operlog',
|
||||
'/system/log/logininfor': '/monitor/logininfor',
|
||||
'/system/log/operlog': '/monitor/operlog',
|
||||
};
|
||||
|
||||
const MENU_ROUTE_ALIASES: Record<string, string[]> = {
|
||||
'/projectBank/projectProgress': ['/dashboard/project-execution'],
|
||||
'/workAppraisal/myPerformance': ['/projectBank/userScore'],
|
||||
'/monitor/logininfor': ['/system/logininfor'],
|
||||
'/monitor/operlog': ['/system/operlog'],
|
||||
'/monitor/logininfor': ['/logininfor', '/log/logininfor', '/system/logininfor', '/system/log/logininfor'],
|
||||
'/monitor/operlog': ['/operlog', '/log/operlog', '/system/operlog', '/system/log/operlog'],
|
||||
};
|
||||
|
||||
const ICON_MAP: Record<string, React.ReactNode> = {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ interface PermissionContextValue {
|
|||
ready: boolean;
|
||||
isAdmin: boolean;
|
||||
userName: string;
|
||||
currentUser: {
|
||||
userId?: string | number;
|
||||
userName?: string;
|
||||
nickName?: string;
|
||||
};
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
routers: RouterNode[];
|
||||
|
|
@ -25,8 +30,14 @@ const ALWAYS_ALLOW_PATHS = new Set(['/', '/profile', '/user/profile']);
|
|||
const BACKEND_PATH_TO_APP_PATH: Record<string, string> = {
|
||||
'/project': '/project/list',
|
||||
'/projectBank/userScore': '/workAppraisal/myPerformance',
|
||||
'/logininfor': '/monitor/logininfor',
|
||||
'/operlog': '/monitor/operlog',
|
||||
'/log/logininfor': '/monitor/logininfor',
|
||||
'/log/operlog': '/monitor/operlog',
|
||||
'/system/logininfor': '/monitor/logininfor',
|
||||
'/system/operlog': '/monitor/operlog',
|
||||
'/system/log/logininfor': '/monitor/logininfor',
|
||||
'/system/log/operlog': '/monitor/operlog',
|
||||
};
|
||||
const ROUTE_ALIASES: Record<string, string[]> = {
|
||||
'/profile': ['/user/profile'],
|
||||
|
|
@ -39,8 +50,8 @@ const ROUTE_ALIASES: Record<string, string[]> = {
|
|||
'/workAppraisal/myPerformance': ['/projectBank/userScore'],
|
||||
'/projectBank/userScore': ['/workAppraisal/myPerformance'],
|
||||
'/projectBank/userScoreDetail': ['/workAppraisal/myPerformance'],
|
||||
'/monitor/logininfor': ['/system/logininfor'],
|
||||
'/monitor/operlog': ['/system/operlog'],
|
||||
'/monitor/logininfor': ['/logininfor', '/log/logininfor', '/system/logininfor', '/system/log/logininfor'],
|
||||
'/monitor/operlog': ['/operlog', '/log/operlog', '/system/operlog', '/system/log/operlog'],
|
||||
};
|
||||
const SUPER_PERMI = '*:*:*';
|
||||
const ADMIN_ROLE = 'admin';
|
||||
|
|
@ -198,6 +209,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [userName, setUserName] = useState('');
|
||||
const [currentUser, setCurrentUser] = useState<PermissionContextValue['currentUser']>({});
|
||||
const [roles, setRoles] = useState<string[]>([]);
|
||||
const [permissions, setPermissions] = useState<string[]>([]);
|
||||
const [routers, setRouters] = useState<RouterNode[]>([]);
|
||||
|
|
@ -207,6 +219,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
|
||||
const clearPermissionState = useCallback(() => {
|
||||
setUserName('');
|
||||
setCurrentUser({});
|
||||
setRoles([]);
|
||||
setPermissions([]);
|
||||
setRouters([]);
|
||||
|
|
@ -228,6 +241,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
setReady(false);
|
||||
|
||||
let nextUserName = '';
|
||||
let nextCurrentUser: PermissionContextValue['currentUser'] = {};
|
||||
let nextRoles: string[] = [];
|
||||
let nextPermissions: string[] = [];
|
||||
let nextRouters: RouterNode[] = [];
|
||||
|
|
@ -237,15 +251,29 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
let bootstrapFailed = false;
|
||||
|
||||
try {
|
||||
try {
|
||||
const info = await getInfo();
|
||||
const [infoResult, routersResult] = await Promise.allSettled([getInfo(), getRouters()]);
|
||||
|
||||
if (infoResult.status === 'fulfilled') {
|
||||
const info = infoResult.value;
|
||||
const infoUser = info.user as Record<string, unknown> | undefined;
|
||||
nextRoles = parseStringList(info.roles);
|
||||
nextPermissions = parseStringList(info.permissions);
|
||||
nextUserName = String((info.user as Record<string, unknown> | undefined)?.userName ?? '');
|
||||
} catch (error) {
|
||||
nextCurrentUser = {
|
||||
userId: infoUser?.userId as string | number | undefined,
|
||||
userName: infoUser?.userName as string | undefined,
|
||||
nickName: infoUser?.nickName as string | undefined,
|
||||
};
|
||||
nextUserName = String(nextCurrentUser.userName ?? '');
|
||||
} else {
|
||||
const error = infoResult.reason;
|
||||
console.error('Failed to load /getInfo, fallback to profile:', error);
|
||||
try {
|
||||
const profile = await getUserProfile();
|
||||
nextCurrentUser = {
|
||||
userId: profile.user?.userId,
|
||||
userName: profile.user?.userName,
|
||||
nickName: profile.user?.nickName,
|
||||
};
|
||||
nextUserName = String(profile.user?.userName ?? '');
|
||||
const roleGroup = String((profile as Record<string, unknown>).roleGroup ?? '');
|
||||
nextRoles = roleGroup
|
||||
|
|
@ -258,8 +286,8 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const routersRaw = await getRouters();
|
||||
if (routersResult.status === 'fulfilled') {
|
||||
const routersRaw = routersResult.value;
|
||||
const routes = extractRouteNodes(routersRaw);
|
||||
if (routes.length > 0) {
|
||||
nextRouters = routes;
|
||||
|
|
@ -269,7 +297,8 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
nextDefaultRoutePath = findFirstVisibleRoutePath(routes, '/') || '/index';
|
||||
nextRouteGuardEnabled = true;
|
||||
}
|
||||
} catch (routerError) {
|
||||
} else {
|
||||
const routerError = routersResult.reason;
|
||||
console.error('Failed to load router permission data:', routerError);
|
||||
bootstrapFailed = true;
|
||||
}
|
||||
|
|
@ -281,6 +310,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
notify.warning('权限信息加载超时,已使用基础访问模式');
|
||||
}
|
||||
setUserName(nextUserName);
|
||||
setCurrentUser(nextCurrentUser);
|
||||
setRoles(nextRoles);
|
||||
setPermissions(nextPermissions);
|
||||
setRouters(nextRouters);
|
||||
|
|
@ -371,6 +401,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
ready,
|
||||
isAdmin: isAdmin(),
|
||||
userName,
|
||||
currentUser,
|
||||
roles,
|
||||
permissions,
|
||||
routers,
|
||||
|
|
@ -380,7 +411,7 @@ export const PermissionProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
|||
hasPermi,
|
||||
canAccessPath,
|
||||
}),
|
||||
[loading, ready, isAdmin, userName, roles, permissions, routers, defaultRoutePath, refreshPermissions, hasRole, hasPermi, canAccessPath],
|
||||
[loading, ready, isAdmin, userName, currentUser, roles, permissions, routers, defaultRoutePath, refreshPermissions, hasRole, hasPermi, canAccessPath],
|
||||
);
|
||||
|
||||
return <PermissionContext.Provider value={value}>{children}</PermissionContext.Provider>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { CameraOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Avatar, Upload, message } from 'antd';
|
||||
import { Avatar, Upload } from 'antd';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { uploadUserAvatar } from '@/api/user';
|
||||
import { notify } from '@/utils/notify';
|
||||
|
||||
interface AvatarUploaderProps {
|
||||
avatarUrl?: string;
|
||||
|
|
@ -31,21 +32,21 @@ const AvatarUploader = ({ avatarUrl, displayName, onUploaded }: AvatarUploaderPr
|
|||
const response = await uploadUserAvatar(formData);
|
||||
const nextAvatar = String(response.imgUrl ?? '');
|
||||
onUploaded(nextAvatar);
|
||||
message.success('修改成功');
|
||||
notify.success('修改成功');
|
||||
onSuccess?.(response);
|
||||
} catch (error) {
|
||||
message.error('头像上传失败');
|
||||
notify.error('头像上传失败');
|
||||
onError?.(error as Error);
|
||||
}
|
||||
},
|
||||
beforeUpload: (file) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('请上传图片文件');
|
||||
notify.error('请上传图片文件');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||
if (!isLt5M) {
|
||||
message.error('头像大小不能超过 5MB');
|
||||
notify.error('头像大小不能超过 5MB');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { Button, Form, Input, Space, message } from 'antd';
|
||||
import { Button, Form, Input, Space } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { updateUserPwd } from '@/api/user';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import { notify } from '@/utils/notify';
|
||||
|
||||
interface ResetPasswordValues {
|
||||
oldPassword: string;
|
||||
|
|
@ -25,7 +26,7 @@ const ResetPassword = () => {
|
|||
const onFinish = async (values: ResetPasswordValues) => {
|
||||
try {
|
||||
await updateUserPwd(values.oldPassword, values.newPassword);
|
||||
message.success('修改成功');
|
||||
notify.success('修改成功');
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
console.error('Failed to update password:', error);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Button, Form, Input, Radio, Space, message } from 'antd';
|
||||
import { Button, Form, Input, Radio, Space } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { updateUserProfile } from '@/api/user';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import type { UpdateUserProfilePayload, UserProfileUser } from '@/types/api';
|
||||
import { notify } from '@/utils/notify';
|
||||
|
||||
interface UserInfoProps {
|
||||
user: UserProfileUser;
|
||||
|
|
@ -35,7 +36,7 @@ const UserInfo = ({ user, onUpdated }: UserInfoProps) => {
|
|||
const onFinish = async (values: UpdateUserProfilePayload) => {
|
||||
try {
|
||||
await updateUserProfile(values);
|
||||
message.success('修改成功');
|
||||
notify.success('修改成功');
|
||||
onUpdated(values);
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Card, Spin, Tabs, message } from 'antd';
|
||||
import { Card, Spin, Tabs } from 'antd';
|
||||
import type { TabsProps } from 'antd';
|
||||
import {
|
||||
ApartmentOutlined,
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { useSearchParams } from 'react-router-dom';
|
||||
import { getUserProfile } from '@/api/user';
|
||||
import type { UserProfileUser } from '@/types/api';
|
||||
import { notify } from '@/utils/notify';
|
||||
import AvatarUploader from './AvatarUploader';
|
||||
import ResetPassword from './ResetPassword';
|
||||
import UserInfo from './UserInfo';
|
||||
|
|
@ -50,7 +51,7 @@ const ProfilePage = () => {
|
|||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error('Failed to load user profile:', error);
|
||||
message.error('获取个人中心信息失败');
|
||||
notify.error('获取个人中心信息失败');
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button, DatePicker, Empty, Form, Input, message, Progress, Select, Space, Table, Tooltip } from 'antd';
|
||||
import { Button, DatePicker, Empty, Form, Input, Progress, Select, Space, Table, Tooltip } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import {
|
||||
CalendarOutlined,
|
||||
|
|
@ -18,6 +18,7 @@ import { listProject } from '@/api/project';
|
|||
import { listProjectExecution } from '@/api/projectExecution';
|
||||
import { getDicts } from '@/api/system/dict';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import { notify } from '@/utils/notify';
|
||||
import '@/styles/permission-link.css';
|
||||
import './project-execution.css';
|
||||
|
||||
|
|
@ -397,11 +398,11 @@ const ProjectExecutionPage = () => {
|
|||
|
||||
if (ENABLE_PROJECT_EXECUTION_API && fallbackUsed && !fallbackTipShownRef.current) {
|
||||
fallbackTipShownRef.current = true;
|
||||
message.info('项目执行表接口未就绪,已自动使用项目列表估算执行进度');
|
||||
notify.info('项目执行表接口未就绪,已自动使用项目列表估算执行进度');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch project execution list:', error);
|
||||
message.error('获取项目执行表失败');
|
||||
notify.error('获取项目执行表失败');
|
||||
setRows([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Col, Row, message, Table, Button, Input, Form, Space } from 'antd';
|
||||
import { Card, Col, Row, Table, Button, Input, Form, Space } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import { ReloadOutlined, DeleteOutlined, KeyOutlined, FileTextOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
|
|
@ -19,6 +19,7 @@ import type {
|
|||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
import { notify } from '@/utils/notify';
|
||||
import './cache-list.css';
|
||||
|
||||
interface CacheForm {
|
||||
|
|
@ -126,7 +127,7 @@ const CacheListPage: React.FC = () => {
|
|||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to fetch cache keys:', error);
|
||||
message.error('获取缓存键名列表失败');
|
||||
notify.error('获取缓存键名列表失败');
|
||||
} finally {
|
||||
setSubLoading(false);
|
||||
}
|
||||
|
|
@ -156,7 +157,7 @@ const CacheListPage: React.FC = () => {
|
|||
await getCacheKeys(targetCacheName);
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to fetch cache names:', error);
|
||||
message.error('获取缓存名称列表失败');
|
||||
notify.error('获取缓存名称列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -176,7 +177,7 @@ const CacheListPage: React.FC = () => {
|
|||
});
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to fetch cache value:', error);
|
||||
message.error('获取缓存内容失败');
|
||||
notify.error('获取缓存内容失败');
|
||||
}
|
||||
},
|
||||
[nowCacheName],
|
||||
|
|
@ -193,7 +194,7 @@ const CacheListPage: React.FC = () => {
|
|||
|
||||
const refreshCacheNames = async () => {
|
||||
await getCacheNames();
|
||||
message.success('刷新缓存列表成功');
|
||||
notify.success('刷新缓存列表成功');
|
||||
};
|
||||
|
||||
const handleClearCacheName = async (row: CacheNameRecord) => {
|
||||
|
|
@ -202,16 +203,16 @@ const CacheListPage: React.FC = () => {
|
|||
}
|
||||
try {
|
||||
await clearCacheName(row.cacheName);
|
||||
message.success(`清理缓存名称[${row.cacheName}]成功`);
|
||||
notify.success(`清理缓存名称[${row.cacheName}]成功`);
|
||||
await getCacheNames();
|
||||
} catch {
|
||||
message.error('清理缓存名称失败');
|
||||
notify.error('清理缓存名称失败');
|
||||
}
|
||||
};
|
||||
|
||||
const refreshCacheKeys = async () => {
|
||||
await getCacheKeys();
|
||||
message.success('刷新键名列表成功');
|
||||
notify.success('刷新键名列表成功');
|
||||
};
|
||||
|
||||
const handleClearCacheKey = async (fullCacheKey: string) => {
|
||||
|
|
@ -224,11 +225,11 @@ const CacheListPage: React.FC = () => {
|
|||
|
||||
try {
|
||||
await clearCacheKey(fullCacheKey);
|
||||
message.success(`清理缓存键名[${fullCacheKey}]成功`);
|
||||
notify.success(`清理缓存键名[${fullCacheKey}]成功`);
|
||||
await getCacheKeys(nowCacheName);
|
||||
setCacheForm({ cacheName: nowCacheName, cacheKey: '', cacheValue: '' });
|
||||
} catch {
|
||||
message.error('清理缓存键名失败');
|
||||
notify.error('清理缓存键名失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -238,10 +239,10 @@ const CacheListPage: React.FC = () => {
|
|||
}
|
||||
try {
|
||||
await clearCacheAll();
|
||||
message.success('清理全部缓存成功');
|
||||
notify.success('清理全部缓存成功');
|
||||
await getCacheNames();
|
||||
} catch {
|
||||
message.error('清理全部缓存失败');
|
||||
notify.error('清理全部缓存失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Row, Col, Card, Descriptions, Spin, message, Typography } from 'antd';
|
||||
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
||||
import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
|
||||
import { App, Row, Col, Card, Descriptions, Spin, Typography } from 'antd';
|
||||
import { echarts } from '@/utils/echarts';
|
||||
import { macarons } from '../../themes/macarons';
|
||||
import { getCache } from '../../api/monitor/cache';
|
||||
|
|
@ -9,6 +8,8 @@ import './cache-monitor.css';
|
|||
|
||||
const { Text } = Typography;
|
||||
|
||||
const ReactEChartsCore = lazy(() => import('echarts-for-react/lib/core'));
|
||||
|
||||
echarts.registerTheme('macarons', macarons);
|
||||
|
||||
const defaultCacheData: CacheMonitorResponse = {
|
||||
|
|
@ -25,6 +26,7 @@ const toDisplayText = (value: string | number | undefined): string => {
|
|||
};
|
||||
|
||||
const CacheMonitorPage = () => {
|
||||
const { message } = App.useApp();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [cacheData, setCacheData] = useState<CacheMonitorResponse>(defaultCacheData);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -52,7 +54,9 @@ const CacheMonitorPage = () => {
|
|||
return <div style={{ textAlign: 'center', marginTop: '50px' }}><Text type="danger">{error}</Text></div>;
|
||||
}
|
||||
|
||||
const commandStatsOptions = {
|
||||
const chartFallback = <Spin spinning={true} style={{ display: 'block', marginTop: '120px' }} />;
|
||||
|
||||
const commandStatsOptions = useMemo(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)',
|
||||
|
|
@ -69,9 +73,9 @@ const CacheMonitorPage = () => {
|
|||
animationDuration: 1000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}), [cacheData.commandStats]);
|
||||
|
||||
const usedMemoryOptions = {
|
||||
const usedMemoryOptions = useMemo(() => ({
|
||||
tooltip: {
|
||||
formatter: `{b} <br/>{a} : ${toDisplayText(cacheData.info?.used_memory_human)}`,
|
||||
},
|
||||
|
|
@ -92,7 +96,7 @@ const CacheMonitorPage = () => {
|
|||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}), [cacheData.info]);
|
||||
|
||||
return (
|
||||
<div className="app-container cache-monitor-container">
|
||||
|
|
@ -129,12 +133,26 @@ const CacheMonitorPage = () => {
|
|||
</Col>
|
||||
<Col span={12} className="card-box">
|
||||
<Card title="命令统计">
|
||||
<ReactEChartsCore echarts={echarts} theme="macarons" option={commandStatsOptions} style={{ height: '420px' }} />
|
||||
<Suspense fallback={chartFallback}>
|
||||
<ReactEChartsCore
|
||||
echarts={echarts}
|
||||
theme="macarons"
|
||||
option={commandStatsOptions}
|
||||
style={{ height: '420px' }}
|
||||
/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12} className="card-box">
|
||||
<Card title="内存信息">
|
||||
<ReactEChartsCore echarts={echarts} theme="macarons" option={usedMemoryOptions} style={{ height: '420px' }} />
|
||||
<Suspense fallback={chartFallback}>
|
||||
<ReactEChartsCore
|
||||
echarts={echarts}
|
||||
theme="macarons"
|
||||
option={usedMemoryOptions}
|
||||
style={{ height: '420px' }}
|
||||
/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Key } from 'react';
|
||||
import {
|
||||
App,
|
||||
Table,
|
||||
Form,
|
||||
Input,
|
||||
|
|
@ -8,7 +9,6 @@ import {
|
|||
Button,
|
||||
Dropdown,
|
||||
Modal,
|
||||
message,
|
||||
Space,
|
||||
Tag,
|
||||
Switch,
|
||||
|
|
@ -66,7 +66,41 @@ const normalizeRowKey = (value: Key): string | number => {
|
|||
return typeof value === 'bigint' ? value.toString() : value;
|
||||
};
|
||||
|
||||
const inflightJobListRequests = new Map<string, Promise<Awaited<ReturnType<typeof listJob>>>>();
|
||||
const inflightJobDetailRequests = new Map<string, Promise<Awaited<ReturnType<typeof getJob>>>>();
|
||||
|
||||
const serializeJobQuery = (query: JobQueryParams) => JSON.stringify(query);
|
||||
|
||||
const fetchJobList = async (query: JobQueryParams) => {
|
||||
const requestKey = serializeJobQuery(query);
|
||||
const existingRequest = inflightJobListRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = listJob(query).finally(() => {
|
||||
inflightJobListRequests.delete(requestKey);
|
||||
});
|
||||
inflightJobListRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchJobDetail = async (jobId: string | number) => {
|
||||
const requestKey = String(jobId);
|
||||
const existingRequest = inflightJobDetailRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getJob(jobId).finally(() => {
|
||||
inflightJobDetailRequests.delete(requestKey);
|
||||
});
|
||||
inflightJobDetailRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const JobMonitorPage = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const [form] = Form.useForm<JobRecord>();
|
||||
const [queryForm] = Form.useForm<JobQueryParams>();
|
||||
const [jobList, setJobList] = useState<JobRecord[]>([]);
|
||||
|
|
@ -90,7 +124,7 @@ const JobMonitorPage = () => {
|
|||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await listJob(queryParams);
|
||||
const response = await fetchJobList(queryParams);
|
||||
setJobList(response.rows ?? []);
|
||||
setTotal(Number(response.total ?? 0));
|
||||
} catch (error: unknown) {
|
||||
|
|
@ -99,7 +133,7 @@ const JobMonitorPage = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [queryParams]);
|
||||
}, [message, queryParams]);
|
||||
|
||||
useEffect(() => {
|
||||
void getList();
|
||||
|
|
@ -136,7 +170,7 @@ const JobMonitorPage = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await getJob(jobId);
|
||||
const response = await fetchJobDetail(jobId);
|
||||
setCurrentJobDetail(response);
|
||||
form.setFieldsValue({
|
||||
...response,
|
||||
|
|
@ -159,7 +193,7 @@ const JobMonitorPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `是否确认删除定时任务编号为"${jobIds.join(',')}"的数据项?`,
|
||||
onOk: async () => {
|
||||
|
|
@ -208,7 +242,7 @@ const JobMonitorPage = () => {
|
|||
}
|
||||
const { jobId, jobGroup } = record;
|
||||
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认执行',
|
||||
content: `确认要立即执行一次"${record.jobName ?? ''}"任务吗?`,
|
||||
onOk: async () => {
|
||||
|
|
@ -225,7 +259,7 @@ const JobMonitorPage = () => {
|
|||
const handleExport = async () => {
|
||||
const hide = message.loading('正在导出数据...', 0);
|
||||
try {
|
||||
const response = await listJob({ ...queryParams, pageNum: undefined, pageSize: undefined });
|
||||
const response = await fetchJobList({ ...queryParams, pageNum: undefined, pageSize: undefined });
|
||||
const header = ['任务编号', '任务名称', '任务组名', '调用目标字符串', 'cron执行表达式', '状态'];
|
||||
const rows = response.rows.map((job) => [
|
||||
job.jobId,
|
||||
|
|
@ -262,7 +296,7 @@ const JobMonitorPage = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await getJob(record.jobId);
|
||||
const response = await fetchJobDetail(record.jobId);
|
||||
setCurrentJobDetail(response);
|
||||
setDetailModalVisible(true);
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { Key } from 'react';
|
||||
import {
|
||||
App,
|
||||
|
|
@ -52,6 +52,8 @@ const defaultQueryParams: LogininforQueryParams = {
|
|||
isAsc: 'descending',
|
||||
};
|
||||
|
||||
const inflightLogininforRequests = new Map<string, Promise<Awaited<ReturnType<typeof listLogininfor>>>>();
|
||||
|
||||
const parseTime = (time?: string | number | Date, pattern = 'YYYY-MM-DD HH:mm:ss'): string => {
|
||||
return time ? dayjs(time).format(pattern) : '';
|
||||
};
|
||||
|
|
@ -61,6 +63,23 @@ const escapeCsvCell = (value: unknown): string => {
|
|||
return `"${raw.replace(/"/g, '""')}"`;
|
||||
};
|
||||
|
||||
const serializeLogininforQuery = (query: LogininforQueryParams) => JSON.stringify(query);
|
||||
|
||||
const fetchLogininforList = async (query: LogininforQueryParams) => {
|
||||
const requestKey = serializeLogininforQuery(query);
|
||||
const existingRequest = inflightLogininforRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = listLogininfor(query).finally(() => {
|
||||
inflightLogininforRequests.delete(requestKey);
|
||||
});
|
||||
|
||||
inflightLogininforRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const LoginLogPage = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const [queryForm] = Form.useForm<LogininforQueryParams>();
|
||||
|
|
@ -72,19 +91,22 @@ const LoginLogPage = () => {
|
|||
const [dateRange, setDateRange] = useState<DateRange>(null);
|
||||
const [queryParams, setQueryParams] = useState<LogininforQueryParams>(defaultQueryParams);
|
||||
|
||||
const requestParams = useMemo(() => {
|
||||
const formattedQueryParams: LogininforQueryParams = { ...queryParams };
|
||||
if (dateRange?.[0] && dateRange?.[1]) {
|
||||
formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD HH:mm:ss');
|
||||
formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD HH:mm:ss');
|
||||
} else {
|
||||
formattedQueryParams.beginTime = undefined;
|
||||
formattedQueryParams.endTime = undefined;
|
||||
}
|
||||
return formattedQueryParams;
|
||||
}, [dateRange, queryParams]);
|
||||
|
||||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const formattedQueryParams: LogininforQueryParams = { ...queryParams };
|
||||
if (dateRange?.[0] && dateRange?.[1]) {
|
||||
formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD HH:mm:ss');
|
||||
formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD HH:mm:ss');
|
||||
} else {
|
||||
formattedQueryParams.beginTime = undefined;
|
||||
formattedQueryParams.endTime = undefined;
|
||||
}
|
||||
|
||||
const response = await listLogininfor(formattedQueryParams);
|
||||
const response = await fetchLogininforList(requestParams);
|
||||
setList(response.rows ?? []);
|
||||
setTotal(Number(response.total ?? 0));
|
||||
} catch (error: unknown) {
|
||||
|
|
@ -93,7 +115,7 @@ const LoginLogPage = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dateRange, queryParams]);
|
||||
}, [message, requestParams]);
|
||||
|
||||
useEffect(() => {
|
||||
void getList();
|
||||
|
|
@ -186,8 +208,8 @@ const LoginLogPage = () => {
|
|||
const handleExport = async () => {
|
||||
const hide = message.loading('正在导出数据...', 0);
|
||||
try {
|
||||
const response = await listLogininfor({
|
||||
...queryParams,
|
||||
const response = await fetchLogininforList({
|
||||
...requestParams,
|
||||
pageNum: undefined,
|
||||
pageSize: undefined,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Table, Form, Input, Button, Modal, message } from 'antd';
|
||||
import { App, Table, Form, Input, Button } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import { SearchOutlined, ReloadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { listOnline, forceLogout } from '../../api/monitor/online';
|
||||
|
|
@ -17,6 +17,8 @@ const defaultQueryParams: OnlineQueryParams = {
|
|||
userName: undefined,
|
||||
};
|
||||
|
||||
const inflightOnlineRequests = new Map<string, Promise<Awaited<ReturnType<typeof listOnline>>>>();
|
||||
|
||||
const parseTime = (time?: string | number | Date, pattern = 'YYYY-MM-DD HH:mm:ss'): string => {
|
||||
return time ? dayjs(time).format(pattern) : '';
|
||||
};
|
||||
|
|
@ -31,7 +33,24 @@ const normalizeDateValue = (value: unknown): string | number | Date | undefined
|
|||
return undefined;
|
||||
};
|
||||
|
||||
const serializeOnlineQuery = (query: OnlineQueryParams) => JSON.stringify(query);
|
||||
|
||||
const fetchOnlineList = async (query: OnlineQueryParams) => {
|
||||
const requestKey = serializeOnlineQuery(query);
|
||||
const existingRequest = inflightOnlineRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = listOnline(query).finally(() => {
|
||||
inflightOnlineRequests.delete(requestKey);
|
||||
});
|
||||
inflightOnlineRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const OnlineUserPage = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const { hasPermi } = usePermission();
|
||||
const [queryForm] = Form.useForm<OnlineQueryParams>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -44,7 +63,7 @@ const OnlineUserPage = () => {
|
|||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await listOnline(queryParams);
|
||||
const response = await fetchOnlineList(queryParams);
|
||||
setList(response.rows ?? []);
|
||||
setTotal(Number(response.total ?? 0));
|
||||
} catch (error: unknown) {
|
||||
|
|
@ -53,7 +72,7 @@ const OnlineUserPage = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [queryParams]);
|
||||
}, [message, queryParams]);
|
||||
|
||||
useEffect(() => {
|
||||
void getList();
|
||||
|
|
@ -79,7 +98,7 @@ const OnlineUserPage = () => {
|
|||
}
|
||||
const tokenId = row.tokenId;
|
||||
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认强退',
|
||||
content: `是否确认强退名称为"${row.userName ?? ''}"的用户?`,
|
||||
onOk: async () => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { Key } from 'react';
|
||||
import {
|
||||
App,
|
||||
Table,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Modal,
|
||||
message,
|
||||
Space,
|
||||
Tag,
|
||||
DatePicker,
|
||||
|
|
@ -64,6 +64,8 @@ const defaultQueryParams: OperlogQueryParams = {
|
|||
isAsc: 'descending',
|
||||
};
|
||||
|
||||
const inflightOperlogRequests = new Map<string, Promise<Awaited<ReturnType<typeof listOperlog>>>>();
|
||||
|
||||
const parseTime = (time?: string | number | Date, pattern = 'YYYY-MM-DD HH:mm:ss'): string => {
|
||||
return time ? dayjs(time).format(pattern) : '';
|
||||
};
|
||||
|
|
@ -83,7 +85,25 @@ const escapeCsvCell = (value: unknown): string => {
|
|||
return `"${raw.replace(/"/g, '""')}"`;
|
||||
};
|
||||
|
||||
const serializeOperlogQuery = (query: OperlogQueryParams) => JSON.stringify(query);
|
||||
|
||||
const fetchOperlogList = async (query: OperlogQueryParams) => {
|
||||
const requestKey = serializeOperlogQuery(query);
|
||||
const existingRequest = inflightOperlogRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = listOperlog(query).finally(() => {
|
||||
inflightOperlogRequests.delete(requestKey);
|
||||
});
|
||||
|
||||
inflightOperlogRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const OperationLogPage = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const [queryForm] = Form.useForm<OperlogQueryParams>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [list, setList] = useState<OperlogRecord[]>([]);
|
||||
|
|
@ -94,19 +114,22 @@ const OperationLogPage = () => {
|
|||
const [currentOperlogDetail, setCurrentOperlogDetail] = useState<OperlogRecord>({});
|
||||
const [queryParams, setQueryParams] = useState<OperlogQueryParams>(defaultQueryParams);
|
||||
|
||||
const requestParams = useMemo(() => {
|
||||
const formattedQueryParams: OperlogQueryParams = { ...queryParams };
|
||||
if (dateRange?.[0] && dateRange?.[1]) {
|
||||
formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD HH:mm:ss');
|
||||
formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD HH:mm:ss');
|
||||
} else {
|
||||
formattedQueryParams.beginTime = undefined;
|
||||
formattedQueryParams.endTime = undefined;
|
||||
}
|
||||
return formattedQueryParams;
|
||||
}, [dateRange, queryParams]);
|
||||
|
||||
const getList = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const formattedQueryParams: OperlogQueryParams = { ...queryParams };
|
||||
if (dateRange?.[0] && dateRange?.[1]) {
|
||||
formattedQueryParams.beginTime = dateRange[0].format('YYYY-MM-DD HH:mm:ss');
|
||||
formattedQueryParams.endTime = dateRange[1].format('YYYY-MM-DD HH:mm:ss');
|
||||
} else {
|
||||
formattedQueryParams.beginTime = undefined;
|
||||
formattedQueryParams.endTime = undefined;
|
||||
}
|
||||
|
||||
const response = await listOperlog(formattedQueryParams);
|
||||
const response = await fetchOperlogList(requestParams);
|
||||
setList(response.rows ?? []);
|
||||
setTotal(Number(response.total ?? 0));
|
||||
} catch (error: unknown) {
|
||||
|
|
@ -115,7 +138,7 @@ const OperationLogPage = () => {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dateRange, queryParams]);
|
||||
}, [message, requestParams]);
|
||||
|
||||
useEffect(() => {
|
||||
void getList();
|
||||
|
|
@ -143,7 +166,7 @@ const OperationLogPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `是否确认删除日志编号为"${operIds}"的数据项?`,
|
||||
onOk: async () => {
|
||||
|
|
@ -160,7 +183,7 @@ const OperationLogPage = () => {
|
|||
};
|
||||
|
||||
const handleClean = async () => {
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认清空',
|
||||
content: '是否确认清空所有操作日志数据项?',
|
||||
onOk: async () => {
|
||||
|
|
@ -176,12 +199,12 @@ const OperationLogPage = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const operTypeLabel = (type?: string): string => {
|
||||
const operTypeLabel = (type?: string | number): string => {
|
||||
const dict = sysOperTypeDict.find((item) => item.value === String(type ?? ''));
|
||||
return dict?.label ?? String(type ?? '');
|
||||
};
|
||||
|
||||
const operStatusLabel = (status?: string): string => {
|
||||
const operStatusLabel = (status?: string | number): string => {
|
||||
const dict = sysCommonStatusDict.find((item) => item.value === String(status ?? ''));
|
||||
return dict?.label ?? String(status ?? '');
|
||||
};
|
||||
|
|
@ -189,8 +212,8 @@ const OperationLogPage = () => {
|
|||
const handleExport = async () => {
|
||||
const hide = message.loading('正在导出数据...', 0);
|
||||
try {
|
||||
const response = await listOperlog({
|
||||
...queryParams,
|
||||
const response = await fetchOperlogList({
|
||||
...requestParams,
|
||||
pageNum: undefined,
|
||||
pageSize: undefined,
|
||||
});
|
||||
|
|
@ -242,12 +265,12 @@ const OperationLogPage = () => {
|
|||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const operTypeFormat = (type?: string) => {
|
||||
const operTypeFormat = (type?: string | number) => {
|
||||
const dict = sysOperTypeDict.find((item) => item.value === String(type ?? ''));
|
||||
return dict ? <Tag color={dict.color}>{dict.label}</Tag> : String(type ?? '');
|
||||
};
|
||||
|
||||
const operStatusFormat = (status?: string) => {
|
||||
const operStatusFormat = (status?: string | number) => {
|
||||
const dict = sysCommonStatusDict.find((item) => item.value === String(status ?? ''));
|
||||
return dict ? <Tag color={dict.color}>{dict.label}</Tag> : String(status ?? '');
|
||||
};
|
||||
|
|
@ -277,7 +300,7 @@ const OperationLogPage = () => {
|
|||
title: '操作类型',
|
||||
dataIndex: 'businessType',
|
||||
align: 'center',
|
||||
render: (text) => operTypeFormat(typeof text === 'string' ? text : undefined),
|
||||
render: (text) => operTypeFormat(typeof text === 'string' || typeof text === 'number' ? text : undefined),
|
||||
},
|
||||
{ title: '操作人员', dataIndex: 'operName', align: 'center', ellipsis: true, sorter: true },
|
||||
{ title: '操作地址', dataIndex: 'operIp', align: 'center', ellipsis: true },
|
||||
|
|
@ -286,7 +309,7 @@ const OperationLogPage = () => {
|
|||
title: '操作状态',
|
||||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
render: (text) => operStatusFormat(typeof text === 'string' ? text : undefined),
|
||||
render: (text) => operStatusFormat(typeof text === 'string' || typeof text === 'number' ? text : undefined),
|
||||
},
|
||||
{
|
||||
title: '操作日期',
|
||||
|
|
@ -431,7 +454,9 @@ const OperationLogPage = () => {
|
|||
<Descriptions.Item label="操作模块">
|
||||
{currentOperlogDetail.title} /{' '}
|
||||
{operTypeFormat(
|
||||
typeof currentOperlogDetail.businessType === 'string' ? currentOperlogDetail.businessType : undefined,
|
||||
typeof currentOperlogDetail.businessType === 'string' || typeof currentOperlogDetail.businessType === 'number'
|
||||
? currentOperlogDetail.businessType
|
||||
: undefined,
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="登录信息">
|
||||
|
|
@ -449,7 +474,11 @@ const OperationLogPage = () => {
|
|||
{currentOperlogDetail.jsonResult}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="操作状态">
|
||||
{operStatusFormat(typeof currentOperlogDetail.status === 'string' ? currentOperlogDetail.status : undefined)}
|
||||
{operStatusFormat(
|
||||
typeof currentOperlogDetail.status === 'string' || typeof currentOperlogDetail.status === 'number'
|
||||
? currentOperlogDetail.status
|
||||
: undefined,
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="消耗时间">
|
||||
{typeof currentOperlogDetail.costTime === 'number' ? `${currentOperlogDetail.costTime}毫秒` : ''}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ const defaultServerInfo: ServerInfoResponse = {
|
|||
sysFiles: [],
|
||||
};
|
||||
|
||||
let inflightServerInfoRequest: Promise<ServerInfoResponse> | null = null;
|
||||
|
||||
const parseTime = (time?: string | number | Date, pattern = 'YYYY-MM-DD HH:mm:ss'): string => {
|
||||
return time ? dayjs(time).format(pattern) : '';
|
||||
};
|
||||
|
|
@ -65,7 +67,12 @@ const ServerMonitorPage = () => {
|
|||
setLoading(true);
|
||||
const hide = message.loading('正在加载服务监控数据,请稍候!', 0);
|
||||
try {
|
||||
const response = await getServerInfo();
|
||||
if (!inflightServerInfoRequest) {
|
||||
inflightServerInfoRequest = getServerInfo().finally(() => {
|
||||
inflightServerInfoRequest = null;
|
||||
});
|
||||
}
|
||||
const response = await inflightServerInfoRequest;
|
||||
setServerInfo(response);
|
||||
} catch (error: unknown) {
|
||||
console.error('Failed to fetch server info:', error);
|
||||
|
|
@ -77,7 +84,7 @@ const ServerMonitorPage = () => {
|
|||
};
|
||||
|
||||
void getList();
|
||||
}, []);
|
||||
}, [message]);
|
||||
|
||||
const diskColumns: TableColumnsType<ServerDiskInfo> = [
|
||||
{ title: '盘符路径', dataIndex: 'dirName', align: 'center' },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
App,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
|
|
@ -9,7 +10,6 @@ import {
|
|||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
|
|
@ -265,6 +265,7 @@ const buildEndDate = (start: Dayjs, workHours: number) => {
|
|||
};
|
||||
|
||||
const DemandManagePage: React.FC = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [queryForm] = Form.useForm();
|
||||
|
|
@ -554,8 +555,7 @@ const DemandManagePage: React.FC = () => {
|
|||
}, [pageNum, pageSize, projectId, query, selectedVersion, toDemandRow]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDictOptions();
|
||||
loadUserOptions();
|
||||
void Promise.all([loadDictOptions(), loadUserOptions()]);
|
||||
}, [loadDictOptions, loadUserOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -747,8 +747,7 @@ const DemandManagePage: React.FC = () => {
|
|||
|
||||
setDemandModalOpen(false);
|
||||
setSelectedRowKeys([]);
|
||||
fetchDemands();
|
||||
fetchVersions();
|
||||
void Promise.all([fetchDemands(), fetchVersions()]);
|
||||
} catch {
|
||||
// Validate/API error is handled by antd/request interceptor.
|
||||
} finally {
|
||||
|
|
@ -790,7 +789,7 @@ const DemandManagePage: React.FC = () => {
|
|||
);
|
||||
} catch {
|
||||
message.error('更新失败');
|
||||
fetchDemands();
|
||||
void fetchDemands();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -799,8 +798,7 @@ const DemandManagePage: React.FC = () => {
|
|||
await deleteDemand(record.id);
|
||||
message.success('删除成功');
|
||||
setSelectedRowKeys([]);
|
||||
fetchDemands();
|
||||
fetchVersions();
|
||||
void Promise.all([fetchDemands(), fetchVersions()]);
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
|
|
@ -815,8 +813,7 @@ const DemandManagePage: React.FC = () => {
|
|||
await deleteDemandBatch(selectedRowKeys.join(','));
|
||||
message.success('删除成功');
|
||||
setSelectedRowKeys([]);
|
||||
fetchDemands();
|
||||
fetchVersions();
|
||||
void Promise.all([fetchDemands(), fetchVersions()]);
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
|
|
@ -852,8 +849,7 @@ const DemandManagePage: React.FC = () => {
|
|||
}
|
||||
|
||||
setVersionModalOpen(false);
|
||||
fetchVersions();
|
||||
fetchDemands();
|
||||
void Promise.all([fetchVersions(), fetchDemands()]);
|
||||
} catch {
|
||||
// Validate/API error handled by antd/request interceptor.
|
||||
} finally {
|
||||
|
|
@ -868,8 +864,7 @@ const DemandManagePage: React.FC = () => {
|
|||
if (selectedVersion.nodeId === item.nodeId) {
|
||||
setSelectedVersion({ id: projectId, nodeId: 'all', type: 2, title: '全部版本' });
|
||||
}
|
||||
fetchVersions();
|
||||
fetchDemands();
|
||||
void Promise.all([fetchVersions(), fetchDemands()]);
|
||||
} catch {
|
||||
message.error('版本删除失败');
|
||||
}
|
||||
|
|
@ -885,7 +880,7 @@ const DemandManagePage: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
if (action === 'delete') {
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '此操作将永久删除该版本号,是否继续?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
|
|
@ -1292,7 +1287,7 @@ const DemandManagePage: React.FC = () => {
|
|||
title="需求列表"
|
||||
extra={
|
||||
<Button icon={<LeftOutlined />} onClick={() => navigate('/project/list')}>
|
||||
返回项目列表
|
||||
返回
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, Button, message, Card, Row, Col, DatePicker, InputNumber } from 'antd';
|
||||
import { Form, Input, Button, Card, Row, Col, DatePicker, InputNumber } from 'antd';
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { getProjectDetail, addProject, updateProject, getProjectCode } from '../../api/project';
|
||||
import dayjs from 'dayjs';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import { notify } from '@/utils/notify';
|
||||
import './project-detail.css';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
|
@ -37,7 +38,7 @@ const ProjectDetailPage: React.FC = () => {
|
|||
endDate: endValue ? dayjs(endValue) : null,
|
||||
});
|
||||
} catch (error) {
|
||||
message.error('获取项目详情失败');
|
||||
notify.error('获取项目详情失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -47,7 +48,7 @@ const ProjectDetailPage: React.FC = () => {
|
|||
const response = await getProjectCode();
|
||||
form.setFieldsValue({ projectCode: ((response as Record<string, unknown>).data ?? response) as string });
|
||||
} catch(error) {
|
||||
message.error('获取项目编号失败');
|
||||
notify.error('获取项目编号失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -67,14 +68,14 @@ const ProjectDetailPage: React.FC = () => {
|
|||
};
|
||||
if (isEdit) {
|
||||
await updateProject({ ...payload, projectId: id });
|
||||
message.success('修改成功');
|
||||
notify.success('修改成功');
|
||||
} else {
|
||||
await addProject(payload);
|
||||
message.success('新增成功');
|
||||
notify.success('新增成功');
|
||||
}
|
||||
navigate('/project/list');
|
||||
} catch (error) {
|
||||
message.error(isEdit ? '修改失败' : '新增失败');
|
||||
notify.error(isEdit ? '修改失败' : '新增失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, DatePicker, Empty, Select, Spin, Table, message } from 'antd';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button, DatePicker, Empty, Select, Spin, Table } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import { CalendarOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
|
@ -8,10 +8,11 @@ import type { Dayjs } from 'dayjs';
|
|||
import zhCN from 'antd/es/date-picker/locale/zh_CN';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { getProjectDetail, listProject } from '@/api/project';
|
||||
import { getProjectExecutionInfo, getProjectWorkInfo } from '@/api/projectExecution';
|
||||
import { getProjectWorkInfo } from '@/api/projectExecution';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import '@/styles/permission-link.css';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
import { notify } from '@/utils/notify';
|
||||
import './project-user.css';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
|
@ -43,6 +44,12 @@ interface ProjectOption {
|
|||
projectName: string;
|
||||
}
|
||||
|
||||
type ProjectWorkInfoRequestParams = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
projectId: string | number;
|
||||
};
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null;
|
||||
|
||||
|
|
@ -54,22 +61,6 @@ const toNumber = (value: unknown, fallback = 0) => {
|
|||
return Number.isFinite(num) ? num : fallback;
|
||||
};
|
||||
|
||||
const extractProjectList = (payload: unknown): ProjectOption[] => {
|
||||
const data = normalizeResponseData(payload);
|
||||
const list = Array.isArray(data)
|
||||
? data
|
||||
: isObject(data) && Array.isArray(data.rows)
|
||||
? data.rows
|
||||
: [];
|
||||
return list
|
||||
.filter(isObject)
|
||||
.map((item) => ({
|
||||
projectId: item.projectId as string | number,
|
||||
projectName: String(item.projectName ?? ''),
|
||||
}))
|
||||
.filter((item) => item.projectId !== undefined && item.projectId !== null && item.projectName);
|
||||
};
|
||||
|
||||
const extractProjectRows = (payload: unknown): Record<string, unknown>[] => {
|
||||
const data = normalizeResponseData(payload);
|
||||
if (Array.isArray(data)) {
|
||||
|
|
@ -115,11 +106,58 @@ const normalizeDateParam = (value: string | null) => {
|
|||
return parsed.isValid() ? parsed.format('YYYY-MM-DD') : null;
|
||||
};
|
||||
|
||||
let inflightProjectListRequest: Promise<unknown> | null = null;
|
||||
const inflightProjectDetailRequests = new Map<string, Promise<unknown>>();
|
||||
const inflightProjectWorkInfoRequests = new Map<string, Promise<unknown>>();
|
||||
|
||||
const fetchProjectListRequest = async () => {
|
||||
if (inflightProjectListRequest) {
|
||||
return inflightProjectListRequest;
|
||||
}
|
||||
|
||||
const requestPromise = listProject({ pageNum: 1, pageSize: 1000 }).finally(() => {
|
||||
inflightProjectListRequest = null;
|
||||
});
|
||||
inflightProjectListRequest = requestPromise;
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchProjectDetailRequest = async (projectId: string | number) => {
|
||||
const requestKey = String(projectId);
|
||||
const existingRequest = inflightProjectDetailRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getProjectDetail(projectId).finally(() => {
|
||||
inflightProjectDetailRequests.delete(requestKey);
|
||||
});
|
||||
inflightProjectDetailRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const serializeProjectWorkInfoRequest = (params: ProjectWorkInfoRequestParams) => JSON.stringify(params);
|
||||
|
||||
const fetchProjectWorkInfoRequest = async (params: ProjectWorkInfoRequestParams) => {
|
||||
const requestKey = serializeProjectWorkInfoRequest(params);
|
||||
const existingRequest = inflightProjectWorkInfoRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getProjectWorkInfo(params).finally(() => {
|
||||
inflightProjectWorkInfoRequests.delete(requestKey);
|
||||
});
|
||||
inflightProjectWorkInfoRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const ProjectUserPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = searchParams.get('projectId') ?? searchParams.get('id') ?? '';
|
||||
const queryProjectName = searchParams.get('projectName') ?? '';
|
||||
const queryStartDate = normalizeDateParam(searchParams.get('startDate'));
|
||||
const queryEndDate = normalizeDateParam(searchParams.get('endDate'));
|
||||
const defaultStartDate = queryStartDate ?? dayjs().startOf('month').format('YYYY-MM-DD');
|
||||
|
|
@ -135,6 +173,10 @@ const ProjectUserPage = () => {
|
|||
dayjs(defaultEndDate, 'YYYY-MM-DD'),
|
||||
]);
|
||||
const [detailList, setDetailList] = useState<ProjectUserItem[][]>([]);
|
||||
const [projectMetaReadyKey, setProjectMetaReadyKey] = useState('');
|
||||
const projectDetailPendingRef = useRef('');
|
||||
const projectListRequestIdRef = useRef(0);
|
||||
const projectWorkInfoRequestIdRef = useRef(0);
|
||||
const canViewWorkLog = canAccessPath('/index');
|
||||
|
||||
const openUserLog = (row: ProjectUserItem, queryDate?: string) => {
|
||||
|
|
@ -164,30 +206,26 @@ const ProjectUserPage = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const fetchProjectList = async () => {
|
||||
const requestId = ++projectListRequestIdRef.current;
|
||||
setLoading(true);
|
||||
try {
|
||||
let nextProjectList: ProjectOption[] = [];
|
||||
|
||||
try {
|
||||
const response = await getProjectExecutionInfo({
|
||||
startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
|
||||
endDate: `${dateRange[1].format('YYYY-MM-DD')} 23:59:59`,
|
||||
});
|
||||
nextProjectList = extractProjectList(response);
|
||||
} catch (error) {
|
||||
console.warn('Project execution info timeout, fallback to project list:', error);
|
||||
const response = await listProject({ pageNum: 1, pageSize: 1000 });
|
||||
nextProjectList = extractProjectRows(response).map((item) => ({
|
||||
const response = await fetchProjectListRequest();
|
||||
if (requestId !== projectListRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
const nextProjectList = extractProjectRows(response)
|
||||
.map((item) => ({
|
||||
projectId: item.projectId as string | number,
|
||||
projectName: String(item.projectName ?? ''),
|
||||
})).filter((item) => item.projectId !== undefined && item.projectId !== null && item.projectName);
|
||||
}
|
||||
}))
|
||||
.filter((item) => item.projectId !== undefined && item.projectId !== null && item.projectName);
|
||||
|
||||
setProjectList(nextProjectList);
|
||||
if (nextProjectList.length === 0) {
|
||||
setSelectedProjectId(undefined);
|
||||
setSelectedProjectId(projectId || undefined);
|
||||
setProjectDetail({});
|
||||
setDetailList([]);
|
||||
setProjectMetaReadyKey('');
|
||||
return;
|
||||
}
|
||||
setSelectedProjectId((prev) => {
|
||||
|
|
@ -203,10 +241,16 @@ const ProjectUserPage = () => {
|
|||
return nextProjectList[0].projectId;
|
||||
});
|
||||
} catch (error) {
|
||||
if (requestId !== projectListRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to fetch project list:', error);
|
||||
message.error('获取项目列表失败');
|
||||
setProjectList([]);
|
||||
notify.error('获取项目列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (requestId === projectListRequestIdRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
void fetchProjectList();
|
||||
|
|
@ -215,11 +259,18 @@ const ProjectUserPage = () => {
|
|||
useEffect(() => {
|
||||
const fetchProjectData = async () => {
|
||||
if (!selectedProjectId) {
|
||||
setProjectMetaReadyKey('');
|
||||
return;
|
||||
}
|
||||
const activeProjectId = String(selectedProjectId);
|
||||
projectDetailPendingRef.current = activeProjectId;
|
||||
setProjectMetaReadyKey('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const detailResponse = await getProjectDetail(selectedProjectId);
|
||||
const detailResponse = await fetchProjectDetailRequest(selectedProjectId);
|
||||
if (projectDetailPendingRef.current !== activeProjectId) {
|
||||
return;
|
||||
}
|
||||
const detail = normalizeProjectDetail(detailResponse);
|
||||
setProjectDetail(detail);
|
||||
|
||||
|
|
@ -246,9 +297,14 @@ const ProjectUserPage = () => {
|
|||
if (useRoutePreset) {
|
||||
setRoutePresetApplied(true);
|
||||
}
|
||||
projectDetailPendingRef.current = '';
|
||||
setProjectMetaReadyKey(`${activeProjectId}:${nextStart.valueOf()}:${nextEnd.valueOf()}`);
|
||||
} catch (error) {
|
||||
if (projectDetailPendingRef.current === activeProjectId) {
|
||||
projectDetailPendingRef.current = '';
|
||||
}
|
||||
console.error('Failed to fetch project detail:', error);
|
||||
message.error('获取项目详情失败');
|
||||
notify.error('获取项目详情失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -258,27 +314,40 @@ const ProjectUserPage = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const fetchWorkInfo = async () => {
|
||||
if (!selectedProjectId) {
|
||||
if (!selectedProjectId || !projectMetaReadyKey) {
|
||||
return;
|
||||
}
|
||||
if (projectDetailPendingRef.current === String(selectedProjectId)) {
|
||||
return;
|
||||
}
|
||||
const requestId = ++projectWorkInfoRequestIdRef.current;
|
||||
const requestParams = {
|
||||
startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
|
||||
endDate: `${dateRange[1].format('YYYY-MM-DD')} 23:59:59`,
|
||||
projectId: selectedProjectId,
|
||||
};
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getProjectWorkInfo({
|
||||
startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
|
||||
endDate: `${dateRange[1].format('YYYY-MM-DD')} 23:59:59`,
|
||||
projectId: selectedProjectId,
|
||||
});
|
||||
const response = await fetchProjectWorkInfoRequest(requestParams);
|
||||
if (requestId !== projectWorkInfoRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
setDetailList(extractDetailList(response));
|
||||
} catch (error) {
|
||||
if (requestId !== projectWorkInfoRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to fetch project work info:', error);
|
||||
message.error('获取项目人员表失败');
|
||||
notify.error('获取项目人员表失败');
|
||||
setDetailList([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (requestId === projectWorkInfoRequestIdRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
void fetchWorkInfo();
|
||||
}, [dateRange, selectedProjectId]);
|
||||
}, [dateRange, projectMetaReadyKey, selectedProjectId]);
|
||||
|
||||
const columns = useMemo<TableColumnsType<Record<string, unknown>>>(() => {
|
||||
return dateRange[0]
|
||||
|
|
@ -340,6 +409,34 @@ const ProjectUserPage = () => {
|
|||
return hit?.projectName ?? '';
|
||||
}, [projectList, selectedProjectId]);
|
||||
|
||||
const selectedProjectLabel = useMemo(() => {
|
||||
return projectDetail.projectName || selectedProjectName || queryProjectName || String(selectedProjectId ?? '');
|
||||
}, [projectDetail.projectName, queryProjectName, selectedProjectId, selectedProjectName]);
|
||||
|
||||
const projectSelectOptions = useMemo(() => {
|
||||
const baseOptions = projectList.map((item) => ({
|
||||
label: item.projectName,
|
||||
value: item.projectId,
|
||||
}));
|
||||
|
||||
if (selectedProjectId === undefined || selectedProjectId === null || selectedProjectId === '') {
|
||||
return baseOptions;
|
||||
}
|
||||
|
||||
const exists = baseOptions.some((item) => String(item.value) === String(selectedProjectId));
|
||||
if (exists) {
|
||||
return baseOptions;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: selectedProjectLabel,
|
||||
value: selectedProjectId,
|
||||
},
|
||||
...baseOptions,
|
||||
];
|
||||
}, [projectList, selectedProjectId, selectedProjectLabel]);
|
||||
|
||||
return (
|
||||
<div className="project-user-page">
|
||||
<div className="project-user-back-row">
|
||||
|
|
@ -352,12 +449,9 @@ const ProjectUserPage = () => {
|
|||
<div className="project-user-info-item">
|
||||
<span className="project-user-info-label">选择项目</span>
|
||||
<Select
|
||||
value={selectedProjectId}
|
||||
value={selectedProjectId !== undefined && selectedProjectId !== null && selectedProjectId !== '' ? selectedProjectId : undefined}
|
||||
placeholder="请选择项目"
|
||||
options={projectList.map((item) => ({
|
||||
label: item.projectName,
|
||||
value: item.projectId,
|
||||
}))}
|
||||
options={projectSelectOptions}
|
||||
onChange={(value) => setSelectedProjectId(value)}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
|
|
@ -401,7 +495,7 @@ const ProjectUserPage = () => {
|
|||
let nextEnd = values[1];
|
||||
if (values[1].endOf('day').diff(values[0].startOf('day'), 'day') > MAX_RANGE_DAYS) {
|
||||
nextEnd = values[0].add(MAX_RANGE_DAYS, 'day');
|
||||
message.warning('统计时间最多选择 3 个月,已自动调整结束日期');
|
||||
notify.warning('统计时间最多选择 3 个月,已自动调整结束日期');
|
||||
}
|
||||
setDateRange([values[0], nextEnd]);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, DatePicker, Empty, Input, Modal, Spin, Table, Tree, message } from 'antd';
|
||||
import { useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { App, Button, DatePicker, Empty, Input, Modal, Spin, Table, Tree } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import zhCN from 'antd/es/date-picker/locale/zh_CN';
|
||||
|
|
@ -59,6 +59,23 @@ const toNumber = (value: unknown, fallback = 0) => {
|
|||
|
||||
const getDefaultRange = (): [Dayjs, Dayjs] => [dayjs().startOf('month'), dayjs().endOf('month')];
|
||||
|
||||
type UserListRequestParams = {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
deptId?: string;
|
||||
};
|
||||
|
||||
type UserProjectRequestParams = {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
userId: string | number;
|
||||
};
|
||||
|
||||
const inflightUserListRequests = new Map<string, Promise<unknown>>();
|
||||
const inflightUserProjectRequests = new Map<string, Promise<unknown>>();
|
||||
let inflightCurrentUserRequest: Promise<unknown> | null = null;
|
||||
let inflightDeptTreeRequest: Promise<unknown> | null = null;
|
||||
|
||||
const normalizeExecutionRows = (payload: unknown): ProjectExecutionRow[] => {
|
||||
const data = normalizeResponseData(payload);
|
||||
const rows = Array.isArray(data)
|
||||
|
|
@ -129,7 +146,64 @@ const matchesUserKeyword = (user: UserRow, keyword: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
const serializeUserListRequest = (params: UserListRequestParams) => JSON.stringify(params);
|
||||
const serializeUserProjectRequest = (params: UserProjectRequestParams) => JSON.stringify(params);
|
||||
|
||||
const fetchUserListRequest = async (params: UserListRequestParams) => {
|
||||
const requestKey = serializeUserListRequest(params);
|
||||
const existingRequest = inflightUserListRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = listUser(params).finally(() => {
|
||||
inflightUserListRequests.delete(requestKey);
|
||||
});
|
||||
|
||||
inflightUserListRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchCurrentUserRequest = async () => {
|
||||
if (inflightCurrentUserRequest) {
|
||||
return inflightCurrentUserRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getUserProfile().finally(() => {
|
||||
inflightCurrentUserRequest = null;
|
||||
});
|
||||
inflightCurrentUserRequest = requestPromise;
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchUserProjectRequest = async (params: UserProjectRequestParams) => {
|
||||
const requestKey = serializeUserProjectRequest(params);
|
||||
const existingRequest = inflightUserProjectRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getProjectExecutionInfo(params).finally(() => {
|
||||
inflightUserProjectRequests.delete(requestKey);
|
||||
});
|
||||
inflightUserProjectRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchDeptTreeRequest = async () => {
|
||||
if (inflightDeptTreeRequest) {
|
||||
return inflightDeptTreeRequest;
|
||||
}
|
||||
|
||||
const requestPromise = deptTreeSelect().finally(() => {
|
||||
inflightDeptTreeRequest = null;
|
||||
});
|
||||
inflightDeptTreeRequest = requestPromise;
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const UserProjectPage = () => {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -151,79 +225,122 @@ const UserProjectPage = () => {
|
|||
const [userPageSize, setUserPageSize] = useState(10);
|
||||
const [userTotal, setUserTotal] = useState(0);
|
||||
const deferredUserKeyword = useDeferredValue(userKeyword);
|
||||
const deptTreeLoadedRef = useRef(false);
|
||||
const skipNextProjectFetchRef = useRef(false);
|
||||
const userProjectRequestIdRef = useRef(0);
|
||||
const canViewProjectDetail = canAccessPath('/project/detail');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCurrentUser = async () => {
|
||||
try {
|
||||
const response = await getUserProfile();
|
||||
const payload = normalizeResponseData(response);
|
||||
const user = isObject(payload) && isObject(payload.user) ? payload.user : payload;
|
||||
if (!isObject(user)) {
|
||||
return;
|
||||
}
|
||||
const nextUser: UserRow = {
|
||||
userId: user.userId as string | number | undefined,
|
||||
nickName: user.nickName as string | undefined,
|
||||
userName: user.userName as string | undefined,
|
||||
};
|
||||
setSelectedUser(nextUser);
|
||||
setSelectedUserId(nextUser.userId ?? '');
|
||||
setPendingUserId(nextUser.userId ?? '');
|
||||
setSelectedUserName(getUserDisplayName(nextUser));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch current user profile for user project page:', error);
|
||||
message.error('获取当前用户失败');
|
||||
const fetchCurrentUser = async () => {
|
||||
try {
|
||||
const response = await fetchCurrentUserRequest();
|
||||
const payload = normalizeResponseData(response);
|
||||
const user = isObject(payload) && isObject(payload.user) ? payload.user : payload;
|
||||
if (!isObject(user)) {
|
||||
return null;
|
||||
}
|
||||
const nextUser: UserRow = {
|
||||
userId: user.userId as string | number | undefined,
|
||||
nickName: user.nickName as string | undefined,
|
||||
userName: user.userName as string | undefined,
|
||||
};
|
||||
setSelectedUser(nextUser);
|
||||
setSelectedUserId(nextUser.userId ?? '');
|
||||
setPendingUserId(nextUser.userId ?? '');
|
||||
setSelectedUserName(getUserDisplayName(nextUser));
|
||||
return nextUser;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch current user profile for user project page:', error);
|
||||
message.error('获取当前用户失败');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserProject = async (targetUserId?: string | number) => {
|
||||
const effectiveUserId = targetUserId ?? selectedUserId;
|
||||
if (!effectiveUserId || !dateRange[0] || !dateRange[1]) {
|
||||
return;
|
||||
}
|
||||
const requestId = ++userProjectRequestIdRef.current;
|
||||
const requestParams = {
|
||||
startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
|
||||
endDate: `${dateRange[1].format('YYYY-MM-DD')} 00:00:00`,
|
||||
userId: effectiveUserId,
|
||||
};
|
||||
void fetchCurrentUser();
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetchUserProjectRequest(requestParams);
|
||||
if (requestId !== userProjectRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
setExecutionData(normalizeExecutionRows(response));
|
||||
} catch (error) {
|
||||
if (requestId !== userProjectRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to fetch user project table:', error);
|
||||
message.error('获取人员项目表失败');
|
||||
setExecutionData([]);
|
||||
} finally {
|
||||
if (requestId === userProjectRequestIdRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDeptTree = async () => {
|
||||
if (deptTreeLoadedRef.current) {
|
||||
return;
|
||||
}
|
||||
setDeptLoading(true);
|
||||
try {
|
||||
const response = await fetchDeptTreeRequest();
|
||||
const treeNodes = normalizeDeptTreeNodes(response);
|
||||
setDeptTree(treeNodes);
|
||||
setExpandedDeptKeys(collectDeptKeys(treeNodes));
|
||||
deptTreeLoadedRef.current = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch department tree for user project page:', error);
|
||||
setDeptTree([]);
|
||||
setExpandedDeptKeys([]);
|
||||
} finally {
|
||||
setDeptLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const bootstrap = async () => {
|
||||
const user = await fetchCurrentUser();
|
||||
if (cancelled || !user?.userId) {
|
||||
return;
|
||||
}
|
||||
skipNextProjectFetchRef.current = true;
|
||||
await fetchUserProject(user.userId);
|
||||
};
|
||||
|
||||
void bootstrap();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// dateRange intentionally excluded: initial bootstrap only.
|
||||
// subsequent range/user changes are handled by the dedicated effect below.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserProject = async () => {
|
||||
if (!selectedUserId || !dateRange[0] || !dateRange[1]) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getProjectExecutionInfo({
|
||||
startDate: `${dateRange[0].format('YYYY-MM-DD')} 00:00:00`,
|
||||
endDate: `${dateRange[1].format('YYYY-MM-DD')} 00:00:00`,
|
||||
userId: selectedUserId,
|
||||
});
|
||||
setExecutionData(normalizeExecutionRows(response));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user project table:', error);
|
||||
message.error('获取人员项目表失败');
|
||||
setExecutionData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
void fetchUserProject();
|
||||
}, [dateRange, selectedUserId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userModalOpen) {
|
||||
if (!selectedUserId || !dateRange[0] || !dateRange[1]) {
|
||||
return;
|
||||
}
|
||||
const fetchDeptTree = async () => {
|
||||
setDeptLoading(true);
|
||||
try {
|
||||
const response = await deptTreeSelect();
|
||||
const treeNodes = normalizeDeptTreeNodes(response);
|
||||
setDeptTree(treeNodes);
|
||||
setExpandedDeptKeys(collectDeptKeys(treeNodes));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch department tree for user project page:', error);
|
||||
setDeptTree([]);
|
||||
setExpandedDeptKeys([]);
|
||||
} finally {
|
||||
setDeptLoading(false);
|
||||
}
|
||||
};
|
||||
void fetchDeptTree();
|
||||
}, [userModalOpen]);
|
||||
if (skipNextProjectFetchRef.current) {
|
||||
skipNextProjectFetchRef.current = false;
|
||||
return;
|
||||
}
|
||||
void fetchUserProject();
|
||||
// fetchUserProject is intentionally excluded to avoid recreating effect on every render.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dateRange, selectedUserId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userModalOpen) {
|
||||
|
|
@ -233,7 +350,7 @@ const UserProjectPage = () => {
|
|||
setUserLoading(true);
|
||||
try {
|
||||
const normalizedKeyword = deferredUserKeyword.trim();
|
||||
const response = await listUser({
|
||||
const response = await fetchUserListRequest({
|
||||
pageNum: userPageNum,
|
||||
pageSize: normalizedKeyword ? 1000 : userPageSize,
|
||||
deptId: selectedDeptId || undefined,
|
||||
|
|
@ -251,7 +368,13 @@ const UserProjectPage = () => {
|
|||
setUserLoading(false);
|
||||
}
|
||||
};
|
||||
void fetchUserList();
|
||||
|
||||
if (deptTreeLoadedRef.current) {
|
||||
void fetchUserList();
|
||||
return;
|
||||
}
|
||||
|
||||
void Promise.all([fetchDeptTree(), fetchUserList()]);
|
||||
}, [deferredUserKeyword, selectedDeptId, userModalOpen, userPageNum, userPageSize]);
|
||||
|
||||
const pendingUser = useMemo(
|
||||
|
|
@ -268,11 +391,19 @@ const UserProjectPage = () => {
|
|||
};
|
||||
|
||||
const applySelectedUser = (user?: UserRow | null) => {
|
||||
skipNextProjectFetchRef.current = true;
|
||||
setSelectedUser(user ?? null);
|
||||
setSelectedUserId(user?.userId ?? '');
|
||||
setSelectedUserName(getUserDisplayName(user));
|
||||
setPendingUserId(user?.userId ?? '');
|
||||
setUserModalOpen(false);
|
||||
|
||||
if (user?.userId !== undefined && user?.userId !== null && user.userId !== '') {
|
||||
void fetchUserProject(user.userId);
|
||||
return;
|
||||
}
|
||||
|
||||
setExecutionData([]);
|
||||
};
|
||||
|
||||
const dynamicColumns = useMemo<ColumnsType<ProjectExecutionRow>>(() => {
|
||||
|
|
@ -283,19 +414,29 @@ const UserProjectPage = () => {
|
|||
key: 'projectName',
|
||||
fixed: 'left',
|
||||
width: 180,
|
||||
render: (value: unknown, row) => (
|
||||
canViewProjectDetail ? (
|
||||
ellipsis: { showTitle: false },
|
||||
render: (value: unknown, row) => {
|
||||
const projectName = String(value ?? '-');
|
||||
const projectLabel = (
|
||||
<span className="user-project-link-text" title={projectName}>
|
||||
{projectName}
|
||||
</span>
|
||||
);
|
||||
|
||||
return canViewProjectDetail ? (
|
||||
<Button
|
||||
type="link"
|
||||
className="user-project-link"
|
||||
onClick={() => navigate(`/project/detail?id=${String(row.projectId ?? '')}`)}
|
||||
>
|
||||
{String(value ?? '-')}
|
||||
{projectLabel}
|
||||
</Button>
|
||||
) : (
|
||||
<span className="user-project-link is-disabled permission-link-disabled">{String(value ?? '-')}</span>
|
||||
)
|
||||
),
|
||||
<span className="user-project-link is-disabled permission-link-disabled" title={projectName}>
|
||||
{projectLabel}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '统计工时\n(天)',
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Empty, Spin, Table, Tabs, Tag, Typography, message } from 'antd';
|
||||
import { Alert, Empty, Spin, Table, Tabs, Tag, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { getTaskScoreDetail } from '@/api/appraisal';
|
||||
import { notify } from '@/utils/notify';
|
||||
import '@/pages/workAppraisal/appraisal-detail.css';
|
||||
import './user-score.css';
|
||||
|
||||
|
|
@ -190,7 +191,7 @@ const UserScoreDetailPage = () => {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user score detail:', error);
|
||||
message.error('获取绩效详情失败');
|
||||
notify.error('获取绩效详情失败');
|
||||
setSelfDataset({ groups: [], examineTask: {}, examineUser: {} });
|
||||
setManageDataset({ groups: [], examineTask: {}, examineUser: {} });
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Empty, Select, Spin, Table, Tag, Tree, message } from 'antd';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { App, Button, Empty, Select, Spin, Table, Tag, Tree } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
|
|
@ -53,6 +53,20 @@ interface UserRow {
|
|||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;
|
||||
const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
type TaskListRequestParams = { pageNum: number; pageSize: number };
|
||||
type TaskUserListRequestParams = {
|
||||
isAsc: string;
|
||||
sortFiled: string;
|
||||
taskId: string;
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
};
|
||||
type DeptUserRequestParams = { pageNum: number; pageSize: number; deptId: string };
|
||||
|
||||
const inflightTaskListRequests = new Map<string, Promise<unknown>>();
|
||||
const inflightTaskUserListRequests = new Map<string, Promise<unknown>>();
|
||||
const inflightDeptTreeRequests = new Map<string, Promise<unknown>>();
|
||||
const inflightDeptUserRequests = new Map<string, Promise<unknown>>();
|
||||
|
||||
const normalizeResponseData = (response: unknown) =>
|
||||
isObject(response) && response.data !== undefined ? response.data : response;
|
||||
|
|
@ -127,6 +141,64 @@ const normalizeUserRows = (payload: unknown): UserRow[] => {
|
|||
return rows.filter(isObject) as UserRow[];
|
||||
};
|
||||
|
||||
const serializeRequestParams = (params: Record<string, unknown>) => JSON.stringify(params);
|
||||
|
||||
const fetchTaskListRequest = async (params: TaskListRequestParams) => {
|
||||
const requestKey = serializeRequestParams(params);
|
||||
const existingRequest = inflightTaskListRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getTaskList(params).finally(() => {
|
||||
inflightTaskListRequests.delete(requestKey);
|
||||
});
|
||||
inflightTaskListRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchTaskUserListRequest = async (params: TaskUserListRequestParams) => {
|
||||
const requestKey = serializeRequestParams(params);
|
||||
const existingRequest = inflightTaskUserListRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getTaskUserList(params).finally(() => {
|
||||
inflightTaskUserListRequests.delete(requestKey);
|
||||
});
|
||||
inflightTaskUserListRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchDeptTreeRequest = async () => {
|
||||
const requestKey = 'deptTree';
|
||||
const existingRequest = inflightDeptTreeRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = deptTreeSelect().finally(() => {
|
||||
inflightDeptTreeRequests.delete(requestKey);
|
||||
});
|
||||
inflightDeptTreeRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchDeptUserRequest = async (params: DeptUserRequestParams) => {
|
||||
const requestKey = serializeRequestParams(params);
|
||||
const existingRequest = inflightDeptUserRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = listUser(params).finally(() => {
|
||||
inflightDeptUserRequests.delete(requestKey);
|
||||
});
|
||||
inflightDeptUserRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const getStatusMeta = (row: UserScoreRow) => {
|
||||
const selfStatus = String(row.examineStatusSelf ?? '').trim();
|
||||
const manageStatus = String(row.examineStatus ?? row.status ?? '').trim();
|
||||
|
|
@ -143,6 +215,7 @@ const getStatusMeta = (row: UserScoreRow) => {
|
|||
const getScoreValue = (row: UserScoreRow) => row.score ?? row.manageScore ?? row.selfScore ?? '-';
|
||||
|
||||
const UserScorePage = () => {
|
||||
const { message } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
|
@ -163,29 +236,41 @@ const UserScorePage = () => {
|
|||
const [deptUserIds, setDeptUserIds] = useState<Set<string>>(new Set());
|
||||
const [pageNum, setPageNum] = useState(routePageNum);
|
||||
const [pageSize, setPageSize] = useState(routePageSize);
|
||||
const deptUserCacheRef = useRef<Map<string, Set<string>>>(new Map());
|
||||
const canViewScoreDetail = canAccessPath('/projectBank/userScoreDetail');
|
||||
|
||||
useEffect(() => {
|
||||
const loadBaseData = async () => {
|
||||
setDeptLoading(true);
|
||||
try {
|
||||
const [taskResponse, deptResponse] = await Promise.all([
|
||||
getTaskList({ pageNum: 1, pageSize: 100000 }),
|
||||
deptTreeSelect(),
|
||||
const [taskResult, deptResult] = await Promise.allSettled([
|
||||
fetchTaskListRequest({ pageNum: 1, pageSize: 100000 }),
|
||||
fetchDeptTreeRequest(),
|
||||
]);
|
||||
const tasks = normalizeTaskRows(taskResponse).sort((left, right) =>
|
||||
String(right.endTime ?? right.createTime ?? '').localeCompare(String(left.endTime ?? left.createTime ?? '')),
|
||||
);
|
||||
const treeNodes = normalizeDeptTreeNodes(deptResponse);
|
||||
setTaskList(tasks);
|
||||
setDeptTree(treeNodes);
|
||||
setExpandedDeptKeys(collectDeptKeys(treeNodes));
|
||||
if (!routeTaskId && tasks[0]?.id !== undefined) {
|
||||
setSelectedTaskId(String(tasks[0].id));
|
||||
|
||||
if (taskResult.status === 'fulfilled') {
|
||||
const tasks = normalizeTaskRows(taskResult.value).sort((left, right) =>
|
||||
String(right.endTime ?? right.createTime ?? '').localeCompare(String(left.endTime ?? left.createTime ?? '')),
|
||||
);
|
||||
setTaskList(tasks);
|
||||
if (!routeTaskId && tasks[0]?.id !== undefined) {
|
||||
setSelectedTaskId(String(tasks[0].id));
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to fetch task list for user score page:', taskResult.reason);
|
||||
}
|
||||
|
||||
if (deptResult.status === 'fulfilled') {
|
||||
const treeNodes = normalizeDeptTreeNodes(deptResult.value);
|
||||
setDeptTree(treeNodes);
|
||||
setExpandedDeptKeys(collectDeptKeys(treeNodes));
|
||||
} else {
|
||||
console.error('Failed to fetch department tree for user score page:', deptResult.reason);
|
||||
}
|
||||
|
||||
if (taskResult.status === 'rejected' && deptResult.status === 'rejected') {
|
||||
message.error('获取人员绩效基础数据失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch base data for user score page:', error);
|
||||
message.error('获取人员绩效基础数据失败');
|
||||
} finally {
|
||||
setDeptLoading(false);
|
||||
}
|
||||
|
|
@ -201,7 +286,7 @@ const UserScorePage = () => {
|
|||
setLoading(true);
|
||||
try {
|
||||
const currentTask = taskList.find((task) => String(task.id ?? '') === selectedTaskId);
|
||||
const response = await getTaskUserList({
|
||||
const response = await fetchTaskUserListRequest({
|
||||
isAsc: 'desc',
|
||||
sortFiled: 'all',
|
||||
taskId: selectedTaskId,
|
||||
|
|
@ -234,14 +319,21 @@ const UserScorePage = () => {
|
|||
return;
|
||||
}
|
||||
const loadDeptUsers = async () => {
|
||||
const cached = deptUserCacheRef.current.get(selectedDeptId);
|
||||
if (cached) {
|
||||
setDeptUserIds(cached);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await listUser({
|
||||
const response = await fetchDeptUserRequest({
|
||||
pageNum: 1,
|
||||
pageSize: 1000,
|
||||
deptId: selectedDeptId,
|
||||
});
|
||||
const rows = normalizeUserRows(response);
|
||||
setDeptUserIds(new Set(rows.map((row) => String(row.userId ?? '')).filter(Boolean)));
|
||||
const nextIds = new Set(rows.map((row) => String(row.userId ?? '')).filter(Boolean));
|
||||
deptUserCacheRef.current.set(selectedDeptId, nextIds);
|
||||
setDeptUserIds(nextIds);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch department users for user score page:', error);
|
||||
setDeptUserIds(new Set());
|
||||
|
|
|
|||
|
|
@ -75,6 +75,11 @@
|
|||
}
|
||||
|
||||
.user-project-link.ant-btn-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -82,6 +87,9 @@
|
|||
.user-project-link.is-disabled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
color: #98a2b3;
|
||||
|
|
@ -90,6 +98,14 @@
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.user-project-link-text {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.user-select-modal {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Table, Form, Input, Select, Button, Modal, message, Space, Tag, DatePicker, Popconfirm, Radio
|
||||
App, Table, Form, Input, Select, Button, Modal, Space, Tag, DatePicker, Popconfirm, Radio
|
||||
} from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import {
|
||||
|
|
@ -46,6 +46,7 @@ type ConfigRecord = {
|
|||
};
|
||||
|
||||
const ConfigPage: React.FC = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const { canAccessPath } = usePermission();
|
||||
const canOperateConfig = canAccessPath('/system/config');
|
||||
const [queryForm] = Form.useForm();
|
||||
|
|
@ -152,7 +153,7 @@ const ConfigPage: React.FC = () => {
|
|||
message.warning('请选择要删除的参数');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: `是否确认删除参数编号为"${configIds.join(',')}"的数据项?`,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Table, Form, Input, Select, Button, Modal, message, Space, Tag, InputNumber, TreeSelect, Popconfirm, Radio
|
||||
App, Table, Form, Input, Select, Button, Modal, Space, Tag, InputNumber, TreeSelect, Popconfirm, Radio
|
||||
} from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import {
|
||||
|
|
@ -20,6 +20,7 @@ const sysNormalDisableDict = [
|
|||
];
|
||||
|
||||
const DeptPage: React.FC = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const [queryForm] = Form.useForm();
|
||||
const [deptForm] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -174,7 +175,7 @@ const DeptPage: React.FC = () => {
|
|||
};
|
||||
|
||||
const handleDelete = (row: any) => {
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: `是否确认删除名称为"${row.deptName}"的数据项?`,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Table, Form, Input, Select, Button, Modal, message, Space, Tag, DatePicker, Popconfirm, Radio
|
||||
App, Table, Form, Input, Select, Button, Modal, Space, Tag, DatePicker, Popconfirm, Radio
|
||||
} from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import {
|
||||
|
|
@ -49,6 +49,7 @@ type DictTypeRecord = {
|
|||
};
|
||||
|
||||
const DictPage: React.FC = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const { canAccessPath } = usePermission();
|
||||
const [queryForm] = Form.useForm();
|
||||
const [dictForm] = Form.useForm();
|
||||
|
|
@ -154,7 +155,7 @@ const DictPage: React.FC = () => {
|
|||
message.warning('请选择要删除的字典类型');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: `是否确认删除字典编号为"${dictIds.join(',')}"的数据项?`,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { Key } from 'react';
|
||||
import {
|
||||
App,
|
||||
Table,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Button,
|
||||
Modal,
|
||||
message,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
|
|
@ -108,6 +108,7 @@ const getStatusTag = (status?: string) => {
|
|||
};
|
||||
|
||||
const UserPage = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const [queryForm] = Form.useForm<UserQueryParams>();
|
||||
const [userForm] = Form.useForm<UserFormValues>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -197,7 +198,7 @@ const UserPage = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认删除',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: `是否确认删除用户编号为"${ids.join(',')}"的数据项?`,
|
||||
|
|
@ -224,7 +225,7 @@ const UserPage = () => {
|
|||
const nextStatus = originalStatus === '0' ? '1' : '0';
|
||||
const actionText = nextStatus === '0' ? '启用' : '停用';
|
||||
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认操作',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: `确认要${actionText}用户"${record.userName ?? ''}"吗?`,
|
||||
|
|
@ -257,7 +258,7 @@ const UserPage = () => {
|
|||
}
|
||||
const userId = record.userId;
|
||||
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '重置密码',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: `是否确认重置用户"${record.userName ?? ''}"的密码?`,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
App,
|
||||
Alert,
|
||||
Button,
|
||||
Empty,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Space,
|
||||
Spin,
|
||||
|
|
@ -178,6 +178,7 @@ const ScoreBar = ({ value, editable, onChange }: ScoreBarProps) => {
|
|||
};
|
||||
|
||||
const AppraisalDetailPage = () => {
|
||||
const { message, modal } = App.useApp();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { canAccessPath } = usePermission();
|
||||
|
|
@ -475,7 +476,7 @@ const AppraisalDetailPage = () => {
|
|||
};
|
||||
|
||||
if (submitStatus === 1) {
|
||||
Modal.confirm({
|
||||
modal.confirm({
|
||||
title: '确认提交绩效评分',
|
||||
content: '提交后将无法修改,该操作不可逆,请确认后再试',
|
||||
okText: '确定',
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, Row, Col, message, Tabs, Tag, Empty } from 'antd';
|
||||
import { Card, Row, Col, Tabs, Tag, Empty } from 'antd';
|
||||
import { getTaskListSelf } from '../../api/appraisal';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import { notify } from '@/utils/notify';
|
||||
import '@/styles/permission-link.css';
|
||||
import './appraisal.css'; // Assuming a shared CSS for appraisal pages
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ const AppraisalManagerPage: React.FC = () => {
|
|||
setTaskList(response.data || {});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch task list:', error);
|
||||
message.error('获取考核任务列表失败');
|
||||
notify.error('获取考核任务列表失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ const AppraisalManagerPage: React.FC = () => {
|
|||
return;
|
||||
}
|
||||
if (!task.taskEditFlag) {
|
||||
message.warning('分数正在计算中,请等待计算完成');
|
||||
notify.warning('分数正在计算中,请等待计算完成');
|
||||
return;
|
||||
}
|
||||
navigate(`/workAppraisal/managerUser?taskId=${task.id}&isEdit=${isEdit}`);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Table, Form, Input, Select, Button, message, Space, Tag } from 'antd';
|
||||
import { Table, Form, Input, Select, Button, Space, Tag } from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { getTaskUserList } from '../../api/appraisal';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import Permission from '@/components/Permission';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import { notify } from '@/utils/notify';
|
||||
import ReadonlyAction from '@/components/Permission/ReadonlyAction';
|
||||
|
||||
const AppraisalManagerUserPage: React.FC = () => {
|
||||
|
|
@ -46,7 +47,7 @@ const AppraisalManagerUserPage: React.FC = () => {
|
|||
setTotal(response.total ?? 0);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch task user list:', error);
|
||||
message.error('获取考核用户列表失败');
|
||||
notify.error('获取考核用户列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Collapse, Empty, Form, Input, Select, Spin, message } from 'antd';
|
||||
import { Alert, Collapse, Empty, Form, Input, Select, Spin } from 'antd';
|
||||
import type { CollapseProps } from 'antd';
|
||||
import { getTaskModelSet } from '@/api/appraisal';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { notify } from '@/utils/notify';
|
||||
import './appraisal-module-detail.css';
|
||||
|
||||
interface ScoreConfigItem {
|
||||
|
|
@ -185,7 +186,7 @@ const AppraisalModuleDetailPage = () => {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch appraisal module detail:', error);
|
||||
message.error('获取考核看板详情失败');
|
||||
notify.error('获取考核看板详情失败');
|
||||
setScoreList([]);
|
||||
setActiveGroupTitle('');
|
||||
setSelectedCategoryKey('');
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Empty, message, Spin, Tabs, Tag } from 'antd';
|
||||
import { Button, Empty, Spin, Tabs, Tag } from 'antd';
|
||||
import { CarryOutOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getTaskListSelf } from '@/api/appraisal';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import { notify } from '@/utils/notify';
|
||||
import '@/styles/permission-link.css';
|
||||
import './manager.css';
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ const ManagerPage = () => {
|
|||
setTaskBuckets(normalizeTaskBuckets(response));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch appraisal tasks:', error);
|
||||
message.error('获取考核任务失败');
|
||||
notify.error('获取考核任务失败');
|
||||
setTaskBuckets(EMPTY_BUCKETS);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -136,13 +137,13 @@ const ManagerPage = () => {
|
|||
return;
|
||||
}
|
||||
if (!isTaskEditable(task.taskEditFlag)) {
|
||||
message.warning('分数正在计算中,请等待计算完成');
|
||||
notify.warning('分数正在计算中,请等待计算完成');
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = task.id;
|
||||
if (taskId === undefined || taskId === null) {
|
||||
message.warning('任务ID缺失,无法打开详情');
|
||||
notify.warning('任务ID缺失,无法打开详情');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Empty, message, Spin, Tabs, Tag } from 'antd';
|
||||
import { Button, Empty, Spin, Tabs, Tag } from 'antd';
|
||||
import { CarryOutOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getTaskListSelfNormal } from '@/api/appraisal';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
import { notify } from '@/utils/notify';
|
||||
import '@/styles/permission-link.css';
|
||||
import './manager.css';
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ const NormalWorkerPage = () => {
|
|||
setTaskBuckets(normalizeTaskBuckets(response));
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch normal worker tasks:', error);
|
||||
message.error('获取考核评分任务失败');
|
||||
notify.error('获取考核评分任务失败');
|
||||
setTaskBuckets(EMPTY_BUCKETS);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -136,12 +137,12 @@ const NormalWorkerPage = () => {
|
|||
return;
|
||||
}
|
||||
if (!isTaskEditable(task.taskEditFlag)) {
|
||||
message.warning('分数正在计算中,请等待计算完成');
|
||||
notify.warning('分数正在计算中,请等待计算完成');
|
||||
return;
|
||||
}
|
||||
|
||||
if (task.id === undefined || task.id === null) {
|
||||
message.warning('任务ID缺失,无法进入评分页');
|
||||
notify.warning('任务ID缺失,无法进入评分页');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
|
|
@ -391,6 +391,7 @@ const TaskSetPage = () => {
|
|||
const [scoreList, setScoreList] = useState<ScoreGroup[]>([]);
|
||||
const [activeCollapseKey, setActiveCollapseKey] = useState('');
|
||||
const [selectedSubItemKey, setSelectedSubItemKey] = useState('');
|
||||
const deptTreeLoadedRef = useRef(false);
|
||||
const canAddTask = canAccessPath('/workAppraisal/taskSet');
|
||||
const canEditTask = canAccessPath('/workAppraisal/taskSet');
|
||||
const canRemoveTask = canAccessPath('/workAppraisal/taskSet');
|
||||
|
|
@ -425,10 +426,14 @@ const TaskSetPage = () => {
|
|||
}, [getList]);
|
||||
|
||||
const loadDeptTree = useCallback(async () => {
|
||||
if (deptTreeLoadedRef.current) {
|
||||
return;
|
||||
}
|
||||
setDeptLoading(true);
|
||||
try {
|
||||
const response = await deptTreeSelect();
|
||||
setDeptTree(normalizeDeptTreeNodes(response));
|
||||
deptTreeLoadedRef.current = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to load department tree:', error);
|
||||
message.error('获取部门树失败');
|
||||
|
|
@ -465,15 +470,17 @@ const TaskSetPage = () => {
|
|||
}, [selectedDeptId, userModalOpen, userNameKeyword, userPageNum, userPageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userModalOpen || deptTree.length > 0) {
|
||||
if (!userModalOpen) {
|
||||
return;
|
||||
}
|
||||
void loadDeptTree();
|
||||
}, [deptTree.length, loadDeptTree, userModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadUserList();
|
||||
}, [loadUserList]);
|
||||
if (deptTreeLoadedRef.current) {
|
||||
void loadUserList();
|
||||
return;
|
||||
}
|
||||
|
||||
void Promise.all([loadDeptTree(), loadUserList()]);
|
||||
}, [loadDeptTree, loadUserList, userModalOpen]);
|
||||
|
||||
const totalWeight = useMemo(
|
||||
() => scoreList.reduce((sum, group) => sum + toNumber(group.weight, 0), 0),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
DatePicker,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Popover,
|
||||
|
|
@ -39,7 +39,6 @@ import {
|
|||
userProject,
|
||||
} from '@/api/worklog';
|
||||
import { listProject } from '@/api/project';
|
||||
import { getUserProfile } from '@/api/user';
|
||||
import { TokenKey } from '@/utils/auth';
|
||||
import PageBackButton from '@/components/PageBackButton';
|
||||
import { usePermission } from '@/contexts/PermissionContext';
|
||||
|
|
@ -50,12 +49,6 @@ const { TextArea } = Input;
|
|||
const DAY_VALUE_FORMAT = 'YYYY-MM-DD 00:00:00';
|
||||
const FILE_UPLOAD_URL = '/api/common/upload';
|
||||
|
||||
interface UserInfo {
|
||||
userId?: string | number;
|
||||
userName?: string;
|
||||
nickName?: string;
|
||||
}
|
||||
|
||||
interface CalendarLogItem {
|
||||
date?: string;
|
||||
state?: number;
|
||||
|
|
@ -196,7 +189,82 @@ const buildWorkLogPayload = (row: WorkLogRow): Record<string, unknown> => ({
|
|||
fileList: normalizeFileList(row.fileList),
|
||||
});
|
||||
|
||||
let inflightFallbackProjectListRequest: Promise<unknown> | null = null;
|
||||
const inflightWorklogUserProjectRequests = new Map<string, Promise<unknown>>();
|
||||
const inflightCalendarRequests = new Map<string, Promise<unknown>>();
|
||||
const inflightDayListRequests = new Map<string, Promise<unknown>>();
|
||||
const inflightDayRemainingRequests = new Map<string, Promise<unknown>>();
|
||||
|
||||
const fetchUserProjectListRequest = async (userId: string) => {
|
||||
const requestKey = userId;
|
||||
const existingRequest = inflightWorklogUserProjectRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = userProject(userId).finally(() => {
|
||||
inflightWorklogUserProjectRequests.delete(requestKey);
|
||||
});
|
||||
inflightWorklogUserProjectRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchFallbackProjectListRequest = async () => {
|
||||
if (inflightFallbackProjectListRequest) {
|
||||
return inflightFallbackProjectListRequest;
|
||||
}
|
||||
|
||||
const requestPromise = listProject({ pageNum: 1, pageSize: 10000 }).finally(() => {
|
||||
inflightFallbackProjectListRequest = null;
|
||||
});
|
||||
inflightFallbackProjectListRequest = requestPromise;
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchCalendarRequest = async (params: Record<string, unknown>) => {
|
||||
const requestKey = JSON.stringify(params);
|
||||
const existingRequest = inflightCalendarRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getLogData(params).finally(() => {
|
||||
inflightCalendarRequests.delete(requestKey);
|
||||
});
|
||||
inflightCalendarRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchDayListRequest = async (params: Record<string, unknown>) => {
|
||||
const requestKey = JSON.stringify(params);
|
||||
const existingRequest = inflightDayListRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getLogDataDetail(params).finally(() => {
|
||||
inflightDayListRequests.delete(requestKey);
|
||||
});
|
||||
inflightDayListRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const fetchDayRemainingRequest = async (params: Record<string, unknown>) => {
|
||||
const requestKey = JSON.stringify(params);
|
||||
const existingRequest = inflightDayRemainingRequests.get(requestKey);
|
||||
if (existingRequest) {
|
||||
return existingRequest;
|
||||
}
|
||||
|
||||
const requestPromise = getDayTime(params).finally(() => {
|
||||
inflightDayRemainingRequests.delete(requestKey);
|
||||
});
|
||||
inflightDayRemainingRequests.set(requestKey, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
const WorkLogPage = () => {
|
||||
const { message } = App.useApp();
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryUserId = searchParams.get('userId') ?? '';
|
||||
const queryProjectId = searchParams.get('projectId') ?? '';
|
||||
|
|
@ -207,7 +275,6 @@ const WorkLogPage = () => {
|
|||
const [loading, setLoading] = useState(false);
|
||||
const [monthLoading, setMonthLoading] = useState(false);
|
||||
|
||||
const [currentUser, setCurrentUser] = useState<UserInfo>({});
|
||||
const [projectList, setProjectList] = useState<ProjectRow[]>([]);
|
||||
const [calendarList, setCalendarList] = useState<CalendarLogItem[]>([]);
|
||||
|
||||
|
|
@ -228,7 +295,13 @@ const WorkLogPage = () => {
|
|||
|
||||
const previousContentRef = useRef('');
|
||||
const seedRef = useRef(0);
|
||||
const { hasPermi } = usePermission();
|
||||
const bootstrapLoadedRef = useRef(false);
|
||||
const calendarStripRef = useRef<HTMLDivElement | null>(null);
|
||||
const selectedDayButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const projectListRequestIdRef = useRef(0);
|
||||
const calendarRequestIdRef = useRef(0);
|
||||
const dayListRequestIdRef = useRef(0);
|
||||
const { hasPermi, ready: permissionReady, currentUser } = usePermission();
|
||||
|
||||
const token = Cookies.get(TokenKey) ?? '';
|
||||
|
||||
|
|
@ -295,7 +368,7 @@ const WorkLogPage = () => {
|
|||
const getCurrentDayTime = useCallback(
|
||||
async (mode: 'add' | 'edit', row?: WorkLogRow) => {
|
||||
try {
|
||||
const response = await getDayTime({ loggerDate: selectedDay });
|
||||
const response = await fetchDayRemainingRequest({ loggerDate: selectedDay });
|
||||
const remaining = toNumber(normalizeResponseData(response), 0);
|
||||
|
||||
if (mode === 'add') {
|
||||
|
|
@ -321,7 +394,21 @@ const WorkLogPage = () => {
|
|||
showContent: false,
|
||||
fileList: [],
|
||||
};
|
||||
setTableData((prev) => [...prev, newRow]);
|
||||
setTableData((prev) => {
|
||||
const hasPendingRow = prev.some((item) =>
|
||||
!item.loggerId
|
||||
&& item.edit
|
||||
&& String(item.loggerDate ?? '') === selectedDay
|
||||
&& String(item.userId ?? '') === String(viewUserId)
|
||||
&& String(item.projectId ?? '') === String(queryProjectId ?? ''),
|
||||
);
|
||||
|
||||
if (hasPendingRow) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return [...prev, newRow];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -334,34 +421,19 @@ const WorkLogPage = () => {
|
|||
[computeWorkTimeOptions, queryProjectId, selectedDay, selectedDayDate, viewUserId],
|
||||
);
|
||||
|
||||
const fetchUserInfo = useCallback(async () => {
|
||||
try {
|
||||
const response = await getUserProfile();
|
||||
const payload = normalizeResponseData(response);
|
||||
const user = isObject(payload) && isObject(payload.user) ? payload.user : payload;
|
||||
if (!isObject(user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentUser({
|
||||
userId: user.userId as string | number | undefined,
|
||||
userName: user.userName as string | undefined,
|
||||
nickName: user.nickName as string | undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch current user profile:', error);
|
||||
message.error('获取当前用户信息失败');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchAllProjects = useCallback(async () => {
|
||||
if (!viewUserId) {
|
||||
const fetchAllProjects = useCallback(async (targetUserId?: string) => {
|
||||
const effectiveUserId = String(targetUserId ?? viewUserId).trim();
|
||||
const requestId = ++projectListRequestIdRef.current;
|
||||
if (!effectiveUserId) {
|
||||
setProjectList([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await userProject(viewUserId);
|
||||
const response = await fetchUserProjectListRequest(effectiveUserId);
|
||||
if (requestId !== projectListRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
const payload = normalizeResponseData(response);
|
||||
const rows = isObject(payload) && Array.isArray(payload.rows) ? payload.rows : Array.isArray(payload) ? payload : [];
|
||||
setProjectList(normalizeProjectRows(rows));
|
||||
|
|
@ -371,44 +443,63 @@ const WorkLogPage = () => {
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await listProject({ pageNum: 1, pageSize: 10000 });
|
||||
const response = await fetchFallbackProjectListRequest();
|
||||
if (requestId !== projectListRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
const payload = normalizeResponseData(response);
|
||||
const rows = isObject(payload) && Array.isArray(payload.rows) ? payload.rows : Array.isArray(payload) ? payload : [];
|
||||
setProjectList(normalizeProjectRows(rows));
|
||||
} catch (error) {
|
||||
if (requestId !== projectListRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to fetch all projects:', error);
|
||||
message.error('获取项目列表失败');
|
||||
setProjectList([]);
|
||||
}
|
||||
}, [viewUserId]);
|
||||
|
||||
const fetchCalendarList = useCallback(async () => {
|
||||
if (!viewUserId) {
|
||||
const fetchCalendarList = useCallback(async (targetUserId?: string) => {
|
||||
const effectiveUserId = String(targetUserId ?? viewUserId).trim();
|
||||
const requestId = ++calendarRequestIdRef.current;
|
||||
if (!effectiveUserId) {
|
||||
setCalendarList([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setMonthLoading(true);
|
||||
try {
|
||||
const response = await getLogData({
|
||||
const requestParams = {
|
||||
startDate: selectedMonth.startOf('month').format('YYYY-MM-DD 00:00:00'),
|
||||
endDate: selectedMonth.endOf('month').format('YYYY-MM-DD 23:59:59'),
|
||||
userId: viewUserId,
|
||||
});
|
||||
userId: effectiveUserId,
|
||||
};
|
||||
const response = await fetchCalendarRequest(requestParams);
|
||||
if (requestId !== calendarRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = normalizeResponseData(response);
|
||||
setCalendarList(Array.isArray(data) ? (data as CalendarLogItem[]) : []);
|
||||
} catch (error) {
|
||||
if (requestId !== calendarRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to fetch month log list:', error);
|
||||
message.error('获取月度日志失败');
|
||||
setCalendarList([]);
|
||||
} finally {
|
||||
setMonthLoading(false);
|
||||
if (requestId === calendarRequestIdRef.current) {
|
||||
setMonthLoading(false);
|
||||
}
|
||||
}
|
||||
}, [selectedMonth, viewUserId]);
|
||||
|
||||
const fetchDayList = useCallback(async () => {
|
||||
if (!viewUserId) {
|
||||
const fetchDayList = useCallback(async (targetUserId?: string) => {
|
||||
const effectiveUserId = String(targetUserId ?? viewUserId).trim();
|
||||
const requestId = ++dayListRequestIdRef.current;
|
||||
if (!effectiveUserId) {
|
||||
setTableData([]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -416,7 +507,7 @@ const WorkLogPage = () => {
|
|||
setLoading(true);
|
||||
try {
|
||||
const params: Record<string, unknown> = {
|
||||
userId: viewUserId,
|
||||
userId: effectiveUserId,
|
||||
loggerDate: selectedDay,
|
||||
};
|
||||
|
||||
|
|
@ -424,7 +515,10 @@ const WorkLogPage = () => {
|
|||
params.projectId = queryProjectId;
|
||||
}
|
||||
|
||||
const response = await getLogDataDetail(params);
|
||||
const response = await fetchDayListRequest(params);
|
||||
if (requestId !== dayListRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
const payload = normalizeResponseData(response);
|
||||
const rows = Array.isArray(payload)
|
||||
? payload
|
||||
|
|
@ -451,32 +545,84 @@ const WorkLogPage = () => {
|
|||
await getCurrentDayTime('add');
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestId !== dayListRequestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
console.error('Failed to fetch day log list:', error);
|
||||
message.error('获取当日日志失败');
|
||||
setTableData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (requestId === dayListRequestIdRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [disableTable, getCurrentDayTime, queryProjectId, selectedDay, viewUserId]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchUserInfo();
|
||||
}, [fetchUserInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchAllProjects();
|
||||
}, [fetchAllProjects]);
|
||||
let cancelled = false;
|
||||
bootstrapLoadedRef.current = false;
|
||||
|
||||
const bootstrap = async () => {
|
||||
if (!permissionReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const directUserId = queryUserId.trim();
|
||||
if (directUserId) {
|
||||
await Promise.all([
|
||||
fetchAllProjects(directUserId),
|
||||
fetchCalendarList(directUserId),
|
||||
fetchDayList(directUserId),
|
||||
]);
|
||||
if (!cancelled) {
|
||||
bootstrapLoadedRef.current = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedUserId = String(currentUser?.userId ?? '').trim();
|
||||
if (!resolvedUserId) {
|
||||
if (!cancelled) {
|
||||
bootstrapLoadedRef.current = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
fetchAllProjects(resolvedUserId),
|
||||
fetchCalendarList(resolvedUserId),
|
||||
fetchDayList(resolvedUserId),
|
||||
]);
|
||||
|
||||
if (!cancelled) {
|
||||
bootstrapLoadedRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
void bootstrap();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
bootstrapLoadedRef.current = false;
|
||||
};
|
||||
}, [currentUser?.userId, fetchAllProjects, fetchCalendarList, fetchDayList, permissionReady, queryUserId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!bootstrapLoadedRef.current) {
|
||||
return;
|
||||
}
|
||||
void fetchCalendarList();
|
||||
}, [fetchCalendarList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!bootstrapLoadedRef.current) {
|
||||
return;
|
||||
}
|
||||
void fetchDayList();
|
||||
}, [fetchDayList]);
|
||||
|
||||
const projectListFilter = useMemo(() => {
|
||||
const matched = projectList.filter((item) => {
|
||||
return projectList.filter((item) => {
|
||||
const start = pickProjectBoundaryDate(item, ['startDate', 'beginDate', 'planStartDate']);
|
||||
const end = pickProjectBoundaryDate(item, ['endDate', 'finishDate', 'planEndDate']);
|
||||
if (!start && !end) {
|
||||
|
|
@ -490,8 +636,6 @@ const WorkLogPage = () => {
|
|||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return matched.length > 0 ? matched : projectList;
|
||||
}, [projectList, selectedDayDate]);
|
||||
|
||||
const totalWorkTime = useMemo(() => {
|
||||
|
|
@ -742,8 +886,7 @@ const WorkLogPage = () => {
|
|||
}
|
||||
|
||||
message.success('操作成功');
|
||||
await fetchDayList();
|
||||
await fetchCalendarList();
|
||||
await Promise.all([fetchDayList(), fetchCalendarList()]);
|
||||
} catch (error) {
|
||||
console.error('Failed to save work log row:', error);
|
||||
message.error('保存失败');
|
||||
|
|
@ -758,8 +901,7 @@ const WorkLogPage = () => {
|
|||
try {
|
||||
await delLog(row.loggerId);
|
||||
message.success('删除成功');
|
||||
await fetchDayList();
|
||||
await fetchCalendarList();
|
||||
await Promise.all([fetchDayList(), fetchCalendarList()]);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete work log row:', error);
|
||||
message.error('删除失败');
|
||||
|
|
@ -977,6 +1119,25 @@ const WorkLogPage = () => {
|
|||
});
|
||||
}, [monthCellMap, selectedDayOnly, selectedMonth]);
|
||||
|
||||
useEffect(() => {
|
||||
const strip = calendarStripRef.current;
|
||||
const selectedButton = selectedDayButtonRef.current;
|
||||
if (!strip || !selectedButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameId = window.requestAnimationFrame(() => {
|
||||
const targetLeft = selectedButton.offsetLeft - (strip.clientWidth - selectedButton.clientWidth) / 2;
|
||||
const maxScrollLeft = Math.max(strip.scrollWidth - strip.clientWidth, 0);
|
||||
const nextScrollLeft = Math.min(Math.max(targetLeft, 0), maxScrollLeft);
|
||||
strip.scrollTo({ left: nextScrollLeft, behavior: 'auto' });
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(frameId);
|
||||
};
|
||||
}, [monthDayList, selectedDayOnly]);
|
||||
|
||||
const columns: TableColumnsType<WorkLogRow> = [
|
||||
{
|
||||
title: '序号',
|
||||
|
|
@ -1227,7 +1388,7 @@ const WorkLogPage = () => {
|
|||
</div>
|
||||
|
||||
<Spin spinning={monthLoading}>
|
||||
<div className="worklog-calendar-strip">
|
||||
<div ref={calendarStripRef} className="worklog-calendar-strip">
|
||||
{monthDayList.map((item) => {
|
||||
const className = [
|
||||
'worklog-strip-day',
|
||||
|
|
@ -1244,6 +1405,7 @@ const WorkLogPage = () => {
|
|||
key={item.key}
|
||||
type="button"
|
||||
className={className}
|
||||
ref={item.isSelected ? selectedDayButtonRef : null}
|
||||
onClick={() => handlePickDay(item.current)}
|
||||
disabled={item.isFuture}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import path from 'path' // Import path module
|
|||
const inferChunkName = (moduleIds: string[]) => {
|
||||
const joined = moduleIds.join('\n')
|
||||
|
||||
if (joined.includes('/node_modules/antd/es/table') || joined.includes('/node_modules/rc-table/')) {
|
||||
if (
|
||||
joined.includes('/node_modules/rc-pagination/') ||
|
||||
joined.includes('/node_modules/rc-table/') ||
|
||||
joined.includes('/node_modules/rc-resize-observer/') ||
|
||||
joined.includes('/node_modules/rc-virtual-list/')
|
||||
) {
|
||||
return 'antd-table'
|
||||
}
|
||||
if (joined.includes('/node_modules/antd/es/input') || joined.includes('/node_modules/rc-input/')) {
|
||||
|
|
@ -26,7 +31,7 @@ const inferChunkName = (moduleIds: string[]) => {
|
|||
if (joined.includes('/node_modules/antd/es/locale') || joined.includes('/node_modules/antd/locale/')) {
|
||||
return 'antd-locale'
|
||||
}
|
||||
if (joined.includes('/node_modules/@ant-design/icons/')) {
|
||||
if (joined.includes('/node_modules/@ant-design/icons/') || joined.includes('/node_modules/@ant-design/icons-svg/')) {
|
||||
return 'antd-icons'
|
||||
}
|
||||
if (joined.includes('/src/components/')) {
|
||||
|
|
@ -63,7 +68,19 @@ export default defineConfig({
|
|||
if (/node_modules\/(react|react-dom|scheduler)\//.test(id)) {
|
||||
return 'react-vendor';
|
||||
}
|
||||
if (/node_modules\/(echarts|echarts-for-react)\//.test(id)) {
|
||||
if (/node_modules\/(@ant-design\/icons|@ant-design\/icons-svg)\//.test(id)) {
|
||||
return 'antd-icons';
|
||||
}
|
||||
if (/node_modules\/(rc-table|rc-pagination|rc-resize-observer|rc-virtual-list)\//.test(id)) {
|
||||
return 'antd-table';
|
||||
}
|
||||
if (/node_modules\/echarts-for-react\//.test(id)) {
|
||||
return 'echarts-react';
|
||||
}
|
||||
if (/node_modules\/zrender\//.test(id)) {
|
||||
return 'zrender-vendor';
|
||||
}
|
||||
if (/node_modules\/echarts\//.test(id)) {
|
||||
return 'echarts-vendor';
|
||||
}
|
||||
if (/node_modules\/(react-router|react-router-dom)\//.test(id)) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue