Commit ca0cbf30 authored by David Yang's avatar David Yang

feat: 完善项目管理工作台与资源统计

parent 70e6803d
......@@ -12,11 +12,14 @@
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^4.2.0",
"umi-request": "^1.4.0",
"umi-request-progress": "^1.1.0"
"umi-request-progress": "^1.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/react": "^18.0.33",
......@@ -7306,6 +7309,15 @@
"node": ">=8.9.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
......@@ -8512,6 +8524,19 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
......@@ -8677,6 +8702,15 @@
"node": ">=6"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
......@@ -8929,6 +8963,18 @@
"node": ">=10"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/create-ecdh": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
......@@ -10055,6 +10101,36 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/echarts": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.1.0"
}
},
"node_modules/echarts-for-react": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz",
"integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"size-sensor": "^1.0.1"
},
"peerDependencies": {
"echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
"react": "^15.0.0 || >=16.0.0"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
......@@ -11384,6 +11460,15 @@
"node": ">= 0.6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
......@@ -18260,6 +18345,12 @@
"integrity": "sha512-9SQdmsyz4HSP+3gs6PJzhkaMEg+6zTlu9oxIghnwUX3eq+ajq4ft5egl0iyR55LAmO/UwvU8NgIWs/ZyQMa6dw==",
"license": "MIT"
},
"node_modules/size-sensor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz",
"integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==",
"license": "ISC"
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
......@@ -18493,6 +18584,18 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stable": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
......@@ -20739,6 +20842,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
......@@ -20823,6 +20944,27 @@
}
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
......@@ -20924,6 +21066,21 @@
"peerDependencies": {
"zod": "^3.18.0"
}
},
"node_modules/zrender": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
}
}
}
......@@ -14,11 +14,14 @@
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.20",
"echarts": "^6.1.0",
"echarts-for-react": "^3.0.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^4.2.0",
"umi-request": "^1.4.0",
"umi-request-progress": "^1.1.0"
"umi-request-progress": "^1.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/react": "^18.0.33",
......
:root {
--pm-brand: #009cd9;
--pm-brand-strong: #007bb8;
--pm-brand: #b42318;
--pm-brand-strong: #991b1b;
--pm-surface: #ffffff;
--pm-bg: #eef5f9;
--pm-bg: #f8fafc;
--pm-sidebar: #0f172a;
--pm-sidebar-soft: #1e293b;
--pm-text: #0f172a;
......@@ -17,7 +17,10 @@ body,
body {
margin: 0;
background: linear-gradient(180deg, #f8fbfd 0%, #eef5f9 100%);
background:
radial-gradient(circle at top left, rgba(251, 191, 188, 0.22), transparent 26%),
radial-gradient(circle at bottom right, rgba(251, 207, 232, 0.2), transparent 30%),
#f8fafc;
color: var(--pm-text);
font-family:
"PingFang SC",
......
export default function EmptyStatePage({ title, description }) {
return (
<section className="relative overflow-hidden rounded-[32px] border border-white/70 bg-white/90 p-8 shadow-[0_30px_80px_rgba(15,23,42,0.08)]">
<div className="absolute -right-20 -top-20 h-56 w-56 rounded-full bg-sky-100 blur-3xl" />
<div className="absolute bottom-0 left-0 h-40 w-40 rounded-full bg-cyan-50 blur-3xl" />
<div className="absolute -right-20 -top-20 h-56 w-56 rounded-full bg-rose-100 blur-3xl" />
<div className="absolute bottom-0 left-0 h-40 w-40 rounded-full bg-amber-50 blur-3xl" />
<div className="relative">
<div className="inline-flex rounded-full border border-sky-200 bg-sky-50 px-3 py-1 text-xs font-semibold tracking-[0.2em] text-sky-700">
<div className="inline-flex rounded-full border border-rose-200 bg-rose-50 px-3 py-1 text-xs font-semibold tracking-[0.2em] text-rose-700">
COMING SOON
</div>
<h2 className="mt-6 text-3xl font-semibold text-slate-900">{title}</h2>
......
import rsmLogo from '@/assets/rsm_logo.svg';
import rsmLogoSmall from '@/assets/rsm_logo_small.svg';
import xyLogo from '@/assets/xy-logo.png';
import xyLogoSmall from '@/assets/xy-logo_small.png';
import { UserContext } from '@/wrapper/Auth';
import {
CalendarDaysIcon,
......@@ -220,7 +220,7 @@ export default function ProjectManagerLayout() {
}, [location.pathname]);
return (
<div className="flex h-screen overflow-hidden bg-zinc-50">
<div className="flex h-screen overflow-hidden bg-[radial-gradient(circle_at_top_left,_rgba(251,191,188,0.22),_transparent_26%),radial-gradient(circle_at_bottom_right,_rgba(251,207,232,0.2),_transparent_30%),#f8fafc]">
<aside
className={clsx(
'relative z-30 hidden shrink-0 flex-col border-r border-zinc-800 bg-zinc-900 p-5 lg:flex',
......@@ -234,13 +234,13 @@ export default function ProjectManagerLayout() {
)}
>
<img
src={sidebarCollapsed ? rsmLogoSmall : rsmLogo}
alt="RSM Logo"
src={sidebarCollapsed ? xyLogoSmall : xyLogo}
alt="XY Logo"
className={clsx(
'shrink-0 object-contain',
sidebarCollapsed
? 'h-9 w-auto max-w-[52px]'
: 'h-5 w-auto max-w-[128px]',
? 'h-8 w-auto max-w-8'
: 'h-8 w-auto max-w-[140px]',
)}
/>
{!sidebarCollapsed ? (
......@@ -302,9 +302,9 @@ export default function ProjectManagerLayout() {
<div className="px-5 pt-6">
<div className="flex items-center gap-2">
<img
src={rsmLogo}
alt="RSM Logo"
className="h-5 w-auto max-w-[128px] shrink-0 object-contain"
src={xyLogo}
alt="XY Logo"
className="h-8 w-auto max-w-[140px] shrink-0 object-contain"
/>
<h1 className="text-base font-semibold tracking-[0.02em] text-white">
{APP_NAME}
......@@ -346,7 +346,7 @@ export default function ProjectManagerLayout() {
<div className="text-sm text-zinc-500">{currentGroupTitle}</div>
</div>
<div className="flex-1 overflow-y-auto rounded-xl border border-zinc-200 bg-white p-4 sm:p-6">
<div className="flex-1 overflow-y-auto rounded-xl border border-zinc-200 bg-white/95 p-4 shadow-[0_20px_50px_rgba(15,23,42,0.06)] sm:p-6">
<Outlet />
</div>
</main>
......
import {
buildDemoTasks,
buildUserMap,
enrichProjectTask,
fetchProjectTasks,
fetchSystemUsersByIds,
loadProjectTaskFieldOptions,
resolveUserReferenceId,
} from './projectTaskService';
export async function loadProjectDashboardData(currentUsername = '') {
try {
const taskRecords = await fetchProjectTasks({});
const [taskRecords, fieldOptions] = await Promise.all([
fetchProjectTasks({}),
loadProjectTaskFieldOptions(),
]);
const userIds = Array.from(
new Set(
taskRecords.flatMap((task) => {
const metadata = task?.metadata || {};
const memberIds = Array.isArray(metadata.member_ids)
? metadata.member_ids
: metadata.member_ids
? [metadata.member_ids]
: [];
const leaderId =
metadata.leader?._id ||
metadata.leader?.id ||
metadata.leader?.username ||
metadata.leader ||
'';
const projectOwnerIds = Array.isArray(metadata.project_owner)
? metadata.project_owner.map(resolveUserReferenceId)
: [resolveUserReferenceId(metadata.project_owner)].filter(Boolean);
const projectCollaboratorIds = Array.isArray(metadata.project_collaborator)
? metadata.project_collaborator.map(resolveUserReferenceId)
: [resolveUserReferenceId(metadata.project_collaborator)].filter(Boolean);
return [...memberIds, leaderId].filter(Boolean);
return [...projectOwnerIds, ...projectCollaboratorIds].filter(Boolean);
}),
),
);
......@@ -42,29 +41,26 @@ export async function loadProjectDashboardData(currentUsername = '') {
return {
tasks,
users,
fieldOptions,
isDemoMode: false,
error: '',
};
}
return {
tasks: buildDemoTasks().map((task) => enrichProjectTask(task, userMap)),
tasks: [],
users,
isDemoMode: true,
error: '当前未读取到项目记录,已展示演示数据,可继续浏览统计看板。',
fieldOptions,
isDemoMode: false,
error: '当前未读取到真实项目记录,请先创建或导入 `pm_project_task` 数据。',
};
} catch (error) {
const fallbackUsers = [
{ id: 'yang_admin', username: 'yang_admin', displayName: 'yang' },
{ id: 'system', username: 'system', displayName: '系统' },
];
const userMap = buildUserMap(fallbackUsers);
return {
tasks: buildDemoTasks().map((task) => enrichProjectTask(task, userMap)),
users: fallbackUsers,
isDemoMode: true,
error: error?.message || '统计数据加载失败,已切换为演示数据。',
tasks: [],
users: [],
fieldOptions: {},
isDemoMode: false,
error: error?.message || '统计数据加载失败,请稍后重试。',
};
}
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment