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