Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
R
risk_case_library
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
risk_case_library
Commits
dd4b6b28
Commit
dd4b6b28
authored
Jun 15, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 应用切换
parent
dd0aab32
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
805 additions
and
128 deletions
+805
-128
manifest.json
public/manifest.json
+2
-2
ApplicationSwitcherDialog.jsx
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
+496
-0
Layout.jsx
src/pages/project_manager_xy/Layout.jsx
+71
-14
audit-material-center.business-flow.md
...project_manager_xy/audit-material-center.business-flow.md
+9
-0
integrity-party.business-flow.md
...pages/project_manager_xy/integrity-party.business-flow.md
+6
-2
index.jsx
src/pages/project_manager_xy/integrity-party/index.jsx
+23
-15
integrityPartyService.js
src/pages/project_manager_xy/integrityPartyService.js
+82
-32
index.jsx
src/pages/project_manager_xy/resource-booking/index.jsx
+17
-23
index.jsx
src/pages/project_manager_xy/resource-stat/index.jsx
+99
-40
No files found.
public/manifest.json
View file @
dd4b6b28
{
{
"name"
:
"
project_manager_new
"
,
"name"
:
"
risk_case_library
"
,
"version"
:
"1.0.
1
"
,
"version"
:
"1.0.
4
"
,
"build"
:
"abc"
,
"build"
:
"abc"
,
"description"
:
"merge master"
,
"description"
:
"merge master"
,
"date"
:
"2025-07-14 19:27:20"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/pages/project_manager_xy/ApplicationSwitcherDialog.jsx
0 → 100644
View file @
dd4b6b28
import
{
getApplications
}
from
'@/services/DataService'
;
import
*
as
Headless
from
'@headlessui/react'
;
import
{
ArrowTopRightOnSquareIcon
,
ExclamationCircleIcon
,
SparklesIcon
,
}
from
'@heroicons/react/24/outline'
;
import
clsx
from
'clsx'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
function
normalizeLabelList
(
items
)
{
if
(
!
Array
.
isArray
(
items
))
{
return
[];
}
return
items
.
map
((
item
)
=>
{
if
(
typeof
item
===
'string'
)
{
return
item
;
}
return
item
?.
display_name
||
item
?.
title
||
item
?.
name
||
''
;
})
.
filter
(
Boolean
);
}
function
resolveApplicationUrl
(
application
)
{
if
(
application
?.
name
)
{
return
`/
${
application
.
name
}
`
;
}
return
'#'
;
}
function
normalizeApplication
(
application
,
fallback
=
{})
{
const
name
=
application
?.
name
||
fallback
?.
name
||
''
;
return
{
name
,
title
:
application
?.
title
||
fallback
?.
title
||
application
?.
name
||
fallback
?.
name
||
'未命名应用'
,
description
:
application
?.
description
||
fallback
?.
description
||
fallback
?.
title
||
application
?.
title
||
'进入对应应用继续处理业务'
,
logo
:
application
?.
logo
||
fallback
?.
logo
||
null
,
url
:
resolveApplicationUrl
({
name
,
}),
access
:
application
?.
access
||
fallback
?.
access
||
''
,
status
:
application
?.
status
||
fallback
?.
status
||
''
,
type
:
application
?.
type
||
fallback
?.
type
||
''
,
order
:
application
?.
order
??
fallback
?.
order
??
0
,
roles
:
normalizeLabelList
(
application
?.
roles
?.
length
?
application
.
roles
:
fallback
?.
roles
,
),
groups
:
normalizeLabelList
(
application
?.
groups
?.
length
?
application
.
groups
:
fallback
?.
groups
,
),
};
}
function
formatAccessLabel
(
access
)
{
if
(
access
===
'public'
)
{
return
'公开'
;
}
if
(
access
===
'protected'
)
{
return
'受限'
;
}
if
(
access
===
'private'
)
{
return
'私有'
;
}
if
(
access
===
'internal'
)
{
return
'内部'
;
}
return
''
;
}
function
mergeApplications
(
fetchedApplications
,
fallbackApplications
)
{
const
fallbackMap
=
new
Map
(
fallbackApplications
.
map
((
application
)
=>
[
application
.
name
,
application
]),
);
const
normalizedFetched
=
fetchedApplications
.
map
((
application
)
=>
normalizeApplication
(
application
,
fallbackMap
.
get
(
application
?.
name
)),
)
.
filter
((
application
)
=>
application
.
name
);
const
mergedMap
=
new
Map
(
normalizedFetched
.
map
((
application
)
=>
[
application
.
name
,
application
]),
);
fallbackApplications
.
forEach
((
application
)
=>
{
if
(
!
application
?.
name
||
mergedMap
.
has
(
application
.
name
))
{
return
;
}
mergedMap
.
set
(
application
.
name
,
normalizeApplication
(
application
,
application
),
);
});
return
Array
.
from
(
mergedMap
.
values
()).
sort
((
left
,
right
)
=>
{
const
orderDifference
=
Number
(
left
.
order
||
0
)
-
Number
(
right
.
order
||
0
);
if
(
orderDifference
!==
0
)
{
return
orderDifference
;
}
return
left
.
title
.
localeCompare
(
right
.
title
,
'zh-CN'
);
});
}
function
buildApplicationSections
(
applications
,
applicationGroups
)
{
if
(
!
Array
.
isArray
(
applicationGroups
)
||
applicationGroups
.
length
===
0
)
{
return
[
{
key
:
'all'
,
label
:
'全部应用'
,
applications
,
},
];
}
const
applicationMap
=
new
Map
(
applications
.
map
((
application
)
=>
[
application
.
name
,
application
]),
);
const
groupedApplicationNames
=
new
Set
();
const
sections
=
applicationGroups
.
map
((
group
,
index
)
=>
{
const
groupApplications
=
Array
.
isArray
(
group
?.
applications
)
?
group
.
applications
.
map
((
name
)
=>
{
const
application
=
applicationMap
.
get
(
name
);
if
(
application
)
{
groupedApplicationNames
.
add
(
name
);
}
return
application
;
})
.
filter
(
Boolean
)
:
[];
return
{
key
:
group
?.
label
||
`group-
${
index
}
`
,
label
:
group
?.
label
||
'应用分组'
,
applications
:
groupApplications
,
};
})
.
filter
((
section
)
=>
section
.
applications
.
length
>
0
);
const
ungroupedApplications
=
applications
.
filter
(
(
application
)
=>
!
groupedApplicationNames
.
has
(
application
.
name
),
);
if
(
ungroupedApplications
.
length
>
0
)
{
sections
.
push
({
key
:
'ungrouped'
,
label
:
'其他应用'
,
applications
:
ungroupedApplications
,
});
}
return
sections
;
}
function
isCurrentApplication
(
application
)
{
if
(
typeof
window
===
'undefined'
)
{
return
false
;
}
const
currentPath
=
window
.
location
.
pathname
;
const
applicationRoot
=
application
?.
name
?
`/
${
application
.
name
}
`
:
''
;
if
(
application
?.
url
?.
startsWith
(
'http'
))
{
try
{
const
url
=
new
URL
(
application
.
url
);
return
url
.
pathname
===
currentPath
;
}
catch
(
error
)
{
return
false
;
}
}
return
(
application
?.
url
===
currentPath
||
(
!!
applicationRoot
&&
(
currentPath
===
applicationRoot
||
currentPath
.
startsWith
(
`
${
applicationRoot
}
/`
)))
);
}
function
ApplicationCard
({
application
})
{
const
current
=
isCurrentApplication
(
application
);
const
target
=
application
?.
url
?.
startsWith
(
'http'
)
?
'_blank'
:
undefined
;
const
accessLabel
=
formatAccessLabel
(
application
.
access
);
const
firstRole
=
application
.
roles
?.[
0
];
const
firstGroup
=
application
.
groups
?.[
0
];
return
(
<
a
href=
{
application
.
url
}
target=
{
target
}
rel=
{
target
?
'noreferrer'
:
undefined
}
className=
{
clsx
(
'group relative min-h-[84px] overflow-hidden rounded-2xl border border-zinc-200/80 bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(248,250,252,0.94))] p-2.5 shadow-[0_10px_24px_rgba(15,23,42,0.06)] transition duration-200 ease-out hover:-translate-y-0.5 hover:border-rose-200 hover:shadow-[0_16px_32px_rgba(244,63,94,0.12)] focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/60'
,
current
&&
'border-rose-300 bg-[linear-gradient(180deg,rgba(255,241,242,0.95),rgba(255,255,255,0.96))]'
,
)
}
>
<
div
className=
"absolute inset-x-0 top-0 h-14 bg-[radial-gradient(circle_at_top_right,rgba(251,113,133,0.14),transparent_50%)] opacity-0 transition duration-200 group-hover:opacity-100"
/>
<
div
className=
"relative flex items-start justify-between gap-3"
>
<
div
className=
"flex min-w-0 items-center gap-2.5"
>
{
application
.
logo
?
(
<
img
src=
{
application
.
logo
}
alt=
{
application
.
title
}
className=
"size-9 rounded-xl border border-white/80 bg-white object-cover shadow-sm"
/>
)
:
(
<
div
className=
"flex size-9 shrink-0 items-center justify-center rounded-xl bg-[linear-gradient(135deg,#b42318,#f43f5e)] text-sm font-semibold text-white shadow-[0_8px_18px_rgba(180,35,24,0.22)]"
>
{
application
.
title
.
slice
(
0
,
1
)
}
</
div
>
)
}
<
div
className=
"min-w-0"
>
<
div
className=
"truncate text-sm font-semibold text-slate-950"
>
{
application
.
title
}
</
div
>
<
div
className=
"mt-0.5 truncate text-[11px] leading-4 text-slate-500"
>
{
application
.
description
}
</
div
>
</
div
>
</
div
>
<
div
className=
"flex shrink-0 items-center gap-2"
>
{
current
?
(
<
span
className=
"rounded-full bg-rose-100 px-2 py-0.5 text-[11px] font-medium text-rose-700"
>
当前
</
span
>
)
:
null
}
<
span
className=
"rounded-full bg-slate-100 p-1.5 text-slate-500 transition group-hover:bg-rose-50 group-hover:text-rose-600"
>
<
ArrowTopRightOnSquareIcon
className=
"size-3.5"
/>
</
span
>
</
div
>
</
div
>
{
accessLabel
||
firstRole
||
firstGroup
?
(
<
div
className=
"relative mt-2 flex flex-wrap gap-1.5"
>
{
accessLabel
?
(
<
span
className=
"rounded-full bg-zinc-100 px-2 py-0.5 text-[11px] font-medium text-zinc-700"
>
{
accessLabel
}
</
span
>
)
:
null
}
{
firstRole
?
(
<
span
className=
"rounded-full bg-slate-100 px-2 py-0.5 text-[11px] font-medium text-slate-700"
>
{
firstRole
}
</
span
>
)
:
null
}
{
firstGroup
?
(
<
span
className=
"rounded-full bg-rose-50 px-2 py-0.5 text-[11px] font-medium text-rose-700"
>
{
firstGroup
}
</
span
>
)
:
null
}
</
div
>
)
:
null
}
</
a
>
);
}
function
LoadingSkeleton
()
{
return
(
<
div
className=
"grid gap-2.5 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-6"
>
{
Array
.
from
({
length
:
6
}).
map
((
_
,
index
)
=>
(
<
div
key=
{
index
}
className=
"min-h-[84px] rounded-2xl border border-zinc-200 bg-white/90 p-2.5 shadow-[0_10px_24px_rgba(15,23,42,0.05)]"
>
<
div
className=
"animate-pulse"
>
<
div
className=
"flex items-start gap-3"
>
<
div
className=
"size-9 rounded-xl bg-zinc-200"
/>
<
div
className=
"min-w-0 flex-1 space-y-2"
>
<
div
className=
"h-4 w-2/5 rounded-full bg-zinc-200"
/>
<
div
className=
"h-3 w-full rounded-full bg-zinc-100"
/>
</
div
>
</
div
>
<
div
className=
"mt-2 flex gap-1.5"
>
<
div
className=
"h-5 w-12 rounded-full bg-zinc-100"
/>
<
div
className=
"h-5 w-14 rounded-full bg-zinc-100"
/>
</
div
>
</
div
>
</
div
>
))
}
</
div
>
);
}
export
default
function
ApplicationSwitcherDialog
({
open
,
onClose
,
userInfo
,
siteInfo
,
})
{
const
fallbackApplications
=
useMemo
(()
=>
{
if
(
!
Array
.
isArray
(
userInfo
?.
applications
))
{
return
[];
}
return
userInfo
.
applications
.
map
((
application
)
=>
normalizeApplication
(
application
,
{
...
application
,
description
:
application
?.
title
,
}),
)
.
filter
((
application
)
=>
application
.
name
);
},
[
userInfo
]);
const
[
applications
,
setApplications
]
=
useState
(
fallbackApplications
);
const
[
status
,
setStatus
]
=
useState
(
fallbackApplications
.
length
>
0
?
'success'
:
'idle'
,
);
const
[
errorMessage
,
setErrorMessage
]
=
useState
(
''
);
const
applicationSections
=
useMemo
(()
=>
{
return
buildApplicationSections
(
applications
,
siteInfo
?.
group
);
},
[
applications
,
siteInfo
?.
group
]);
useEffect
(()
=>
{
if
(
fallbackApplications
.
length
===
0
)
{
return
;
}
setApplications
((
previous
)
=>
{
if
(
previous
.
length
>
0
)
{
return
previous
;
}
return
fallbackApplications
;
});
},
[
fallbackApplications
]);
useEffect
(()
=>
{
if
(
!
open
)
{
return
;
}
let
cancelled
=
false
;
const
loadApplications
=
async
()
=>
{
setStatus
((
previous
)
=>
(
previous
===
'success'
?
previous
:
'loading'
));
setErrorMessage
(
''
);
try
{
const
response
=
await
getApplications
();
const
fetchedApplications
=
Array
.
isArray
(
response
)
?
response
:
[];
if
(
cancelled
)
{
return
;
}
const
mergedApplications
=
mergeApplications
(
fetchedApplications
,
fallbackApplications
,
);
setApplications
(
mergedApplications
);
setStatus
(
'success'
);
}
catch
(
error
)
{
if
(
cancelled
)
{
return
;
}
setApplications
(
fallbackApplications
);
setStatus
(
'error'
);
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
}
};
loadApplications
();
return
()
=>
{
cancelled
=
true
;
};
},
[
fallbackApplications
,
open
]);
return
(
<
Headless
.
Dialog
open=
{
open
}
onClose=
{
onClose
}
className=
"relative z-[180]"
>
<
Headless
.
DialogBackdrop
transition
className=
"fixed inset-0 bg-[rgba(15,23,42,0.48)] backdrop-blur-sm transition duration-300 ease-out data-closed:opacity-0"
/>
<
div
className=
"fixed inset-0 overflow-y-auto p-3 sm:p-4 lg:p-5"
>
<
div
className=
"flex min-h-full items-center justify-center"
>
<
Headless
.
DialogPanel
transition
className=
"flex max-h-[calc(100dvh-2rem)] w-full max-w-[92rem] flex-col overflow-hidden rounded-[28px] border border-white/70 bg-[linear-gradient(180deg,rgba(255,255,255,0.96),rgba(248,250,252,0.94))] p-4 shadow-[0_30px_80px_rgba(15,23,42,0.22)] ring-1 ring-black/5 transition duration-300 ease-out data-closed:translate-y-4 data-closed:scale-[0.97] data-closed:opacity-0 sm:p-5"
>
<
div
className=
"flex shrink-0 flex-col gap-3 border-b border-zinc-200/80 pb-4 sm:flex-row sm:items-start sm:justify-between"
>
<
div
className=
"min-w-0"
>
<
div
className=
"inline-flex items-center gap-2 rounded-full bg-rose-50 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-rose-600"
>
<
SparklesIcon
className=
"size-3.5"
/>
Applications
</
div
>
<
Headless
.
DialogTitle
className=
"mt-2 text-xl font-semibold tracking-tight text-slate-950 sm:text-2xl"
>
应用切换
</
Headless
.
DialogTitle
>
<
p
className=
"mt-1 max-w-2xl text-xs leading-5 text-slate-500"
>
这里展示当前账号可访问的全部应用,点击卡片即可进入对应工作区。
</
p
>
</
div
>
<
div
className=
"flex items-center gap-3"
>
<
div
className=
"rounded-2xl border border-rose-100 bg-rose-50/70 px-3 py-2 text-right"
>
<
div
className=
"text-[11px] font-medium tracking-wide text-rose-500"
>
可见应用
</
div
>
<
div
className=
"mt-0.5 text-xl font-semibold tracking-tight text-slate-950"
>
{
applications
.
length
}
</
div
>
</
div
>
<
button
type=
"button"
onClick=
{
onClose
}
className=
"inline-flex h-10 items-center justify-center rounded-xl border border-zinc-200 bg-white px-3.5 text-sm font-medium text-slate-600 transition hover:border-zinc-300 hover:text-slate-900"
>
关闭
</
button
>
</
div
>
</
div
>
<
div
className=
"mt-4 min-h-0 flex-1 overflow-y-auto pr-1 [scrollbar-gutter:stable]"
>
{
status
===
'error'
&&
errorMessage
?
(
<
div
className=
"mb-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50/80 px-4 py-3 text-sm text-amber-900"
>
<
ExclamationCircleIcon
className=
"mt-0.5 size-5 shrink-0 text-amber-500"
/>
<
div
>
{
errorMessage
}
</
div
>
</
div
>
)
:
null
}
{
status
===
'loading'
&&
applications
.
length
===
0
?
(
<
LoadingSkeleton
/>
)
:
applications
.
length
>
0
?
(
<
div
className=
"space-y-4"
>
{
applicationSections
.
map
((
section
)
=>
(
<
section
key=
{
section
.
key
}
>
<
div
className=
"mb-2 flex items-center gap-3"
>
<
h3
className=
"text-sm font-semibold text-slate-900"
>
{
section
.
label
}
</
h3
>
<
span
className=
"h-px flex-1 bg-zinc-200/80"
/>
<
span
className=
"text-xs font-medium text-slate-400"
>
{
section
.
applications
.
length
}
</
span
>
</
div
>
<
div
className=
"grid gap-2.5 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-6"
>
{
section
.
applications
.
map
((
application
)
=>
(
<
ApplicationCard
key=
{
application
.
name
}
application=
{
application
}
/>
))
}
</
div
>
</
section
>
))
}
</
div
>
)
:
(
<
div
className=
"flex min-h-72 flex-col items-center justify-center rounded-[28px] border border-dashed border-zinc-200 bg-zinc-50/80 px-6 text-center"
>
<
div
className=
"text-lg font-semibold text-slate-900"
>
暂无可切换应用
</
div
>
<
p
className=
"mt-2 max-w-md text-sm leading-6 text-slate-500"
>
当前账号还没有分配其它应用权限,后续分配后这里会自动展示。
</
p
>
</
div
>
)
}
</
div
>
</
Headless
.
DialogPanel
>
</
div
>
</
div
>
</
Headless
.
Dialog
>
);
}
src/pages/project_manager_xy/Layout.jsx
View file @
dd4b6b28
import
ApplicationSwitcherDialog
from
'@/pages/project_manager_xy/ApplicationSwitcherDialog'
;
import
xyLogoSmall
from
'@/assets/xy-logo_small.png'
;
import
xyLogoSmall
from
'@/assets/xy-logo_small.png'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
import
{
ArchiveBoxIcon
,
ArchiveBoxIcon
,
Bars3Icon
,
Bars3Icon
,
...
@@ -180,9 +182,11 @@ function MobileSidebar({ open, close, children }) {
...
@@ -180,9 +182,11 @@ function MobileSidebar({ open, close, children }) {
export
default
function
ProjectManagerLayout
()
{
export
default
function
ProjectManagerLayout
()
{
const
location
=
useLocation
();
const
location
=
useLocation
();
const
[
isMobile
,
setIsMobile
]
=
useState
(
false
);
const
[
isMobile
,
setIsMobile
]
=
useState
(
false
);
const
[
isApplicationDialogOpen
,
setIsApplicationDialogOpen
]
=
useState
(
false
);
const
[
isSidebarCollapsed
,
setIsSidebarCollapsed
]
=
useState
(
false
);
const
[
isSidebarCollapsed
,
setIsSidebarCollapsed
]
=
useState
(
false
);
const
[
isMobileSidebarOpen
,
setIsMobileSidebarOpen
]
=
useState
(
false
);
const
[
isMobileSidebarOpen
,
setIsMobileSidebarOpen
]
=
useState
(
false
);
const
userInfo
=
useContext
(
UserContext
);
const
userInfo
=
useContext
(
UserContext
);
const
siteInfo
=
useContext
(
SiteContext
);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
typeof
window
===
'undefined'
)
{
if
(
typeof
window
===
'undefined'
)
{
...
@@ -224,6 +228,21 @@ export default function ProjectManagerLayout() {
...
@@ -224,6 +228,21 @@ export default function ProjectManagerLayout() {
);
);
},
[
location
.
pathname
]);
},
[
location
.
pathname
]);
const
applicationSwitcherButton
=
(
<
button
type=
"button"
onClick=
{
()
=>
setIsApplicationDialogOpen
(
true
)
}
aria
-
label=
"Open application switcher"
title=
"应用切换"
className=
{
clsx
(
'inline-flex items-center justify-center rounded-[20px] border border-[#f97316]/70 bg-[linear-gradient(180deg,rgba(39,39,42,0.92),rgba(24,24,27,0.96))] text-[#fb923c] shadow-[0_16px_30px_rgba(249,115,22,0.16)] transition duration-200 hover:-translate-y-0.5 hover:border-[#fb923c] hover:text-[#fdba74] hover:shadow-[0_18px_36px_rgba(249,115,22,0.26)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#fb923c]/50'
,
sidebarCollapsed
?
'size-11'
:
'size-14'
,
)
}
>
<
Squares2X2Icon
className=
{
clsx
(
sidebarCollapsed
?
'size-4'
:
'size-5'
)
}
/>
</
button
>
);
return
(
return
(
<
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]"
>
<
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
<
aside
...
@@ -234,22 +253,34 @@ export default function ProjectManagerLayout() {
...
@@ -234,22 +253,34 @@ export default function ProjectManagerLayout() {
>
>
<
div
<
div
className=
{
clsx
(
className=
{
clsx
(
'mb-7 flex items-center'
,
'mb-7'
,
sidebarCollapsed
?
'justify-center'
:
'px-1'
,
sidebarCollapsed
?
'flex flex-col items-center gap-3'
:
'flex items-start justify-between gap-4 px-1'
,
)
}
)
}
>
>
{
sidebarCollapsed
?
(
{
sidebarCollapsed
?
(
<
img
<>
src=
{
xyLogoSmall
}
<
img
alt=
"XY Logo"
src=
{
xyLogoSmall
}
className=
"h-9 w-auto max-w-[52px] shrink-0 object-contain"
alt=
"XY Logo"
/>
className=
"h-8 w-auto max-w-8 shrink-0 object-contain"
)
:
null
}
/>
{
!
sidebarCollapsed
?
(
{
applicationSwitcherButton
}
<
h1
className=
"truncate text-sm font-semibold tracking-[0.02em] text-white/95"
>
</>
{
APP_NAME
}
)
:
(
</
h1
>
<>
)
:
null
}
<
div
className=
"min-w-0 flex-1"
>
<
h1
className=
"truncate text-sm font-semibold tracking-[0.02em] text-white/95"
>
{
APP_NAME
}
</
h1
>
<
div
className=
"mt-1 text-xs text-zinc-400"
>
风险案例与廉洁党建台账工作台
</
div
>
</
div
>
{
applicationSwitcherButton
}
</>
)
}
</
div
>
</
div
>
<
nav
className=
"flex-1 space-y-5 overflow-y-auto"
>
<
nav
className=
"flex-1 space-y-5 overflow-y-auto"
>
...
@@ -275,6 +306,17 @@ export default function ProjectManagerLayout() {
...
@@ -275,6 +306,17 @@ export default function ProjectManagerLayout() {
</
div
>
</
div
>
))
}
))
}
</
nav
>
</
nav
>
{
!
sidebarCollapsed
?
(
<
div
className=
"mt-6 rounded-2xl border border-zinc-800 bg-zinc-950/70 p-4 text-sm text-zinc-300"
>
<
div
className=
"font-medium text-white/90"
>
{
userInfo
?.
display_name
||
userInfo
?.
username
||
'当前用户'
}
</
div
>
<
div
className=
"mt-1 text-xs text-zinc-500"
>
当前正在查看风险案例库
</
div
>
</
div
>
)
:
null
}
</
aside
>
</
aside
>
<
div
className=
"relative z-30 hidden lg:flex"
>
<
div
className=
"relative z-30 hidden lg:flex"
>
...
@@ -302,10 +344,11 @@ export default function ProjectManagerLayout() {
...
@@ -302,10 +344,11 @@ export default function ProjectManagerLayout() {
close=
{
()
=>
setIsMobileSidebarOpen
(
false
)
}
close=
{
()
=>
setIsMobileSidebarOpen
(
false
)
}
>
>
<
div
className=
"px-5 pt-6"
>
<
div
className=
"px-5 pt-6"
>
<
div
className=
"flex items-center"
>
<
div
>
<
h1
className=
"text-base font-semibold tracking-[0.02em] text-white"
>
<
h1
className=
"text-base font-semibold tracking-[0.02em] text-white"
>
{
APP_NAME
}
{
APP_NAME
}
</
h1
>
</
h1
>
<
div
className=
"text-xs text-zinc-500"
>
风险案例库工作台
</
div
>
</
div
>
</
div
>
</
div
>
</
div
>
...
@@ -341,12 +384,26 @@ export default function ProjectManagerLayout() {
...
@@ -341,12 +384,26 @@ export default function ProjectManagerLayout() {
<
Bars3Icon
className=
"size-5"
/>
<
Bars3Icon
className=
"size-5"
/>
</
NavbarItem
>
</
NavbarItem
>
<
div
className=
"text-sm text-zinc-500"
>
{
currentGroupTitle
}
</
div
>
<
div
className=
"text-sm text-zinc-500"
>
{
currentGroupTitle
}
</
div
>
<
NavbarItem
onClick=
{
()
=>
setIsApplicationDialogOpen
(
true
)
}
aria
-
label=
"Open application switcher"
className=
"border border-rose-200 bg-white text-rose-500 shadow-sm hover:bg-rose-50 hover:text-rose-600"
>
<
Squares2X2Icon
className=
"size-4"
/>
</
NavbarItem
>
</
div
>
</
div
>
<
div
className=
"flex-1 overflow-y-auto rounded-[32px] border border-white/70 bg-white/75 p-4 shadow-[0_24px_80px_rgba(15,23,42,0.06)] backdrop-blur sm:p-6"
>
<
div
className=
"flex-1 overflow-y-auto rounded-[32px] border border-white/70 bg-white/75 p-4 shadow-[0_24px_80px_rgba(15,23,42,0.06)] backdrop-blur sm:p-6"
>
<
Outlet
/>
<
Outlet
/>
</
div
>
</
div
>
</
main
>
</
main
>
<
ApplicationSwitcherDialog
open=
{
isApplicationDialogOpen
}
onClose=
{
()
=>
setIsApplicationDialogOpen
(
false
)
}
userInfo=
{
userInfo
}
siteInfo=
{
siteInfo
}
/>
</
div
>
</
div
>
);
);
}
}
src/pages/project_manager_xy/audit-material-center.business-flow.md
View file @
dd4b6b28
...
@@ -24,9 +24,16 @@
...
@@ -24,9 +24,16 @@
-
左侧菜单:
`驾驶舱`
-
左侧菜单:
`驾驶舱`
-
左侧菜单:
`案例管理`
-
左侧菜单:
`案例管理`
-
左侧菜单顶部:
`应用切换`
推荐职责划分:
推荐职责划分:
-
`应用切换`
-
桌面端侧栏展开时显示在应用标题右侧,侧栏收起时显示在 Logo 下方
-
移动端显示在顶部菜单栏右侧
-
点击后打开应用切换弹框,优先调用
`/application/info`
读取当前账号可见应用
-
应用列表加载失败时,回退展示当前登录用户信息中的
`applications`
-
点击应用卡片后跳转到对应应用根路径
-
`驾驶舱`
-
`驾驶舱`
-
展示案例总数、分类数量、标签数量、案例类型数量、待整理数量、最近 7 天更新
-
展示案例总数、分类数量、标签数量、案例类型数量、待整理数量、最近 7 天更新
-
通过图表展示分类结构、案例类型排行、案例来源分布
-
通过图表展示分类结构、案例类型排行、案例来源分布
...
@@ -242,6 +249,7 @@
...
@@ -242,6 +249,7 @@
-
查询按钮
-
查询按钮
-
筛选项为普通文本输入框,不提供下拉框
-
筛选项为普通文本输入框,不提供下拉框
-
`Segment`
视图切换(卡片 / 表格)
-
`Segment`
视图切换(卡片 / 表格)
-
顶部快捷操作区采用紧凑工具条呈现,保留手动新建、Excel 导入、Excel 导出、重置筛选四个入口
-
手动新建按钮
-
手动新建按钮
-
Excel 导入按钮
-
Excel 导入按钮
-
导入弹框中的导出模板按钮
-
导入弹框中的导出模板按钮
...
@@ -313,6 +321,7 @@
...
@@ -313,6 +321,7 @@
-
批量导入时,单条失败不应影响其它条目继续导入
-
批量导入时,单条失败不应影响其它条目继续导入
-
保存失败时,保留当前表单输入,提示用户重试
-
保存失败时,保留当前表单输入,提示用户重试
-
删除前必须有明确确认,避免误删
-
删除前必须有明确确认,避免误删
-
应用列表加载失败时,应用切换弹框保留打开状态,并提示已回退展示当前账号上的应用信息
---
---
...
...
src/pages/project_manager_xy/integrity-party.business-flow.md
View file @
dd4b6b28
...
@@ -16,7 +16,8 @@
...
@@ -16,7 +16,8 @@
-
左侧菜单:
`驾驶舱`
-
左侧菜单:
`驾驶舱`
-
页面内可切换
`案例库统计 / 廉洁党建统计`
-
页面内可切换
`案例库统计 / 廉洁党建统计`
-
廉洁党建统计展示台账总数、本年度记录、附件完整率、最近 30 天更新、台账分类分布、年度分布和最近更新台账
-
廉洁党建统计展示台账总数、本年度记录、最近 30 天更新、台账分类分布、年度分布和本年度模块概览
-
本年度模块概览展示培训类 4 个培训类别的开展场次和参加人数、谈话类已谈话人数及投资序列/管理序列人数、调研类/专题会议类/廉洁主题活动/节前廉洁提醒的开展次数
-
左侧菜单:
`廉洁党建建设管理`
-
左侧菜单:
`廉洁党建建设管理`
-
页面内通过 Tab 切换
`培训类 / 谈话类 / 调研类 / 专题会议类 / 廉洁主题活动 / 节前廉洁提醒`
-
页面内通过 Tab 切换
`培训类 / 谈话类 / 调研类 / 专题会议类 / 廉洁主题活动 / 节前廉洁提醒`
-
每个 Tab 对应独立台账对象和独立 Excel 模板
-
每个 Tab 对应独立台账对象和独立 Excel 模板
...
@@ -42,7 +43,7 @@
...
@@ -42,7 +43,7 @@
4.
筛选区统一包含关键词、年份和当前分类关键字段
4.
筛选区统一包含关键词、年份和当前分类关键字段
5.
用户点击
`查询`
后,前端调用
`POST /record/query`
5.
用户点击
`查询`
后,前端调用
`POST /record/query`
6.
列表按
`updated_at`
、
`created_at`
倒序展示
6.
列表按
`updated_at`
、
`created_at`
倒序展示
7.
表格仅展示核心字段
、附件状态、更新时间和操作入口
7.
表格仅展示核心字段
和操作入口;非培训类保留附件状态、更新时间辅助列,培训类外层展示序号、培训类别、时间、授课主题、授课人、参加人数、培训对象、牵头部门,不展示附件状态和更新时间
8.
完整字段在编辑弹窗中维护
8.
完整字段在编辑弹窗中维护
### 4.2 手动新建台账
### 4.2 手动新建台账
...
@@ -114,6 +115,9 @@
...
@@ -114,6 +115,9 @@
-
保存时日期字段转换为 ISO 8601 字符串
-
保存时日期字段转换为 ISO 8601 字符串
-
布尔字段使用三态 UI:是、否、未填写,对应
`true / false / null`
-
布尔字段使用三态 UI:是、否、未填写,对应
`true / false / null`
-
培训类别固定为:廉政党课讲授、廉洁合规培训、专题廉洁合规教育培训、新员工廉洁教育培训
-
培训类别固定为:廉政党课讲授、廉洁合规培训、专题廉洁合规教育培训、新员工廉洁教育培训
-
驾驶舱培训类按
`training_category`
统计本年度场次,并汇总
`participant_count`
为参加人数
-
驾驶舱谈话类仅统计本年度存在
`actual_talk_date`
的已谈话记录;投资序列按
`is_investment_sequence = true`
统计,管理序列按
`is_leader = true`
统计
-
驾驶舱调研类、专题会议类、廉洁主题活动、节前廉洁提醒按本年度记录数统计开展次数
-
重大节假日固定为:元旦、春节、清明、五一、端午、中秋、国庆、其他
-
重大节假日固定为:元旦、春节、清明、五一、端午、中秋、国庆、其他
-
选择重大节假日
`其他`
时,可填写
`holiday_other`
-
选择重大节假日
`其他`
时,可填写
`holiday_other`
-
人员、部门、岗位、单位等字段一期全部使用普通文本输入,不做系统引用
-
人员、部门、岗位、单位等字段一期全部使用普通文本输入,不做系统引用
...
...
src/pages/project_manager_xy/integrity-party/index.jsx
View file @
dd4b6b28
...
@@ -760,8 +760,12 @@ export default function IntegrityPartyPage() {
...
@@ -760,8 +760,12 @@ export default function IntegrityPartyPage() {
{
fieldMap
[
fieldName
]?.
label
||
fieldName
}
{
fieldMap
[
fieldName
]?.
label
||
fieldName
}
</
TableHeader
>
</
TableHeader
>
))
}
))
}
<
TableHeader
>
附件状态
</
TableHeader
>
{
activeConfig
.
showAttachmentStatusColumn
===
false
?
null
:
(
<
TableHeader
>
更新时间
</
TableHeader
>
<
TableHeader
>
附件状态
</
TableHeader
>
)
}
{
activeConfig
.
showUpdatedAtColumn
===
false
?
null
:
(
<
TableHeader
>
更新时间
</
TableHeader
>
)
}
<
TableHeader
className=
"text-right"
>
操作
</
TableHeader
>
<
TableHeader
className=
"text-right"
>
操作
</
TableHeader
>
</
TableRow
>
</
TableRow
>
</
TableHead
>
</
TableHead
>
...
@@ -790,19 +794,23 @@ export default function IntegrityPartyPage() {
...
@@ -790,19 +794,23 @@ export default function IntegrityPartyPage() {
</
span
>
</
span
>
</
TableCell
>
</
TableCell
>
))
}
))
}
<
TableCell
>
{
activeConfig
.
showAttachmentStatusColumn
===
false
?
null
:
(
<
span
<
TableCell
>
className=
{
clsx
(
<
span
'inline-flex rounded-full px-2.5 py-1 text-xs font-medium'
,
className=
{
clsx
(
record
.
attachmentCount
>
0
'inline-flex rounded-full px-2.5 py-1 text-xs font-medium'
,
?
'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200'
record
.
attachmentCount
>
0
:
'bg-zinc-100 text-zinc-600 ring-1 ring-zinc-200'
,
?
'bg-emerald-50 text-emerald-700 ring-1 ring-emerald-200'
)
}
:
'bg-zinc-100 text-zinc-600 ring-1 ring-zinc-200'
,
>
)
}
{
record
.
attachmentStatus
}
>
</
span
>
{
record
.
attachmentStatus
}
</
TableCell
>
</
span
>
<
TableCell
>
{
formatDateTime
(
record
.
updatedAt
)
}
</
TableCell
>
</
TableCell
>
)
}
{
activeConfig
.
showUpdatedAtColumn
===
false
?
null
:
(
<
TableCell
>
{
formatDateTime
(
record
.
updatedAt
)
}
</
TableCell
>
)
}
<
TableCell
className=
"text-right"
>
<
TableCell
className=
"text-right"
>
<
Button
type=
"button"
plain
onClick=
{
()
=>
openEditDialog
(
record
)
}
>
<
Button
type=
"button"
plain
onClick=
{
()
=>
openEditDialog
(
record
)
}
>
<
PencilSquareIcon
data
-
slot=
"icon"
/>
<
PencilSquareIcon
data
-
slot=
"icon"
/>
...
...
src/pages/project_manager_xy/integrityPartyService.js
View file @
dd4b6b28
...
@@ -66,9 +66,13 @@ export const INTEGRITY_LEDGER_CONFIGS = [
...
@@ -66,9 +66,13 @@ export const INTEGRITY_LEDGER_CONFIGS = [
'training_category'
,
'training_category'
,
'training_date'
,
'training_date'
,
'lecture_topic'
,
'lecture_topic'
,
'lecturer'
,
'participant_count'
,
'training_target'
,
'training_target'
,
'lead_department'
,
'lead_department'
,
],
],
showAttachmentStatusColumn
:
false
,
showUpdatedAtColumn
:
false
,
batchFields
:
[
'training_category'
,
'training_date'
,
'lead_department'
],
batchFields
:
[
'training_category'
,
'training_date'
,
'lead_department'
],
fields
:
[
fields
:
[
field
(
'序号'
,
'sequence_no'
,
FIELD_TYPES
.
NUMBER
),
field
(
'序号'
,
'sequence_no'
,
FIELD_TYPES
.
NUMBER
),
...
@@ -1235,25 +1239,9 @@ export async function loadIntegrityPartyDashboard() {
...
@@ -1235,25 +1239,9 @@ export async function loadIntegrityPartyDashboard() {
const
now
=
dayjs
();
const
now
=
dayjs
();
const
currentYear
=
now
.
year
();
const
currentYear
=
now
.
year
();
const
recentThreshold
=
now
.
subtract
(
30
,
'day'
);
const
recentThreshold
=
now
.
subtract
(
30
,
'day'
);
const
totalAttachmentFields
=
records
.
reduce
((
sum
,
record
)
=>
{
const
currentYearRecords
=
records
.
filter
(
const
config
=
getConfig
(
record
.
ledgerKey
);
(
record
)
=>
getRecordYear
(
record
)
===
String
(
currentYear
),
return
(
);
sum
+
config
.
fields
.
filter
((
fieldConfig
)
=>
fieldConfig
.
type
===
FIELD_TYPES
.
FILE
)
.
length
);
},
0
);
const
filledAttachmentFields
=
records
.
reduce
((
sum
,
record
)
=>
{
const
config
=
getConfig
(
record
.
ledgerKey
);
return
(
sum
+
config
.
fields
.
filter
((
fieldConfig
)
=>
fieldConfig
.
type
===
FIELD_TYPES
.
FILE
)
.
filter
((
fieldConfig
)
=>
{
return
normalizeFileList
(
record
.
rawMetadata
[
fieldConfig
.
name
]).
length
>
0
;
}).
length
);
},
0
);
const
ledgerGroups
=
ledgerResults
.
map
((
result
)
=>
({
const
ledgerGroups
=
ledgerResults
.
map
((
result
)
=>
({
key
:
result
.
config
.
key
,
key
:
result
.
config
.
key
,
label
:
result
.
config
.
tabLabel
,
label
:
result
.
config
.
tabLabel
,
...
@@ -1266,34 +1254,96 @@ export async function loadIntegrityPartyDashboard() {
...
@@ -1266,34 +1254,96 @@ export async function loadIntegrityPartyDashboard() {
(
item
)
=>
item
.
year
||
'未填写年份'
,
(
item
)
=>
item
.
year
||
'未填写年份'
,
(
item
)
=>
item
.
year
||
'未填写年份'
,
(
item
)
=>
item
.
year
||
'未填写年份'
,
);
);
const
recentRecords
=
[...
records
]
.
sort
((
left
,
right
)
=>
String
(
right
.
updatedAt
||
right
.
createdAt
).
localeCompare
(
String
(
left
.
updatedAt
||
left
.
createdAt
),
),
)
.
slice
(
0
,
8
);
return
{
return
{
totalCount
:
ledgerResults
.
reduce
((
sum
,
result
)
=>
sum
+
result
.
total
,
0
),
totalCount
:
ledgerResults
.
reduce
((
sum
,
result
)
=>
sum
+
result
.
total
,
0
),
currentYearCount
:
records
.
filter
((
record
)
=>
getRecordYear
(
record
)
===
String
(
currentYear
))
.
length
,
currentYearCount
:
currentYearRecords
.
length
,
recentCount
:
records
.
filter
((
record
)
=>
{
recentCount
:
records
.
filter
((
record
)
=>
{
if
(
!
record
.
updatedAt
)
{
if
(
!
record
.
updatedAt
)
{
return
false
;
return
false
;
}
}
return
dayjs
(
record
.
updatedAt
).
isAfter
(
recentThreshold
);
return
dayjs
(
record
.
updatedAt
).
isAfter
(
recentThreshold
);
}).
length
,
}).
length
,
attachmentCompletionRate
:
totalAttachmentFields
>
0
?
Math
.
round
((
filledAttachmentFields
/
totalAttachmentFields
)
*
100
)
:
0
,
ledgerGroups
,
ledgerGroups
,
yearGroups
,
yearGroups
,
recentRecords
,
moduleOverview
:
buildIntegrityModuleOverview
(
currentYearRecords
,
currentYear
)
,
errors
:
ledgerResults
.
map
((
result
)
=>
result
.
error
).
filter
(
Boolean
),
errors
:
ledgerResults
.
map
((
result
)
=>
result
.
error
).
filter
(
Boolean
),
};
};
}
}
function
buildIntegrityModuleOverview
(
currentYearRecords
,
currentYear
)
{
const
trainingRecords
=
currentYearRecords
.
filter
(
(
record
)
=>
record
.
ledgerKey
===
'training'
,
);
const
talkedRecords
=
currentYearRecords
.
filter
((
record
)
=>
{
if
(
record
.
ledgerKey
!==
'talk'
)
{
return
false
;
}
const
actualTalkDate
=
formatDate
(
record
.
rawMetadata
?.
actual_talk_date
??
record
.
metadata
?.
actual_talk_date
,
);
return
actualTalkDate
.
startsWith
(
String
(
currentYear
));
});
return
{
year
:
currentYear
,
trainingCategories
:
TRAINING_CATEGORY_OPTIONS
.
map
((
category
)
=>
{
const
categoryRecords
=
trainingRecords
.
filter
(
(
record
)
=>
String
(
record
.
rawMetadata
?.
training_category
??
record
.
metadata
?.
training_category
??
''
,
).
trim
()
===
category
,
);
return
{
key
:
category
,
label
:
category
,
sessionCount
:
categoryRecords
.
length
,
participantCount
:
categoryRecords
.
reduce
((
sum
,
record
)
=>
{
const
participantCount
=
Number
(
record
.
rawMetadata
?.
participant_count
??
record
.
metadata
?.
participant_count
??
0
,
);
return
sum
+
(
Number
.
isFinite
(
participantCount
)
?
participantCount
:
0
);
},
0
),
};
}),
talkSummary
:
{
totalCount
:
talkedRecords
.
length
,
investmentCount
:
talkedRecords
.
filter
(
(
record
)
=>
normalizeBoolean
(
record
.
rawMetadata
?.
is_investment_sequence
??
record
.
metadata
?.
is_investment_sequence
,
)
===
true
,
).
length
,
managementCount
:
talkedRecords
.
filter
(
(
record
)
=>
normalizeBoolean
(
record
.
rawMetadata
?.
is_leader
??
record
.
metadata
?.
is_leader
)
===
true
,
).
length
,
},
yearlyModuleCounts
:
[
buildYearlyModuleCount
(
currentYearRecords
,
'research'
,
'调研类'
),
buildYearlyModuleCount
(
currentYearRecords
,
'meeting'
,
'专题会议类'
),
buildYearlyModuleCount
(
currentYearRecords
,
'activity'
,
'廉洁主题活动'
),
buildYearlyModuleCount
(
currentYearRecords
,
'holiday_reminder'
,
'节前廉洁提醒'
),
],
};
}
function
buildYearlyModuleCount
(
records
,
ledgerKey
,
label
)
{
return
{
key
:
ledgerKey
,
label
,
count
:
records
.
filter
((
record
)
=>
record
.
ledgerKey
===
ledgerKey
).
length
,
};
}
function
getRecordYear
(
record
)
{
function
getRecordYear
(
record
)
{
const
config
=
getConfig
(
record
.
ledgerKey
);
const
config
=
getConfig
(
record
.
ledgerKey
);
const
yearField
=
config
.
fields
.
find
(
const
yearField
=
config
.
fields
.
find
(
...
...
src/pages/project_manager_xy/resource-booking/index.jsx
View file @
dd4b6b28
...
@@ -1049,33 +1049,27 @@ export default function ResourceBookingPage() {
...
@@ -1049,33 +1049,27 @@ export default function ResourceBookingPage() {
return (
return (
<div className="space-y-6">
<div className="space-y-6">
<Panel className="relative overflow-hidden p-
6 sm:p-8
">
<Panel className="relative overflow-hidden p-
4 sm:p-5
">
<div className="absolute inset-0 bg-[linear-gradient(135deg,rgba(255,255,255,0.9
6),rgba(255,241,242,0.94)),radial-gradient(circle_at_top_left,rgba(251,191,188,0.2),transparent_36%),radial-gradient(circle_at_top_right,rgba(251,113,133,0.16),transparent_34%
)]" />
<div className="absolute inset-0 bg-[linear-gradient(135deg,rgba(255,255,255,0.9
8),rgba(255,247,247,0.94)
)]" />
<div className="relative">
<div className="relative">
<div className="
mt-5 flex flex-col gap-6
xl:flex-row xl:items-center xl:justify-between">
<div className="
flex flex-col gap-4
xl:flex-row xl:items-center xl:justify-between">
<div className="m
ax-w-3xl
">
<div className="m
in-w-0
">
<h1 className="text-
3xl font-semibold tracking-tight text-zinc-950 sm:text-4
xl">
<h1 className="text-
2xl font-semibold tracking-tight text-zinc-950 sm:text-3
xl">
案例管理
案例管理
</h1>
</h1>
</div>
</div>
<div className="w-full xl:w-[22rem] xl:shrink-0">
<div className="w-full rounded-2xl border border-zinc-200/70 bg-white/80 p-2 shadow-sm backdrop-blur-sm xl:w-auto xl:shrink-0">
<div className="rounded-[26px] border border-white/70 bg-white/65 p-4 shadow-[0_20px_50px_rgba(148,163,184,0.12)] backdrop-blur-sm">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="flex items-start justify-between gap-4">
<div className="flex h-10 shrink-0 items-center gap-2 px-2 text-xs font-semibold tracking-[0.16em] text-zinc-500">
<div>
<BookOpenIcon className="size-4 text-[var(--pm-brand)]" />
<div className="text-xs font-semibold tracking-[0.18em] text-zinc-500">
快捷操作
快捷操作
</div>
</div>
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-[linear-gradient(145deg,rgba(180,35,24,0.14),rgba(244,63,94,0.14))] text-[var(--pm-brand)]">
<BookOpenIcon className="size-5" />
</div>
</div>
</div>
<div className="
mt-4 space-y-3
">
<div className="
grid flex-1 grid-cols-2 gap-2 sm:flex sm:flex-wrap sm:justify-end
">
<Button
<Button
color="blue"
color="blue"
className="w-full justify-center whitespace-nowrap cursor-pointer"
className="w-full justify-center whitespace-nowrap cursor-pointer
sm:w-auto
"
onClick={() => {
onClick={() => {
setError('');
setError('');
setNotice('');
setNotice('');
...
@@ -1087,7 +1081,7 @@ export default function ResourceBookingPage() {
...
@@ -1087,7 +1081,7 @@ export default function ResourceBookingPage() {
</Button>
</Button>
<Button
<Button
outline
outline
className="w-full justify-center whitespace-nowrap bg-white/80 cursor-pointer"
className="w-full justify-center whitespace-nowrap bg-white/80 cursor-pointer
sm:w-auto
"
onClick={() => {
onClick={() => {
setError('');
setError('');
setNotice('');
setNotice('');
...
@@ -1099,7 +1093,7 @@ export default function ResourceBookingPage() {
...
@@ -1099,7 +1093,7 @@ export default function ResourceBookingPage() {
</Button>
</Button>
<Button
<Button
outline
outline
className="w-full justify-center whitespace-nowrap bg-white/80 cursor-pointer"
className="w-full justify-center whitespace-nowrap bg-white/80 cursor-pointer
sm:w-auto
"
onClick={handleExportCases}
onClick={handleExportCases}
disabled={exporting || loading}
disabled={exporting || loading}
>
>
...
@@ -1108,7 +1102,7 @@ export default function ResourceBookingPage() {
...
@@ -1108,7 +1102,7 @@ export default function ResourceBookingPage() {
</Button>
</Button>
<Button
<Button
plain
plain
className="w-full justify-center whitespace-nowrap text-zinc-600 cursor-pointer"
className="w-full justify-center whitespace-nowrap text-zinc-600 cursor-pointer
sm:w-auto
"
onClick={handleResetFilters}
onClick={handleResetFilters}
>
>
<ArrowPathIcon className="size-4" />
<ArrowPathIcon className="size-4" />
...
@@ -1122,7 +1116,7 @@ export default function ResourceBookingPage() {
...
@@ -1122,7 +1116,7 @@ export default function ResourceBookingPage() {
{error ? (
{error ? (
<div
<div
className={clsx(
className={clsx(
'mt-
6
rounded-2xl border px-4 py-3 text-sm',
'mt-
4
rounded-2xl border px-4 py-3 text-sm',
isDemoMode
isDemoMode
? 'border-amber-200 bg-amber-50 text-amber-700'
? 'border-amber-200 bg-amber-50 text-amber-700'
: 'border-rose-200 bg-rose-50 text-rose-700',
: 'border-rose-200 bg-rose-50 text-rose-700',
...
@@ -1133,7 +1127,7 @@ export default function ResourceBookingPage() {
...
@@ -1133,7 +1127,7 @@ export default function ResourceBookingPage() {
) : null}
) : null}
{!error && notice ? (
{!error && notice ? (
<div className="mt-
6
rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
<div className="mt-
4
rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">
{notice}
{notice}
</div>
</div>
) : null}
) : null}
...
...
src/pages/project_manager_xy/resource-stat/index.jsx
View file @
dd4b6b28
...
@@ -4,7 +4,6 @@ import {
...
@@ -4,7 +4,6 @@ import {
ChartPieIcon
,
ChartPieIcon
,
ClipboardDocumentListIcon
,
ClipboardDocumentListIcon
,
DocumentTextIcon
,
DocumentTextIcon
,
PaperClipIcon
,
RectangleStackIcon
,
RectangleStackIcon
,
TagIcon
,
TagIcon
,
}
from
'@heroicons/react/24/outline'
;
}
from
'@heroicons/react/24/outline'
;
...
@@ -404,7 +403,7 @@ export default function ResourceStatPage() {
...
@@ -404,7 +403,7 @@ export default function ResourceStatPage() {
return
(
return
(
<
div
className=
"space-y-6"
>
<
div
className=
"space-y-6"
>
<
Panel
className=
"relative overflow-hidden p-6 sm:p-8"
>
<
Panel
className=
"relative overflow-hidden p-6 sm:p-8"
>
<
div
className=
"absolute inset-
x-0 top-0 h-40 bg-[radial-gradient(circle_at_top_left,_rgba(245,158,11,0.16),_transparent_40%),radial-gradient(circle_at_top_right,_rgba(14,165,233,0.18),_transparent_35
%)]"
/>
<
div
className=
"absolute inset-
0 bg-[linear-gradient(135deg,rgba(255,247,237,0.94)_0%,rgba(255,255,255,0.9)_48%,rgba(224,242,254,0.86)_100%),radial-gradient(circle_at_top_left,rgba(245,158,11,0.14),transparent_36%),radial-gradient(circle_at_top_right,rgba(14,165,233,0.18),transparent_34
%)]"
/>
<
div
className=
"relative"
>
<
div
className=
"relative"
>
<
div
className=
"mt-5 flex flex-col gap-6 xl:flex-row xl:items-end xl:justify-between"
>
<
div
className=
"mt-5 flex flex-col gap-6 xl:flex-row xl:items-end xl:justify-between"
>
<
div
className=
"max-w-3xl"
>
<
div
className=
"max-w-3xl"
>
...
@@ -575,15 +574,23 @@ function IntegrityDashboardView({ dashboard, loading }) {
...
@@ -575,15 +574,23 @@ function IntegrityDashboardView({ dashboard, loading }) {
totalCount
:
0
,
totalCount
:
0
,
currentYearCount
:
0
,
currentYearCount
:
0
,
recentCount
:
0
,
recentCount
:
0
,
attachmentCompletionRate
:
0
,
ledgerGroups
:
[],
ledgerGroups
:
[],
yearGroups
:
[],
yearGroups
:
[],
recentRecords
:
[],
moduleOverview
:
{
year
:
new
Date
().
getFullYear
(),
trainingCategories
:
[],
talkSummary
:
{
totalCount
:
0
,
investmentCount
:
0
,
managementCount
:
0
,
},
yearlyModuleCounts
:
[],
},
};
};
return
(
return
(
<>
<>
<
div
className=
"grid gap-4 md:grid-cols-
2 xl:grid-cols-4
"
>
<
div
className=
"grid gap-4 md:grid-cols-
3
"
>
<
MetricCard
<
MetricCard
icon=
{
ClipboardDocumentListIcon
}
icon=
{
ClipboardDocumentListIcon
}
label=
"台账总数"
label=
"台账总数"
...
@@ -596,12 +603,6 @@ function IntegrityDashboardView({ dashboard, loading }) {
...
@@ -596,12 +603,6 @@ function IntegrityDashboardView({ dashboard, loading }) {
value=
{
safeDashboard
.
currentYearCount
}
value=
{
safeDashboard
.
currentYearCount
}
tone=
"green"
tone=
"green"
/>
/>
<
MetricCard
icon=
{
PaperClipIcon
}
label=
"附件完整率"
value=
{
`${safeDashboard.attachmentCompletionRate}%`
}
tone=
"amber"
/>
<
MetricCard
<
MetricCard
icon=
{
ArrowTrendingUpIcon
}
icon=
{
ArrowTrendingUpIcon
}
label=
"最近 30 天更新"
label=
"最近 30 天更新"
...
@@ -615,39 +616,97 @@ function IntegrityDashboardView({ dashboard, loading }) {
...
@@ -615,39 +616,97 @@ function IntegrityDashboardView({ dashboard, loading }) {
<
ChartPanel
title=
"年度分布"
groups=
{
safeDashboard
.
yearGroups
}
/>
<
ChartPanel
title=
"年度分布"
groups=
{
safeDashboard
.
yearGroups
}
/>
</
div
>
</
div
>
<
Panel
className=
"p-6"
>
<
IntegrityModuleOverview
overview=
{
safeDashboard
.
moduleOverview
}
loading=
{
loading
}
/>
<
SectionHeader
title=
"最近更新台账"
/>
</>
<
div
className=
"space-y-3"
>
);
{
safeDashboard
.
recentRecords
.
length
>
0
?
(
}
safeDashboard
.
recentRecords
.
map
((
item
)
=>
(
<
div
function
IntegrityModuleOverview
({
overview
,
loading
})
{
key=
{
item
.
id
}
const
safeOverview
=
overview
||
{
className=
"flex flex-col gap-3 rounded-[24px] border border-zinc-200 bg-zinc-50/70 px-4 py-4 sm:flex-row sm:items-center sm:justify-between"
year
:
new
Date
().
getFullYear
(),
>
trainingCategories
:
[],
<
div
className=
"min-w-0"
>
talkSummary
:
{
<
div
className=
"flex flex-wrap items-center gap-2"
>
totalCount
:
0
,
<
div
className=
"truncate text-sm font-semibold text-zinc-950 sm:text-base"
>
investmentCount
:
0
,
{
item
.
title
}
managementCount
:
0
,
</
div
>
},
<
Pill
>
{
item
.
ledgerLabel
}
</
Pill
>
yearlyModuleCounts
:
[],
<
Pill
tone=
{
item
.
attachmentCount
>
0
?
'green'
:
'amber'
}
>
};
附件
{
item
.
attachmentCount
}
const
talkItems
=
[
</
Pill
>
[
'已谈话人数'
,
safeOverview
.
talkSummary
.
totalCount
],
[
'投资序列'
,
safeOverview
.
talkSummary
.
investmentCount
],
[
'管理序列'
,
safeOverview
.
talkSummary
.
managementCount
],
];
return
(
<
Panel
className=
"p-6"
>
<
SectionHeader
title=
{
`${safeOverview.year} 年模块概览`
}
/>
{
loading
?
(
<
div
className=
"text-sm text-zinc-400"
>
正在加载廉洁党建统计...
</
div
>
)
:
(
<
div
className=
"grid gap-6 xl:grid-cols-[1.35fr_0.9fr_1fr]"
>
<
div
>
<
div
className=
"mb-3 text-sm font-semibold text-zinc-900"
>
培训类
</
div
>
<
div
className=
"overflow-hidden rounded-2xl border border-zinc-200"
>
<
div
className=
"grid grid-cols-[1fr_88px_88px] bg-zinc-50 px-4 py-3 text-xs font-semibold text-zinc-500"
>
<
div
>
培训类别
</
div
>
<
div
className=
"text-right"
>
场次
</
div
>
<
div
className=
"text-right"
>
人数
</
div
>
</
div
>
{
safeOverview
.
trainingCategories
.
map
((
item
)
=>
(
<
div
key=
{
item
.
key
}
className=
"grid grid-cols-[1fr_88px_88px] border-t border-zinc-200 px-4 py-3 text-sm"
>
<
div
className=
"min-w-0 truncate font-medium text-zinc-800"
>
{
item
.
label
}
</
div
>
</
div
>
<
div
className=
"mt-2 text-sm text-zinc-500"
>
<
div
className=
"text-right tabular-nums text-zinc-950"
>
更新于
{
item
.
updatedAtLabel
}
{
item
.
sessionCount
}
</
div
>
<
div
className=
"text-right tabular-nums text-zinc-950"
>
{
item
.
participantCount
}
</
div
>
</
div
>
</
div
>
</
div
>
<
div
className=
"text-sm text-zinc-500"
>
{
item
.
attachmentStatus
}
</
div
>
))
}
</
div
>
))
)
:
(
<
div
className=
"text-sm text-zinc-400"
>
{
loading
?
'正在加载廉洁党建统计...'
:
'暂无最近更新台账'
}
</
div
>
</
div
>
)
}
</
div
>
<
div
>
<
div
className=
"mb-3 text-sm font-semibold text-zinc-900"
>
谈话类
</
div
>
<
div
className=
"grid gap-3"
>
{
talkItems
.
map
(([
label
,
value
])
=>
(
<
div
key=
{
label
}
className=
"flex items-center justify-between rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3"
>
<
div
className=
"text-sm text-zinc-600"
>
{
label
}
</
div
>
<
div
className=
"text-2xl font-semibold tabular-nums text-zinc-950"
>
{
value
}
</
div
>
</
div
>
))
}
</
div
>
</
div
>
<
div
>
<
div
className=
"mb-3 text-sm font-semibold text-zinc-900"
>
年度开展次数
</
div
>
<
div
className=
"space-y-2"
>
{
safeOverview
.
yearlyModuleCounts
.
map
((
item
)
=>
(
<
div
key=
{
item
.
key
}
className=
"flex items-center justify-between border-b border-zinc-200 py-3 last:border-b-0"
>
<
div
className=
"text-sm font-medium text-zinc-700"
>
{
item
.
label
}
</
div
>
<
div
className=
"text-xl font-semibold tabular-nums text-zinc-950"
>
{
item
.
count
}
</
div
>
</
div
>
))
}
</
div
>
</
div
>
</
div
>
</
div
>
</
Panel
>
)
}
</>
</
Panel
>
);
);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment