Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
I
integrity_practice_management_new
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
integrity_practice_management_new
Commits
0f9e79a0
Commit
0f9e79a0
authored
Aug 26, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: align application switcher with Xinyuan baseline
parent
14cd2363
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
196 additions
and
74 deletions
+196
-74
ui-architecture.md
docs/ui-architecture.md
+1
-0
ui-migration-checklist.md
docs/ui-migration-checklist.md
+1
-1
xinyuan-design-system.md
docs/xinyuan-design-system.md
+1
-1
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+5
-3
ApplicationSwitcherDialog.jsx
src/components/ApplicationSwitcherDialog.jsx
+163
-68
application.js
src/config/application.js
+1
-0
shell.css
src/design-system/shell.css
+15
-0
Layout.jsx
src/pages/integrity_practice_management/Layout.jsx
+8
-1
integrity-practice-management.business-flow.md
...management/integrity-practice-management.business-flow.md
+1
-0
No files found.
docs/ui-architecture.md
View file @
0f9e79a0
...
@@ -20,6 +20,7 @@ app.js (主题初始化)
...
@@ -20,6 +20,7 @@ app.js (主题初始化)
-
`src/config/application.js`
:应用名、base、主题存储键。
-
`src/config/application.js`
:应用名、base、主题存储键。
-
`src/design-system/`
:权威主题、Tokens 和跨应用 Shell 样式;页面不得复制这些规则。
-
`src/design-system/`
:权威主题、Tokens 和跨应用 Shell 样式;页面不得复制这些规则。
-
`AppShell/DashboardSidebar/DashboardNavbar/ApplicationSwitcherDialog`
:业务无关的共享壳层。
-
`AppShell/DashboardSidebar/DashboardNavbar/ApplicationSwitcherDialog`
:业务无关的共享壳层。
-
`ApplicationSwitcherDialog`
从既有
`SiteContext.site.group`
读取动态分组,从
`GET /application/info`
与
`UserContext.applications`
合并真实可访问应用;分组仅影响呈现,不改变路由、会话或跳转。
-
`SubjectWorkbench`
:把现有 Subject/View/动作配置适配为工作台 props,不定义组件外观。
-
`SubjectWorkbench`
:把现有 Subject/View/动作配置适配为工作台 props,不定义组件外观。
-
`record-workbench`
:DataGrid/Pagination/ActionBar 组合;详情能力统一映射为操作列首个预览图标,首个业务列仅展示文本;不发请求、不解释业务动作。
-
`record-workbench`
:DataGrid/Pagination/ActionBar 组合;详情能力统一映射为操作列首个预览图标,首个业务列仅展示文本;不发请求、不解释业务动作。
-
`RecordDetailDrawer`
:详情草稿、Modal 状态和字段编排;保存仍由
`useSubjectRecord`
处理。
-
`RecordDetailDrawer`
:详情草稿、Modal 状态和字段编排;保存仍由
`useSubjectRecord`
处理。
...
...
docs/ui-migration-checklist.md
View file @
0f9e79a0
...
@@ -13,7 +13,7 @@
...
@@ -13,7 +13,7 @@
-
[
x
]
登录、鉴权、真实退出
-
[
x
]
登录、鉴权、真实退出
-
[
x
]
Pro Sidebar/Navbar/offcanvas/mobile
-
[
x
]
Pro Sidebar/Navbar/offcanvas/mobile
-
[
x
]
HeroUI 应用切换 Modal:
46rem/42rem、桌面两列/移动单列、12px 列间距
、API Logo/首字 identity、唯一当前态;用户 Dropdown
-
[
x
]
HeroUI 应用切换 Modal:
model_analysis 基准、46rem/42rem、28px inset、动态分组与真实计数、桌面两列/移动单列
、API Logo/首字 identity、唯一当前态;用户 Dropdown
-
[
x
]
七条 SubjectWorkbench 路由
-
[
x
]
七条 SubjectWorkbench 路由
-
[
x
]
Tabs、SearchField/Surface 筛选
-
[
x
]
Tabs、SearchField/Surface 筛选
-
[
x
]
Pro DataGrid 选择、排序、固定列、横向滚动
-
[
x
]
Pro DataGrid 选择、排序、固定列、横向滚动
...
...
docs/xinyuan-design-system.md
View file @
0f9e79a0
...
@@ -17,7 +17,7 @@ Inter 禁止通过 `src/tailwind.css` 导入。Tailwind v4 内容扫描必须排
...
@@ -17,7 +17,7 @@ Inter 禁止通过 `src/tailwind.css` 导入。Tailwind v4 内容扫描必须排
-
Shell:
`@heroui-pro/react/sidebar`
、
`navbar`
;桌面 offcanvas 真折叠,移动端
`Sidebar.Mobile`
。
-
Shell:
`@heroui-pro/react/sidebar`
、
`navbar`
;桌面 offcanvas 真折叠,移动端
`Sidebar.Mobile`
。
-
全局操作:HeroUI Button/Tooltip/Dropdown;用户菜单的退出项调用真实登出回调。
-
全局操作:HeroUI Button/Tooltip/Dropdown;用户菜单的退出项调用真实登出回调。
-
应用切换:
HeroUI Modal + Card/Surface/Avatar/Chip;最大 46rem x 42rem,桌面两列、移动端单列,列间距 12px。应用项使用 HeroUI Card 默认 24px 圆角与 16px 内边距;API 有 Logo 时显示真实 Logo,无 Logo 时显示应用首字,不得给所有应用重复套用鑫元 Logo
。
-
应用切换:
严格使用 model_analysis 当前 HeroUI Modal + Card/Surface/Avatar/Chip 基准;最大 46rem x 42rem、28px inset、24px 圆角,桌面两列、移动端单列,间距 12px。分组来自
`site.group`
,保持后端分组与组内顺序并显示真实数量;未分组应用进入“其他应用”,无配置时回退“全部应用”。API 有 Logo 时显示真实 Logo,无 Logo 时显示应用首字,当前应用只显示一个 accent Chip
。
-
数据视图:HeroUI Tabs;不手写 segmented/tab 外观。
-
数据视图:HeroUI Tabs;不手写 segmented/tab 外观。
-
筛选:HeroUI Surface + SearchField/Autocomplete/DatePicker/Disclosure。没有业务配置的筛选不得添加。
-
筛选:HeroUI Surface + SearchField/Autocomplete/DatePicker/Disclosure。没有业务配置的筛选不得添加。
-
列表:HeroUI Pro DataGrid、EmptyState、ActionBar + HeroUI Pagination;分页固定沿用业务默认每页 10 条,不显示额外页容量选择器。首列只在真实横向溢出时固定且仅展示文本,详情入口统一为操作列首个预览图标。操作列宽度由 32px 动作按钮、8px 间距和标准单元格内边距计算,
`minWidth/width/maxWidth`
必须一致;表格未横向溢出时首个业务列使用
`1fr`
吸收剩余宽度,操作列不得参与余量分配;仅在横向溢出时固定在右端。
-
列表:HeroUI Pro DataGrid、EmptyState、ActionBar + HeroUI Pagination;分页固定沿用业务默认每页 10 条,不显示额外页容量选择器。首列只在真实横向溢出时固定且仅展示文本,详情入口统一为操作列首个预览图标。操作列宽度由 32px 动作按钮、8px 间距和标准单元格内边距计算,
`minWidth/width/maxWidth`
必须一致;表格未横向溢出时首个业务列使用
`1fr`
吸收剩余宽度,操作列不得参与余量分配;仅在横向溢出时固定在右端。
...
...
docs/xinyuan-ui-audit.md
View file @
0f9e79a0
...
@@ -45,7 +45,7 @@
...
@@ -45,7 +45,7 @@
-
登录:已使用基准居中 Card、Logo、字段高度、24px 节奏、验证码、filled/focus/autofill 状态;保留原登录协议。
-
登录:已使用基准居中 Card、Logo、字段高度、24px 节奏、验证码、filled/focus/autofill 状态;保留原登录协议。
-
Shell:HeroUI Pro
`Sidebar.Provider collapsible="offcanvas"`
、Sidebar、Mobile、Trigger、Navbar 与真实用户 Dropdown/登出。
-
Shell:HeroUI Pro
`Sidebar.Provider collapsible="offcanvas"`
、Sidebar、Mobile、Trigger、Navbar 与真实用户 Dropdown/登出。
-
应用切换:
HeroUI Modal,46rem/42rem 合同,桌面两列、移动端单列,当前应用状
态与 identity well。
-
应用切换:
以 model_analysis 当前实现为唯一基准,HeroUI Modal 46rem/42rem、28px inset、桌面两列/移动端单列;复用
`SiteContext.site.group`
动态分组,显示真实计数,并保留唯一当前
态与 identity well。
-
Tabs/筛选:HeroUI Tabs、SearchField、Surface;当前业务仅配置标题关键词,不添加虚构筛选字段。
-
Tabs/筛选:HeroUI Tabs、SearchField、Surface;当前业务仅配置标题关键词,不添加虚构筛选字段。
-
列表:Pro DataGrid、原生多选/排序/列宽/固定列/横向滚动、HeroUI Pagination、Pro ActionBar/EmptyState;分页固定每页 10 条并按问题整改基准移除页容量选择器;4 个以上动作只直出前两个,其余进入 Dropdown,危险动作最后。
-
列表:Pro DataGrid、原生多选/排序/列宽/固定列/横向滚动、HeroUI Pagination、Pro ActionBar/EmptyState;分页固定每页 10 条并按问题整改基准移除页容量选择器;4 个以上动作只直出前两个,其余进入 Dropdown,危险动作最后。
-
弹窗:HeroUI Modal/AlertDialog;详情查看态单一 secondary Surface;指派保留远程用户查询和 ID 提交。
-
弹窗:HeroUI Modal/AlertDialog;详情查看态单一 secondary Surface;指派保留远程用户查询和 ID 提交。
...
@@ -69,7 +69,9 @@
...
@@ -69,7 +69,9 @@
操作列宽度回归复测:与运行中的“问题整改”在 1440x900 同视口比较,基准操作列为 144px。当前主表恢复同一列宽分配规则:未横向溢出时由首个业务列
`1fr`
吸收剩余宽度,操作列保持 144px;实测操作列
`x=1244..1388`
,内容区
`x=1260..1372`
,左右内边距均为 16px,三个 32px 按钮间距均为 8px。1440x900、1280x720、768x1024、390x844 浅/深色均复测,操作列在每个视口都为 144px;窄视口横向溢出态固定在 DataGrid 右端,四个视口
`document.body.scrollWidth === innerWidth`
。
操作列宽度回归复测:与运行中的“问题整改”在 1440x900 同视口比较,基准操作列为 144px。当前主表恢复同一列宽分配规则:未横向溢出时由首个业务列
`1fr`
吸收剩余宽度,操作列保持 144px;实测操作列
`x=1244..1388`
,内容区
`x=1260..1372`
,左右内边距均为 16px,三个 32px 按钮间距均为 8px。1440x900、1280x720、768x1024、390x844 浅/深色均复测,操作列在每个视口都为 144px;窄视口横向溢出态固定在 DataGrid 右端,四个视口
`document.body.scrollWidth === innerWidth`
。
应用切换专项复测:对照“问题整改”的 46rem/42rem Modal、Header、24px 关闭按钮和 36px identity 几何,使用桌面两列、移动端单列。1440x900 下两列应用项实测为 338x72px,HeroUI Card 使用默认 24px 圆角和 16px 内边距,网格间距 12px;移除无业务 Logo 时统一显示鑫元 Logo 的错误兜底,恢复 API Logo / 应用首字规则;只显示一个“当前”。390x844 下应用项为 310x72px,正文独立滚动,
`document.body.scrollWidth === innerWidth === 390`
;浅深色均已验证。
应用切换专项基准更新:应用切换改以 model_analysis 当前实现为唯一基准。结构使用 HeroUI Modal compound API,分组直接读取既有
`GET /info -> site.group`
,应用仍由
`GET /application/info`
与登录用户可访问应用合并;保持后端分组及组内顺序、过滤空组和无权限项、显示真实数量,未引用项进入“其他应用”,无分组配置时回退“全部应用”。固定 46rem/42rem、28px inset、24px Modal 圆角、24px/12px 分组节奏、sm 起两列、移动一列、36px identity well,并确保只有一个“当前” Chip。
应用切换浏览器验收:同时打开目标
`http://localhost:8002/integrity_practice_management/`
与基准
`http://localhost:8003/model_analysis/`
,在 1440x900、1280x720、768x1024、390x844 的浅色和深色模式逐项比较。1440x900 实测两侧均为 736x672px、24px 圆角、28px inset、Header 56px、Header/Body 间距 12px、关闭按钮 24x24px 且距右上各 28px、两列各 334px且间距 12px;1280x720 为 736x640px,768x1024 仍为两列,390x844 为 358x672px、左右各 16px 且单列 302px。各视口 Body 均独立滚动,页面横向滚动宽度等于视口宽度。Esc、关闭按钮和遮罩均可关闭,焦点回到“切换应用”入口;打开后 Tab 顺序从 Dialog 到关闭按钮。无
`site.group`
时验证只回退一个“全部应用”分组;
`GET /application/info`
失败时验证 warning 与登录用户应用回退;透明 Logo 保持原图,损坏或缺失 Logo 回退首字,当前应用使用鑫元 Logo,四类 identity well 均为 36x36px;全部路径都保持唯一当前态。全部稳定帧截图存放在
`/tmp/integrity-switcher-qa.*`
,未写入源码或 Git。
操作图标三层几何专项复测:清除 HeroUI
`.button svg`
默认的
`4px -2px`
margin,并把表格操作按钮在桌面和移动端都锁定为 32x32 正圆。
`button`
、Ant
`.anticon`
和内部
`svg`
逐层实测为 32x32 / 16x16 / 16x16,内层 SVG margin 为 0,水平及垂直圆心偏差均为 0px。
操作图标三层几何专项复测:清除 HeroUI
`.button svg`
默认的
`4px -2px`
margin,并把表格操作按钮在桌面和移动端都锁定为 32x32 正圆。
`button`
、Ant
`.anticon`
和内部
`svg`
逐层实测为 32x32 / 16x16 / 16x16,内层 SVG margin 为 0,水平及垂直圆心偏差均为 0px。
...
@@ -81,7 +83,7 @@
...
@@ -81,7 +83,7 @@
退出登录使用独立
`logout-proof`
会话复测:点击 Sidebar 用户菜单的“退出登录”后,
`POST /api/auth/logout`
返回 200,页面跳转到
`/integrity_practice_management/login`
,
`current_user`
与
`auth_token`
同时从 localStorage 删除,仅保留非鉴权主题键;控制台 0 error。此前旧验收会话中登录页仍带
`current_user`
是会话夹具残留,不是产品退出流程的持久化行为。
退出登录使用独立
`logout-proof`
会话复测:点击 Sidebar 用户菜单的“退出登录”后,
`POST /api/auth/logout`
返回 200,页面跳转到
`/integrity_practice_management/login`
,
`current_user`
与
`auth_token`
同时从 localStorage 删除,仅保留非鉴权主题键;控制台 0 error。此前旧验收会话中登录页仍带
`current_user`
是会话夹具残留,不是产品退出流程的持久化行为。
新页面重载后控制台:0 error;保留 1
条开发环境 warning
。曾发现并修复
`ComboBox.InputGroup fullWidth`
DOM 透传错误,以及 checkbox 行点击误开详情的交互冲突。
新页面重载后控制台:0 error;保留 1
种既有
`PressResponder`
开发环境 warning,在 React StrictMode 下打印两次,应用切换打开/关闭没有新增日志
。曾发现并修复
`ComboBox.InputGroup fullWidth`
DOM 透传错误,以及 checkbox 行点击误开详情的交互冲突。
## 未挂载历史边界
## 未挂载历史边界
...
...
src/components/ApplicationSwitcherDialog.jsx
View file @
0f9e79a0
import
xinyuanLogo
from
'@/assets/xy-logo-red.png'
;
import
{
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
APPLICATION_DISPLAY_NAME
,
APPLICATION_ROUTE_NAME
}
from
'@/config/application'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
getApplications
}
from
'@/services/DataService'
;
import
{
Close
,
ExclamationTriangleIcon
}
from
'@/components/AppIcons'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Avatar
,
Card
,
Chip
,
Description
,
Modal
,
Skeleton
,
Surface
}
from
'@heroui/react'
;
import
{
Alert
,
Avatar
,
Card
,
Chip
,
Description
,
Modal
,
Skeleton
,
Surface
}
from
'@heroui/react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
function
normalizeApplication
(
application
,
fallback
=
{})
{
function
unwrapApplications
(
response
)
{
const
name
=
application
?.
name
||
fallback
?.
name
||
''
;
for
(
const
candidate
of
[
response
,
response
?.
data
,
response
?.
applications
,
response
?.
apps
,
response
?.
records
,
response
?.
rows
,
response
?.
list
,
response
?.
result
])
{
if
(
Array
.
isArray
(
candidate
))
return
candidate
;
}
return
[];
}
function
normalizeApplication
(
application
=
{})
{
const
name
=
application
.
name
||
''
;
const
current
=
name
===
APPLICATION_ROUTE_NAME
;
return
{
return
{
name
,
name
,
title
:
application
?.
title
||
fallback
?.
title
||
name
||
'未命名应用'
,
title
:
current
?
APPLICATION_DISPLAY_NAME
:
application
.
title
||
name
||
'未命名应用'
,
description
:
application
?.
description
||
fallback
?.
description
||
name
||
'应用'
,
description
:
application
.
description
||
application
.
subtitle
||
application
.
title
||
'进入对应应用继续处理业务'
,
logo
:
application
?.
logo
||
fallback
?.
logo
||
null
,
logo
:
current
?
xinyuanLogo
:
application
.
logo
||
application
.
icon
||
null
,
url
:
application
?.
url
||
(
name
?
`/
${
name
}
`
:
'#'
),
order
:
Number
(
application
.
order
||
0
),
url
:
application
.
url
||
application
.
path
||
(
name
?
`/
${
name
}
`
:
'#'
),
};
};
}
}
function
mergeApplications
(
fetched
,
fallback
)
{
function
mergeApplications
(
fetched
,
fallbacks
)
{
const
fallbackMap
=
new
Map
(
fallback
.
map
((
item
)
=>
[
item
.
name
,
item
]));
const
merged
=
new
Map
();
const
merged
=
new
Map
();
fetched
.
forEach
((
item
)
=>
{
[...
fallbacks
,
...
fetched
,
{
name
:
APPLICATION_ROUTE_NAME
,
title
:
APPLICATION_DISPLAY_NAME
}]
const
normalized
=
normalizeApplication
(
item
,
fallbackMap
.
get
(
item
?.
name
));
.
map
(
normalizeApplication
)
if
(
normalized
.
name
)
merged
.
set
(
normalized
.
name
,
normalized
);
.
filter
((
application
)
=>
application
.
name
)
});
.
forEach
((
application
)
=>
merged
.
set
(
application
.
name
,
{
...
merged
.
get
(
application
.
name
),
...
application
}));
fallback
.
forEach
((
item
)
=>
{
return
[...
merged
.
values
()].
sort
((
left
,
right
)
=>
left
.
order
-
right
.
order
||
left
.
title
.
localeCompare
(
right
.
title
,
'zh-CN'
));
if
(
item
.
name
&&
!
merged
.
has
(
item
.
name
))
merged
.
set
(
item
.
name
,
item
);
}
});
return
Array
.
from
(
merged
.
values
());
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-
${
index
}
-
${
group
?.
label
||
'applications'
}
`,
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) {
function isCurrentApplication(application) {
if (application.name === APPLICATION_ROUTE_NAME) return true;
if (typeof window === 'undefined') return false;
if (typeof window === 'undefined') return false;
const
root
=
application
?.
name
?
`/
${
application
.
name
}
`
:
''
;
try {
return
Boolean
(
root
&&
(
window
.
location
.
pathname
===
root
||
window
.
location
.
pathname
.
startsWith
(
`
${
root
}
/`
)));
const target = new URL(application.url, window.location.origin);
const root = application.name ? `
/
$
{
application
.
name
}
` : target.pathname;
return window.location.pathname === root || window.location.pathname.startsWith(`
$
{
root
}
/`
)
;
}
catch
{
return
false
;
}
}
}
function
ApplicationCard
({
application
})
{
function
ApplicationCard
({
application
,
current
})
{
const
current
=
isCurrentApplication
(
application
);
const
target
=
/^https
?
:
\/\/
/i
.
test
(
application
.
url
)
?
'_blank'
:
undefined
;
const
external
=
application
.
url
?.
startsWith
(
'http'
);
const
initial
=
application
.
title
.
trim
().
slice
(
0
,
1
).
toUpperCase
()
||
'A'
;
const
initial
=
application
.
title
.
trim
().
slice
(
0
,
1
).
toUpperCase
()
||
'A'
;
return
(
return
(
<
Card
className=
"h-full"
variant=
"secondary"
>
<
Card
className=
"h-full"
variant=
"secondary"
>
...
@@ -45,18 +98,18 @@ function ApplicationCard({ application }) {
...
@@ -45,18 +98,18 @@ function ApplicationCard({ application }) {
aria
-
current=
{
current
?
'page'
:
undefined
}
aria
-
current=
{
current
?
'page'
:
undefined
}
className=
"flex min-w-0 items-center gap-3 no-underline"
className=
"flex min-w-0 items-center gap-3 no-underline"
href=
{
application
.
url
}
href=
{
application
.
url
}
rel=
{
external
?
'noreferrer'
:
undefined
}
rel=
{
target
?
'noreferrer'
:
undefined
}
target=
{
external
?
'_blank'
:
undefined
}
target=
{
target
}
>
>
<
Surface
aria
-
hidden=
"true"
className=
"
grid size-9 shrink-0 place-items-center overflow-hidden rounded-[var(--rc-radius-control)]
"
variant=
"tertiary"
>
<
Surface
aria
-
hidden=
"true"
className=
"
rc-app-identity-well grid size-9 shrink-0 place-items-center overflow-hidden
"
variant=
"tertiary"
>
<
Avatar
className=
"
size-full rounded-[var(--rc-radius-control)] bg-transparent
"
color=
"accent"
variant=
"soft"
>
<
Avatar
className=
"
rc-app-identity-avatar size-full
"
color=
"accent"
variant=
"soft"
>
{
application
.
logo
?
<
Avatar
.
Image
className=
"object-contain p-1.5"
src=
{
application
.
logo
}
alt=
""
/>
:
null
}
{
application
.
logo
?
<
Avatar
.
Image
alt=
""
className=
"object-contain p-1.5"
src=
{
application
.
logo
}
/>
:
null
}
<
Avatar
.
Fallback
>
{
initial
}
</
Avatar
.
Fallback
>
<
Avatar
.
Fallback
>
{
initial
}
</
Avatar
.
Fallback
>
</
Avatar
>
</
Avatar
>
</
Surface
>
</
Surface
>
<
span
className=
"min-w-0 flex-1"
>
<
span
className=
"min-w-0 flex-1"
>
<
span
className=
"block
break-words font-medium leading-5 text-foreground
"
title=
{
application
.
title
}
>
{
application
.
title
}
</
span
>
<
span
className=
"block
truncate text-sm font-medium leading-5
"
title=
{
application
.
title
}
>
{
application
.
title
}
</
span
>
<
span
className=
"block truncate text-sm text-muted"
title=
{
application
.
description
}
>
{
application
.
description
||
application
.
name
}
</
spa
n
>
<
Description
className=
"mt-0.5 block truncate leading-4"
title=
{
application
.
description
}
>
{
application
.
description
}
</
Descriptio
n
>
</
span
>
</
span
>
{
current
?
<
Chip
color=
"accent"
size=
"sm"
>
当前
</
Chip
>
:
null
}
{
current
?
<
Chip
color=
"accent"
size=
"sm"
>
当前
</
Chip
>
:
null
}
</
a
>
</
a
>
...
@@ -65,62 +118,104 @@ function ApplicationCard({ application }) {
...
@@ -65,62 +118,104 @@ function ApplicationCard({ application }) {
);
);
}
}
export
default
function
ApplicationSwitcherDialog
({
open
,
onClose
,
userInfo
})
{
function
LoadingSkeleton
()
{
const
fallback
=
useMemo
(
return
(
()
=>
(
userInfo
?.
applications
||
[])
<
div
aria
-
label=
"正在加载应用"
className=
"grid gap-3 sm:grid-cols-2"
>
.
map
((
item
)
=>
normalizeApplication
(
item
,
{
...
item
,
description
:
item
?.
title
}))
{
Array
.
from
({
length
:
6
}).
map
((
_
,
index
)
=>
(
.
filter
((
item
)
=>
item
.
name
),
<
Card
key=
{
index
}
variant=
"secondary"
>
<
Card
.
Content
className=
"flex items-center gap-3"
>
<
Skeleton
className=
"size-9"
/>
<
div
className=
"flex-1 space-y-2"
><
Skeleton
className=
"h-4 w-2/3"
/><
Skeleton
className=
"h-3 w-full"
/></
div
>
</
Card
.
Content
>
</
Card
>
))
}
</
div
>
);
}
export
default
function
ApplicationSwitcherDialog
({
open
,
onClose
,
siteInfo
,
userInfo
})
{
const
fallbacks
=
useMemo
(
()
=>
(
Array
.
isArray
(
userInfo
?.
applications
)
?
userInfo
.
applications
:
[]),
[
userInfo
],
[
userInfo
],
);
);
const
[
applications
,
setApplications
]
=
useState
(
fallback
);
const
[
applications
,
setApplications
]
=
useState
(()
=>
mergeApplications
([],
fallbacks
));
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
status
,
setStatus
]
=
useState
(
'idle'
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
errorMessage
,
setErrorMessage
]
=
useState
(
''
);
const
currentApplicationName
=
useMemo
(
()
=>
applications
.
find
(
isCurrentApplication
)?.
name
||
APPLICATION_ROUTE_NAME
,
[
applications
],
);
const
applicationSections
=
useMemo
(
()
=>
buildApplicationSections
(
applications
,
siteInfo
?.
group
),
[
applications
,
siteInfo
?.
group
],
);
const
currentApplicationKey
=
useMemo
(()
=>
{
for
(
const
section
of
applicationSections
)
{
const
applicationIndex
=
section
.
applications
.
findIndex
(
(
application
)
=>
application
.
name
===
currentApplicationName
,
);
if
(
applicationIndex
>=
0
)
return
`
${
section
.
key
}
:
${
applicationIndex
}
:
${
currentApplicationName
}
`
;
}
return
''
;
},
[
applicationSections
,
currentApplicationName
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
!
open
)
return
undefined
;
if
(
!
open
)
return
undefined
;
let
cancelled
=
false
;
let
cancelled
=
false
;
set
Loading
(
true
);
set
Status
((
previous
)
=>
(
previous
===
'success'
?
previous
:
'loading'
)
);
setError
(
''
);
setError
Message
(
''
);
getApplications
()
getApplications
()
.
then
((
response
)
=>
{
.
then
((
response
)
=>
{
if
(
!
cancelled
)
setApplications
(
mergeApplications
(
Array
.
isArray
(
response
)
?
response
:
[],
fallback
));
if
(
cancelled
)
return
;
setApplications
(
mergeApplications
(
unwrapApplications
(
response
),
fallbacks
));
setStatus
(
'success'
);
})
})
.
catch
(()
=>
{
.
catch
(()
=>
{
if
(
!
cancelled
)
{
if
(
cancelled
)
return
;
setApplications
(
fallback
);
setApplications
(
mergeApplications
([],
fallbacks
));
setError
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
setStatus
(
'error'
);
}
setErrorMessage
(
'应用列表加载失败,已回退展示当前账号上的应用信息。'
);
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
});
return
()
=>
{
return
()
=>
{
cancelled
=
true
;
};
cancelled
=
true
;
},
[
fallbacks
,
open
]);
};
},
[
fallback
,
open
]);
return
(
return
(
<
Modal
.
Root
>
<
Modal
.
Root
>
<
Modal
.
Trigger
aria
-
hidden=
"true"
className=
"sr-only"
tabIndex=
{
-
1
}
/>
<
Modal
.
Backdrop
isOpen=
{
open
}
variant=
"blur"
onOpenChange=
{
(
isOpen
)
=>
!
isOpen
&&
onClose
?.()
}
>
<
Modal
.
Backdrop
isOpen=
{
open
}
variant=
"blur"
onOpenChange=
{
(
isOpen
)
=>
!
isOpen
&&
onClose
?.()
}
>
<
Modal
.
Container
className=
"w-full sm:w-full"
placement=
"center"
scroll=
"inside"
size=
"lg"
>
<
Modal
.
Container
placement=
"center"
scroll=
"inside"
size=
"lg"
>
<
Modal
.
Dialog
<
Modal
.
Dialog
aria
-
label=
"应用切换"
className=
"rc-app-switcher-dialog p-7"
>
className=
"max-h-[min(42rem,calc(100dvh-2rem))]"
<
Modal
.
CloseTrigger
style=
{
{
maxWidth
:
'min(46rem, calc(100vw - 2rem))'
}
}
aria
-
label=
"关闭"
>
style=
{
{
insetInlineEnd
:
'1.75rem'
,
top
:
'1.75rem'
}
}
<
Modal
.
CloseTrigger
aria
-
label=
"关闭"
><
Close
size=
{
16
}
/></
Modal
.
CloseTrigger
>
/
>
<
Modal
.
Header
>
<
Modal
.
Header
>
<
div
className=
"min-w-0"
><
Modal
.
Heading
>
应用切换
</
Modal
.
Heading
><
Description
>
选择你可访问的应用
</
Description
></
div
>
<
Modal
.
Heading
>
应用切换
</
Modal
.
Heading
>
<
Description
className=
"leading-5"
>
选择你可访问的应用
</
Description
>
</
Modal
.
Header
>
</
Modal
.
Header
>
<
Modal
.
Body
>
<
Modal
.
Body
style=
{
{
marginTop
:
'0.75rem'
}
}
>
{
error
?
<
Alert
status=
"warning"
><
Alert
.
Indicator
><
ExclamationTriangleIcon
size=
{
20
}
/></
Alert
.
Indicator
><
Alert
.
Content
><
Alert
.
Description
>
{
error
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
status
===
'error'
&&
errorMessage
?
<
Alert
status=
"warning"
><
Alert
.
Indicator
><
ExclamationTriangleIcon
size=
{
20
}
/></
Alert
.
Indicator
><
Alert
.
Content
><
Alert
.
Description
>
{
errorMessage
}
</
Alert
.
Description
></
Alert
.
Content
></
Alert
>
:
null
}
{
loading
&&
applications
.
length
===
0
?
(
{
status
===
'loading'
&&
applications
.
length
===
0
?
<
LoadingSkeleton
/>
:
applications
.
length
>
0
?
(
<
div
aria
-
label=
"正在加载应用"
className=
"grid gap-3 sm:grid-cols-2"
>
<
div
className=
"space-y-6"
>
{
Array
.
from
({
length
:
6
}).
map
((
_
,
index
)
=>
<
Card
className=
"h-full"
key=
{
index
}
variant=
"secondary"
><
Card
.
Content
className=
"flex items-center gap-3"
><
Skeleton
className=
"size-9"
/><
div
className=
"flex-1 space-y-2"
><
Skeleton
className=
"h-4 w-2/3"
/><
Skeleton
className=
"h-3 w-full"
/></
div
></
Card
.
Content
></
Card
>)
}
{
applicationSections
.
map
((
section
)
=>
(
</
div
>
<
section
key=
{
section
.
key
}
>
)
:
applications
.
length
>
0
?
(
<
div
className=
"mb-3 flex items-center justify-between gap-3"
>
<
div
className=
"grid gap-3 sm:grid-cols-2"
>
<
h3
className=
"text-sm font-medium leading-5"
>
{
section
.
label
}
</
h3
>
{
applications
.
map
((
application
)
=>
<
ApplicationCard
application=
{
application
}
key=
{
application
.
name
}
/>)
}
<
Description
>
{
section
.
applications
.
length
}
个应用
</
Description
>
</
div
>
<
div
className=
"grid gap-3 sm:grid-cols-2"
>
{
section
.
applications
.
map
((
application
,
applicationIndex
)
=>
{
const
applicationKey
=
`${section.key}:${applicationIndex}:${application.name}`
;
return
(
<
ApplicationCard
application=
{
application
}
current=
{
applicationKey
===
currentApplicationKey
}
key=
{
applicationKey
}
/>
);
})
}
</
div
>
</
section
>
))
}
</
div
>
</
div
>
)
:
(
)
:
(
<
EmptyState
size=
"md"
><
EmptyState
.
Header
><
EmptyState
.
Title
>
暂无可切换应用
</
EmptyState
.
Title
><
EmptyState
.
Description
>
当前账号还没有分配其它应用权限。
</
EmptyState
.
Description
></
EmptyState
.
Header
></
EmptyState
>
<
EmptyState
size=
"md"
><
EmptyState
.
Header
><
EmptyState
.
Title
>
暂无可切换应用
</
EmptyState
.
Title
><
EmptyState
.
Description
>
当前账号还没有分配其它应用权限。
</
EmptyState
.
Description
></
EmptyState
.
Header
></
EmptyState
>
...
...
src/config/application.js
View file @
0f9e79a0
export
const
APPLICATION_DISPLAY_NAME
=
'廉洁从业管理'
;
export
const
APPLICATION_DISPLAY_NAME
=
'廉洁从业管理'
;
export
const
APPLICATION_ROUTE_NAME
=
'integrity_practice_management'
;
export
const
APPLICATION_BASE_PATH
=
'/integrity_practice_management/'
;
export
const
APPLICATION_BASE_PATH
=
'/integrity_practice_management/'
;
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
...
...
src/design-system/shell.css
View file @
0f9e79a0
...
@@ -539,6 +539,21 @@
...
@@ -539,6 +539,21 @@
margin-top
:
1.5rem
;
margin-top
:
1.5rem
;
}
}
.rc-app-switcher-dialog
{
width
:
min
(
46rem
,
calc
(
100vw
-
2rem
));
max-width
:
min
(
46rem
,
calc
(
100vw
-
2rem
));
max-height
:
min
(
42rem
,
calc
(
100
dvh
-
2rem
));
}
.rc-app-identity-well
,
.rc-app-identity-avatar
{
border-radius
:
var
(
--rc-radius-control
);
}
.rc-app-identity-avatar
{
background
:
transparent
;
}
.rc-detail-surface.surface--secondary
{
.rc-detail-surface.surface--secondary
{
border-radius
:
var
(
--rc-radius-detail-surface
);
border-radius
:
var
(
--rc-radius-detail-surface
);
}
}
...
...
src/pages/integrity_practice_management/Layout.jsx
View file @
0f9e79a0
...
@@ -3,6 +3,7 @@ import { Archive, CheckSquare, FileDone, FileText, Settings } from '@/components
...
@@ -3,6 +3,7 @@ import { Archive, CheckSquare, FileDone, FileText, Settings } from '@/components
import
ApplicationSwitcherDialog
from
'@/components/ApplicationSwitcherDialog'
;
import
ApplicationSwitcherDialog
from
'@/components/ApplicationSwitcherDialog'
;
import
{
APPLICATION_DISPLAY_NAME
,
ROUTE_TITLES
}
from
'@/config/application'
;
import
{
APPLICATION_DISPLAY_NAME
,
ROUTE_TITLES
}
from
'@/config/application'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
Outlet
,
useLocation
}
from
'umi'
;
import
{
Outlet
,
useLocation
}
from
'umi'
;
import
{
INTEGRITY_PRACTICE_MENU
}
from
'./integrityPracticeConfig'
;
import
{
INTEGRITY_PRACTICE_MENU
}
from
'./integrityPracticeConfig'
;
...
@@ -21,6 +22,7 @@ const MENU_GROUPS = INTEGRITY_PRACTICE_MENU.map((group, index) => ({
...
@@ -21,6 +22,7 @@ const MENU_GROUPS = INTEGRITY_PRACTICE_MENU.map((group, index) => ({
export
default
function
IntegrityPracticeLayout
()
{
export
default
function
IntegrityPracticeLayout
()
{
const
location
=
useLocation
();
const
location
=
useLocation
();
const
userInfo
=
useContext
(
UserContext
);
const
userInfo
=
useContext
(
UserContext
);
const
siteInfo
=
useContext
(
SiteContext
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
switcherOpen
,
setSwitcherOpen
]
=
useState
(
false
);
const
[
switcherOpen
,
setSwitcherOpen
]
=
useState
(
false
);
const
pageTitle
=
useMemo
(
const
pageTitle
=
useMemo
(
...
@@ -46,7 +48,12 @@ export default function IntegrityPracticeLayout() {
...
@@ -46,7 +48,12 @@ export default function IntegrityPracticeLayout() {
>
>
<
Outlet
/>
<
Outlet
/>
</
AppShell
>
</
AppShell
>
<
ApplicationSwitcherDialog
open=
{
switcherOpen
}
onClose=
{
()
=>
setSwitcherOpen
(
false
)
}
userInfo=
{
userInfo
}
/>
<
ApplicationSwitcherDialog
open=
{
switcherOpen
}
siteInfo=
{
siteInfo
}
userInfo=
{
userInfo
}
onClose=
{
()
=>
setSwitcherOpen
(
false
)
}
/>
</>
</>
);
);
}
}
src/pages/integrity_practice_management/integrity-practice-management.business-flow.md
View file @
0f9e79a0
...
@@ -6,6 +6,7 @@
...
@@ -6,6 +6,7 @@
-
默认入口:
`/self_check_the_results_of_the_working_draft`
-
默认入口:
`/self_check_the_results_of_the_working_draft`
-
页面菜单与 Subject 动作配置:
`integrityPracticeConfig.js`
-
页面菜单与 Subject 动作配置:
`integrityPracticeConfig.js`
-
页面统一使用鑫元 HeroUI Pro Shell、
`RecordWorkbench`
和
`RecordDetailDrawer`
;Sidebar 为 offcanvas,移动端通过可访问性 Dialog 打开
-
页面统一使用鑫元 HeroUI Pro Shell、
`RecordWorkbench`
和
`RecordDetailDrawer`
;Sidebar 为 offcanvas,移动端通过可访问性 Dialog 打开
-
应用切换继续读取现有
`GET /application/info`
和登录用户可访问应用,并按
`GET /info`
返回的
`site.group`
动态分组;未分组的可访问应用进入“其他应用”,无分组配置时回退“全部应用”,不改变鉴权、会话或跳转协议
-
Subject 字段、Select 选项、列表视图和详情视图均从后端动态读取,页面不维护字段副本或硬编码选项
-
Subject 字段、Select 选项、列表视图和详情视图均从后端动态读取,页面不维护字段副本或硬编码选项
| 菜单分组 | 页面 | Subject |
| 菜单分组 | 页面 | Subject |
...
...
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