Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
problem_rectification
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
problem_rectification
Commits
fa6182cc
Commit
fa6182cc
authored
Aug 26, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: align view permissions and add notifications
parent
ba8d2186
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
1074 additions
and
92 deletions
+1074
-92
.umirc.ts
.umirc.ts
+5
-0
xinyuan-design-system.md
docs/xinyuan-design-system.md
+3
-0
AppIcons.jsx
src/components/AppIcons.jsx
+2
-0
DashboardSidebar.jsx
src/components/DashboardSidebar.jsx
+8
-2
index.jsx
src/components/record-workbench/index.jsx
+23
-1
application.js
src/config/application.js
+2
-0
SubjectViewContent.jsx
...ages/RecordDetailDrawer/components/SubjectViewContent.jsx
+3
-3
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+5
-5
Layout.jsx
src/pages/problem_rectification/Layout.jsx
+50
-1
archiveWorkbenchService.js
.../problem_rectification/archive/archiveWorkbenchService.js
+26
-16
index.jsx
src/pages/problem_rectification/list/index.jsx
+79
-11
NotificationListPage.jsx
...blem_rectification/notifications/NotificationListPage.jsx
+424
-0
notificationService.js
...roblem_rectification/notifications/notificationService.js
+180
-0
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+43
-8
problemRectificationListService.js
.../problem_rectification/problemRectificationListService.js
+26
-13
record-api.md
src/services/record-api.md
+46
-0
subjectsDefinition.md
src/services/subjectsDefinition.md
+23
-0
subjectView.js
src/utils/subjectView.js
+126
-32
No files found.
.umirc.ts
View file @
fa6182cc
...
...
@@ -11,6 +11,7 @@ const ROUTE_PATHS = {
businessDashboard
:
'/integrity_risk_table/business_dashboard'
,
problemRectification
:
'/problem_rectification'
,
issueRectificationArchive
:
'/issue_rectification_archive'
,
notifications
:
'/notifications'
,
problemRectificationForm
:
'/problem_rectification_form'
,
problemRectificationFlowTable
:
'/problem_rectification_flow_table'
,
}
as
const
;
...
...
@@ -56,6 +57,10 @@ export default defineConfig({
path
:
ROUTE_PATHS
.
issueRectificationArchive
,
component
:
'./problem_rectification/archive/index'
,
},
{
path
:
ROUTE_PATHS
.
notifications
,
component
:
'./problem_rectification/notifications/NotificationListPage'
,
},
{
path
:
ROUTE_PATHS
.
problemRectificationForm
,
component
:
'./problem_rectification/form/index'
,
...
...
docs/xinyuan-design-system.md
View file @
fa6182cc
...
...
@@ -106,6 +106,9 @@ Surface 用于表达页面内的包含关系,Overlay 用于表达浮动关系
## 6. DataGrid 与分页
-
所有业务列表使用 HeroUI Pro DataGrid,原生承载选择、排序、列宽、固定列和空状态。
-
Subject View 的
`settings.shortTitle = true`
统一解释为仅隐藏
`title`
展示列;共享列解析器必须保留标题数据在查询、排序、权限和详情上下文中的作用。标题列承载的业务信息与入口必须通过
`RecordWorkbench.contextColumns`
迁移到独立、可见的上下文列,并继续在固定操作列保留关键入口;禁止因隐藏标题而同时移除明细、详情、状态查看或行操作能力。上下文列不属于
`settings.columns`
业务字段,不参与数据查询、排序或字段权限。
-
`type = "detail"`
的视图使用同一
`shortTitle`
语义:为
`true`
时隐藏详情弹框 Header 中的记录标题和字段区中的
`title`
字段;功能标题必须始终保留作为 Modal 的可访问名称。存在多个详情视图时,以当前激活视图为准并随页签切换即时更新。
-
Subject Grid 权限必须与历史多维表格一致:
`viewAuth`
只决定视图可见性,字段
`auth`
只决定编辑能力;无
`viewAuth`
的视图和无字段
`auth`
的字段对普通用户默认拒绝。系统管理员和当前业务所属历史应用的“管理”角色可旁路两类授权,但
`disableColumns`
对所有账号始终只读。授权数据只从当前历史应用对应的
`user.applications[]`
项读取,不得跨应用合并角色、用户组或权限。由 Grid 进入详情弹框时,Grid 视图是唯一字段编辑权限来源,Detail 视图不得再次叠加字段权限。
-
选择列使用
`showSelectionCheckboxes`
,不得手写选择列或在 Cell 内嵌旧 Checkbox。
-
固定列、最小列宽和横向滚动是数据可读性行为,不通过 CSS 改写 DataGrid 表头、行高、边框、背景或 Hover。
-
DataGrid 无论列数多少都占满列表可用宽度。非宽表使用 HeroUI Pro 原生列尺寸:选择列和操作列保持固定,第一业务主列使用
`1fr`
吸收剩余空间,表格内容保持
`min-width: 100%`
;只有真实最小列宽超过视口时才使用像素列宽和内部横向滚动,不允许出现表面全宽但正文只停在左侧的空白断层。
...
...
src/components/AppIcons.jsx
View file @
fa6182cc
import
BankOutlined
from
'@ant-design/icons/BankOutlined'
;
import
BarChartOutlined
from
'@ant-design/icons/BarChartOutlined'
;
import
AppstoreOutlined
from
'@ant-design/icons/AppstoreOutlined'
;
import
BellOutlined
from
'@ant-design/icons/BellOutlined'
;
import
CalendarOutlined
from
'@ant-design/icons/CalendarOutlined'
;
import
CheckOutlined
from
'@ant-design/icons/CheckOutlined'
;
import
CheckCircleOutlined
from
'@ant-design/icons/CheckCircleOutlined'
;
...
...
@@ -62,6 +63,7 @@ export const Bank = withSize(BankOutlined);
export
const
Archive
=
withSize
(
InboxOutlined
);
export
const
AppGrid
=
withSize
(
AppstoreOutlined
);
export
const
BarChart
=
withSize
(
BarChartOutlined
);
export
const
Bell
=
withSize
(
BellOutlined
);
export
const
Calendar
=
withSize
(
CalendarOutlined
);
export
const
Check
=
withSize
(
CheckOutlined
);
export
const
ChevronDown
=
withSize
(
DownOutlined
);
...
...
src/components/DashboardSidebar.jsx
View file @
fa6182cc
...
...
@@ -3,6 +3,7 @@ import xyLogoWhite from '@/assets/xy-logo-white.png';
import
{
APPLICATION_DISPLAY_NAME
}
from
'@/config/application'
;
import
{
Sidebar
}
from
'@heroui-pro/react/sidebar'
;
import
{
Avatar
,
Button
,
Dropdown
,
Label
,
Tooltip
}
from
'@heroui/react'
;
import
{
Heading
}
from
'react-aria-components'
;
import
{
AppGrid
,
LogOut
}
from
'./AppIcons'
;
function
getUserDisplay
(
userInfo
)
{
...
...
@@ -132,6 +133,11 @@ function SidebarContents({
<
Icon
size=
{
16
}
/>
</
Sidebar
.
MenuIcon
>
<
Sidebar
.
MenuLabel
>
{
item
.
name
}
</
Sidebar
.
MenuLabel
>
{
Number
(
item
.
count
)
>
0
?
(
<
Sidebar
.
MenuChip
>
{
Number
(
item
.
count
)
>
99
?
'99+'
:
Number
(
item
.
count
)
}
</
Sidebar
.
MenuChip
>
)
:
null
}
</
Sidebar
.
MenuItem
>
);
})
}
...
...
@@ -154,9 +160,9 @@ export default function DashboardSidebar(props) {
<
SidebarContents
{
...
props
}
/>
</
Sidebar
>
<
Sidebar
.
Mobile
className=
"app-sidebar-glass rc-mobile-sidebar"
>
<
h2
slot=
"title"
className=
"sr-only"
>
<
Heading
slot=
"title"
className=
"sr-only"
>
{
APPLICATION_DISPLAY_NAME
}
导航
</
h2
>
</
Heading
>
<
SidebarContents
{
...
props
}
/>
</
Sidebar
.
Mobile
>
</>
...
...
src/components/record-workbench/index.jsx
View file @
fa6182cc
...
...
@@ -459,6 +459,7 @@ export default function RecordWorkbench({
onQuery
,
title
,
columns
=
EMPTY_ARRAY
,
contextColumns
=
EMPTY_ARRAY
,
columnGroups
=
EMPTY_ARRAY
,
records
=
EMPTY_ARRAY
,
selectedIds
=
EMPTY_ARRAY
,
...
...
@@ -780,6 +781,27 @@ export default function RecordWorkbench({
},
]
: []),
...contextColumns.map((column, columnIndex) => ({
key: column.key,
title: column.title,
width: column.width || 280,
minWidth: column.minWidth || column.width || 240,
pin: column.pin,
isPrimary: column.isPrimary ?? columnIndex === 0,
render: (row) => {
if (row.isExpandedRecord || row.isExpandedPanel) {
return null;
}
return column.render?.({
record: row.record,
expandable: row.expandable,
expanded: row.expanded,
toggleExpanded: () => toggleExpandedRow(row.key),
openDetail,
});
},
})),
...stickyColumns.orderedColumns.map((column, columnIndex) => {
const stickyMeta = stickyColumns.stickyMeta[column.field];
const columnGroupId = columnGroupMeta.fieldToGroup[column.field];
...
...
@@ -808,7 +830,7 @@ export default function RecordWorkbench({
: stickyMeta?.side === 'right'
? 'end'
: undefined,
isPrimary: columnIndex === 0,
isPrimary: co
ntextColumns.length === 0 && co
lumnIndex === 0,
render: (row) => {
if (row.isExpandedPanel) {
return columnIndex === 0 ? row.expandedContent : null;
...
...
src/config/application.js
View file @
fa6182cc
export
const
APPLICATION_DISPLAY_NAME
=
'问题整改'
;
export
const
APPLICATION_BASE_PATH
=
'/problem_rectification_new/'
;
export
const
AUTHORIZATION_APPLICATION_NAME
=
'integrity_risk_manager'
;
export
const
THEME_STORAGE_KEY
=
'xinyuan_color_theme'
;
export
const
ROUTE_TITLES
=
Object
.
freeze
({
...
...
@@ -7,6 +8,7 @@ export const ROUTE_TITLES = Object.freeze({
'/integrity_risk_table/business_dashboard'
:
'整改分析'
,
'/problem_rectification'
:
'问题整改'
,
'/issue_rectification_archive'
:
'往期材料归档'
,
'/notifications'
:
'通知消息'
,
'/problem_rectification_form'
:
'整改处理'
,
'/problem_rectification_flow_table'
:
'整改流程'
,
});
src/pages/RecordDetailDrawer/components/SubjectViewContent.jsx
View file @
fa6182cc
...
...
@@ -87,9 +87,9 @@ export default function SubjectViewContent({
const
fieldType
=
field
.
fieldDef
?.
type
||
'text'
;
const
fieldLabel
=
getFieldLabel
(
field
);
const
isFullWidth
=
FULL_WIDTH_FIELD_TYPES
.
has
(
fieldType
);
const
fieldDisabled
=
isFieldDisabled
?.(
field
.
field
,
view
)
||
isFieldDisabledByView
(
field
.
field
,
view
);
const
fieldDisabled
=
isFieldDisabled
?
isFieldDisabled
(
field
.
field
,
view
)
:
isFieldDisabledByView
(
field
.
field
,
view
);
return
(
<
div
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
fa6182cc
...
...
@@ -20,9 +20,8 @@ import {
buildViewColumns
,
checkViewCondition
,
getFieldValue
,
isFieldDisabledByView
,
isViewAccessible
,
setFieldValue
,
shouldHideViewTitleColumn
,
}
from
'@/utils/subjectView'
;
const
EMPTY_ARRAY
=
[];
...
...
@@ -121,7 +120,6 @@ const RecordDetailDrawer = (props, ref) => {
const
filtered
=
(
views
||
[])
.
filter
((
view
)
=>
view
.
type
===
'detail'
)
.
filter
((
view
)
=>
(
filterView
.
length
===
0
?
true
:
filterView
.
includes
(
view
.
name
)))
.
filter
((
view
)
=>
isViewAccessible
(
view
))
.
filter
((
view
)
=>
checkViewCondition
(
view
.
settings
?.
visabled
,
remoteRecord
));
if
(
filtered
.
length
>
0
)
{
...
...
@@ -169,6 +167,7 @@ const RecordDetailDrawer = (props, ref) => {
);
const
activeView
=
detailViews
.
find
((
view
)
=>
view
.
name
===
activeTabKey
)
||
detailViews
[
0
];
const
hideRecordTitle
=
shouldHideViewTitleColumn
(
activeView
);
const
isRecordLocked
=
remoteRecord
?.
metadata
?.
is_locked
===
true
;
const
effectiveReadonly
=
readonly
||
isRecordLocked
;
const
disabledFieldSet
=
useMemo
(
...
...
@@ -180,8 +179,7 @@ const RecordDetailDrawer = (props, ref) => {
);
const
isDrawerFieldDisabled
=
(
fieldPath
,
currentView
)
=>
disabledFieldSet
.
has
(
fieldPath
)
||
isFieldDisabled
(
fieldPath
,
currentView
)
||
isFieldDisabledByView
(
fieldPath
,
authorizationView
);
isFieldDisabled
(
fieldPath
,
authorizationView
||
currentView
);
const
hasEditableFields
=
Boolean
(
editableRecord
)
&&
...
...
@@ -396,9 +394,11 @@ const RecordDetailDrawer = (props, ref) => {
<
Modal
.
Header
className=
"pr-16"
>
<
Modal
.
Heading
>
{
title
||
'问题整改详情'
}
</
Modal
.
Heading
>
{
!
hideRecordTitle
?
(
<
div
className=
"truncate text-sm text-muted"
title=
{
recordTitle
}
>
{
loading
?
'正在读取记录信息'
:
recordTitle
}
</
div
>
)
:
null
}
</
Modal
.
Header
>
{
detailViews
.
length
>
1
?
(
...
...
src/pages/problem_rectification/Layout.jsx
View file @
fa6182cc
...
...
@@ -6,10 +6,15 @@ import AppShell from '@/components/AppShell';
import
{
Archive
,
BarChart
,
Bell
,
CheckCircleIcon
,
Project
,
}
from
'@/components/AppIcons'
;
import
ApplicationSwitcherDialog
from
'@/pages/problem_rectification/ApplicationSwitcherDialog'
;
import
{
loadUnreadNotificationCount
,
NOTIFICATION_STATE_EVENT
,
}
from
'@/pages/problem_rectification/notifications/notificationService'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
SiteContext
}
from
'@/wrapper/Site'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
...
...
@@ -40,6 +45,12 @@ const MENU_GROUPS = [
path
:
'/issue_rectification_archive'
,
icon
:
Archive
,
},
{
name
:
'通知消息'
,
path
:
'/notifications'
,
icon
:
Bell
,
showsNotificationCount
:
true
,
},
],
},
];
...
...
@@ -50,6 +61,21 @@ export default function ProblemRectificationLayout() {
const
siteInfo
=
useContext
(
SiteContext
);
const
[
sidebarOpen
,
setSidebarOpen
]
=
useState
(
true
);
const
[
isApplicationDialogOpen
,
setIsApplicationDialogOpen
]
=
useState
(
false
);
const
[
unreadNotificationCount
,
setUnreadNotificationCount
]
=
useState
(
0
);
const
menuGroups
=
useMemo
(
()
=>
MENU_GROUPS
.
map
((
group
)
=>
({
...
group
,
items
:
group
.
items
.
map
((
item
)
=>
({
...
item
,
count
:
item
.
showsNotificationCount
?
unreadNotificationCount
:
undefined
,
})),
})),
[
unreadNotificationCount
],
);
const
currentTitle
=
useMemo
(
()
=>
ROUTE_TITLES
[
location
.
pathname
]
||
APPLICATION_DISPLAY_NAME
,
...
...
@@ -60,10 +86,33 @@ export default function ProblemRectificationLayout() {
document
.
title
=
`
${
currentTitle
}
-
${
APPLICATION_DISPLAY_NAME
}
`
;
},
[
currentTitle
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
const
refreshUnreadCount
=
async
()
=>
{
try
{
const
count
=
await
loadUnreadNotificationCount
();
if
(
!
cancelled
)
setUnreadNotificationCount
(
count
);
}
catch
(
error
)
{
if
(
!
cancelled
)
setUnreadNotificationCount
(
0
);
}
};
refreshUnreadCount
();
window
.
addEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshUnreadCount
);
const
timer
=
window
.
setInterval
(
refreshUnreadCount
,
60000
);
return
()
=>
{
cancelled
=
true
;
window
.
removeEventListener
(
NOTIFICATION_STATE_EVENT
,
refreshUnreadCount
);
window
.
clearInterval
(
timer
);
};
},
[
location
.
pathname
]);
return
(
<>
<
AppShell
menuGroups=
{
MENU_GROUPS
}
menuGroups=
{
menuGroups
}
onLogout=
{
userInfo
?.
logout
}
onOpenApplicationSwitcher=
{
()
=>
setIsApplicationDialogOpen
(
true
)
}
open=
{
sidebarOpen
}
...
...
src/pages/problem_rectification/archive/archiveWorkbenchService.js
View file @
fa6182cc
import
{
countRecords
,
getSubjectView
,
loadRecord
,
loadSubject
,
queryRecords
}
from
'@/services/DataService'
;
import
{
buildViewColumns
,
buildViewSearchColumns
,
checkViewCondition
,
formatFieldValue
,
getFieldValue
,
...
...
@@ -29,17 +30,20 @@ export function normalizeArchiveBatchRecord(record) {
}
export
async
function
loadArchiveWorkbenchResources
()
{
const
[
archiveRecordsResult
,
archiveSubject
,
archiveViews
]
=
await
Promise
.
all
([
queryRecords
({
const
[
archiveSubject
,
archiveViews
]
=
await
Promise
.
all
([
loadSubject
(
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
),
getSubjectView
(
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
),
]);
const
accessibleViews
=
buildArchiveGridViews
(
archiveViews
||
[]);
const
archiveRecordsResult
=
accessibleViews
.
length
>
0
?
await
queryRecords
({
subject
:
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
,
page
:
0
,
page_size
:
1000
,
sort
:
'-updated_at -created_at'
,
mode
:
'full'
,
}),
loadSubject
(
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
),
getSubjectView
(
ISSUE_RECTIFICATION_ARCHIVE_SUBJECT
),
]);
})
:
{
records
:
[]
};
const
records
=
(
archiveRecordsResult
?.
records
||
[]).
map
(
normalizeArchiveBatchRecord
);
return
{
...
...
@@ -50,31 +54,37 @@ export async function loadArchiveWorkbenchResources() {
}
export
function
buildArchiveGridViews
(
views
=
[])
{
const
gridViews
=
views
.
filter
((
view
)
=>
view
.
type
===
'grid'
)
.
filter
((
view
)
=>
isViewAccessible
(
view
));
const
configuredGridViews
=
views
.
filter
((
view
)
=>
view
.
type
===
'grid'
);
const
gridViews
=
configuredGridViews
.
filter
((
view
)
=>
isViewAccessible
(
view
));
if
(
gridViews
.
length
>
0
)
{
if
(
gridViews
.
length
>
0
||
configuredGridViews
.
length
>
0
)
{
return
gridViews
;
}
return
[
{
const
fallbackView
=
{
title
:
'全部批次'
,
type
:
'grid'
,
name
:
'all'
,
settings
:
{},
},
];
};
return
isViewAccessible
(
fallbackView
)
?
[
fallbackView
]
:
[
];
}
export
function
buildArchiveColumns
(
view
,
subject
)
{
return
buildViewColumns
(
view
,
subject
)
.
filter
((
column
)
=>
column
.
field
!==
'title'
||
!
column
.
hide
)
;
return
buildViewColumns
(
view
,
subject
);
}
export
function
buildArchiveConfiguredView
(
records
,
filters
,
gridView
,
subject
)
{
if
(
!
gridView
)
{
return
{
filteredRecords
:
[],
columns
:
[],
};
}
const
keyword
=
normalizeText
(
filters
?.
keyword
).
toLowerCase
();
const
columns
=
buildArchiveColumns
(
gridView
,
subject
);
const
searchColumns
=
buildViewSearchColumns
(
gridView
,
subject
);
const
filteredRecords
=
records
.
filter
((
record
)
=>
{
const
rawRecord
=
record
.
raw
||
record
;
...
...
@@ -98,7 +108,7 @@ export function buildArchiveConfiguredView(records, filters, gridView, subject)
return
true
;
}
const
searchText
=
c
olumns
const
searchText
=
searchC
olumns
.
map
((
column
)
=>
formatFieldValue
(
getFieldValue
(
rawRecord
,
column
.
field
),
column
.
fieldDef
))
.
join
(
' '
)
.
toLowerCase
();
...
...
src/pages/problem_rectification/list/index.jsx
View file @
fa6182cc
...
...
@@ -51,6 +51,7 @@ import {
getFieldValue
,
getSelectOptions
,
normalizeText
,
shouldHideViewTitleColumn
,
}
from
'@/utils/subjectView'
;
import
{
DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE
,
...
...
@@ -895,6 +896,14 @@ export default function ProblemRectificationListPage() {
const reloadList = useCallback(
async (overrides = {}) => {
if (!subject || !activeGridView) {
setRecords([]);
setGridViewCounts({});
setPagination({
page: 0,
pageSize,
totalRecords: 0,
totalPages: 1,
});
return;
}
...
...
@@ -1628,7 +1637,7 @@ export default function ProblemRectificationListPage() {
await reloadList();
setSuccessMessage(
isDepartmentFlow
? `
推送成功,已生成
$
{
departmentAssignments
.
length
}
条部门整改明细,可从
记录标题右侧
展开查看。
`
? `
推送成功,已生成
$
{
departmentAssignments
.
length
}
条部门整改明细,可从
问题动态或标题列
展开查看。
`
: `
推送成功,已将
$
{
selectedRecords
.
length
}
条问题的
$
{
Array
.
from
(
batchDepartmentItemsByRecordId
.
values
()).
flat
().
length
}
条部门整改明细发送给填写人。
`,
);
} catch (requestError) {
...
...
@@ -1732,6 +1741,63 @@ export default function ProblemRectificationListPage() {
}}
title="问题清单"
columns={configuredView.columns || []}
contextColumns={
shouldHideViewTitleColumn(activeGridView)
? [
{
key: 'problem-context',
title: '问题动态',
width: 320,
minWidth: 320,
pin: 'start',
render: ({ record, expandable, expanded, toggleExpanded }) => {
const delayRequestDisplay = resolveDelayRequestDisplay(record);
return (
<div className="flex flex-wrap items-center gap-2">
{expandable ? (
<HeroButton
className="text-accent"
aria-expanded={expanded}
size="sm"
variant="ghost"
onPress={toggleExpanded}
>
<ChevronRightIcon
data-slot="icon"
className={clsx('transition-transform', expanded && 'rotate-90')}
/>
{expanded
? '收起问题明细'
: `
$
{
record
.
departmentItems
.
length
}
条问题明细
`}
</HeroButton>
) : (
<span className="text-sm text-muted">暂无问题明细</span>
)}
{delayRequestDisplay ? (
<HeroButton
size="sm"
variant={delayRequestDisplay.tone === 'rose' ? 'danger-soft' : 'secondary'}
title={delayRequestDisplay.title}
aria-label={`
查看
$
{
record
.
title
||
record
.
projectName
||
'当前问题'
}
的延期申请详情
`}
onPress={() => setDelayDetailRecord(record)}
>
<CalendarDaysIcon className="size-3.5 shrink-0" aria-hidden="true" />
{delayRequestDisplay.text}
</HeroButton>
) : (
<span className="text-sm text-muted">未发起延期</span>
)}
{record.isLocked ? <StatusPill text="已锁定" tone="amber" /> : null}
</div>
);
},
},
]
: []
}
columnGroups={activeGridView?.settings?.groups || []}
records={filteredRecords}
selectedIds={selectedIds}
...
...
@@ -1744,7 +1810,7 @@ export default function ProblemRectificationListPage() {
buildDepartmentDetailRecord(record, item, workflowUserLabels),
)
)}
toolbarActions={[
toolbarActions={
activeGridView ?
[
{
key: 'export',
label: '导出',
...
...
@@ -1766,8 +1832,8 @@ export default function ProblemRectificationListPage() {
tone: 'primary',
onClick: () => setShowCreateModal(true),
},
]}
bulkActions={[
]
: []
}
bulkActions={
activeGridView ?
[
{
key: 'push',
label: '推送整改',
...
...
@@ -1794,8 +1860,8 @@ export default function ProblemRectificationListPage() {
disabled: () => selectedIds.length === 0 || hasLockedSelection || actionLoading,
onClick: () => handleDelete(selectedRecords),
},
]}
rowActions={[
]
: []
}
rowActions={
activeGridView ?
[
{
key: 'view-detail',
label: '查看详情',
...
...
@@ -1843,8 +1909,8 @@ export default function ProblemRectificationListPage() {
disabled: ({ record }) => actionLoading || record.isLocked,
onClick: ({ record }) => handleDelete([record]),
},
]}
detailPanel={{
]
: []
}
detailPanel={
activeGridView ?
{
getRecordName: (record) => record.name || record.id,
onDirtyClose: reloadList,
render: ({ open, recordName, onClose, onDataChange }) => (
...
...
@@ -1875,15 +1941,17 @@ export default function ProblemRectificationListPage() {
useAuditMaterialCenter
/>
),
}}
}
: null
}
states={{
loading: loading || recordsLoading,
error,
success: '',
loadingTitle: '正在加载问题整改列表',
loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。',
emptyTitle: '当前视图暂无数据',
emptyDescription: '可以切换其他视图,或调整检查报告出具月份、严重程度、整改评级、主责部门和重点关注筛选。',
emptyTitle: activeGridView ? '当前视图暂无数据' : '暂无可访问的视图',
emptyDescription: activeGridView
? '可以切换其他视图,或调整检查报告出具月份、严重程度、整改评级、主责部门和重点关注筛选。'
: '当前账号未获得问题整改 Grid 视图的查看权限。',
}}
pagination={{
page,
...
...
src/pages/problem_rectification/notifications/NotificationListPage.jsx
0 → 100644
View file @
fa6182cc
import
{
Bell
,
Close
,
MagnifyingGlassIcon
,
OpenExternal
}
from
'@/components/AppIcons'
;
import
{
DataTablePagination
}
from
'@/components/DataTable'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Avatar
,
Button
,
Card
,
Chip
,
SearchField
,
Spinner
,
Tabs
,
toast
,
}
from
'@heroui/react'
;
import
{
useCallback
,
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
history
}
from
'umi'
;
import
{
executeNotificationAction
,
extractNotificationTarget
,
getNotificationContent
,
getNotificationSender
,
isNotificationRead
,
loadNotificationPage
,
loadUnreadNotificationCount
,
markAllNotificationsRead
,
markNotificationRead
,
NOTIFICATION_PAGE_SIZE
,
resolveNotificationTarget
,
}
from
'./notificationService'
;
const
CATEGORY_OPTIONS
=
[
{
key
:
'message'
,
label
:
'消息'
},
{
key
:
'todo'
,
label
:
'待办'
},
];
const
READ_STATUS_OPTIONS
=
[
{
key
:
'all'
,
label
:
'全部'
},
{
key
:
'unread'
,
label
:
'未读'
},
];
function
formatNotificationTime
(
value
)
{
if
(
!
value
)
return
'时间未知'
;
const
date
=
new
Date
(
value
);
if
(
Number
.
isNaN
(
date
.
getTime
()))
return
String
(
value
);
return
new
Intl
.
DateTimeFormat
(
'zh-CN'
,
{
year
:
'numeric'
,
month
:
'2-digit'
,
day
:
'2-digit'
,
hour
:
'2-digit'
,
minute
:
'2-digit'
,
second
:
'2-digit'
,
hour12
:
false
,
}).
format
(
date
);
}
function
NotificationItem
({
notification
,
category
,
currentUserId
,
isBusy
,
onAction
,
onMarkRead
,
onOpenTarget
,
})
{
const
isRead
=
isNotificationRead
(
notification
,
currentUserId
);
const
sender
=
getNotificationSender
(
notification
);
const
content
=
getNotificationContent
(
notification
)
||
'暂无通知内容'
;
const
target
=
resolveNotificationTarget
(
extractNotificationTarget
(
notification
));
const
actions
=
Array
.
isArray
(
notification
?.
actions
)
?
notification
.
actions
:
[];
const
avatarText
=
sender
.
trim
().
slice
(
0
,
1
).
toUpperCase
()
||
'系'
;
return
(
<
Card
>
<
Card
.
Content
className=
"flex flex-col gap-4 p-4 sm:p-5"
>
<
div
className=
"flex min-w-0 items-start gap-3"
>
<
Avatar
className=
"shrink-0"
size=
"sm"
>
<
Avatar
.
Fallback
>
{
avatarText
}
</
Avatar
.
Fallback
>
</
Avatar
>
<
div
className=
"min-w-0 flex-1"
>
<
div
className=
"flex flex-wrap items-start justify-between gap-2"
>
<
div
className=
"min-w-0"
>
<
h2
className=
"break-words text-sm font-medium text-foreground sm:text-base"
>
{
notification
?.
title
||
(
category
===
'todo'
?
'待办通知'
:
'通知消息'
)
}
</
h2
>
<
p
className=
"mt-1 text-xs text-muted sm:text-sm"
>
{
sender
}
·
{
formatNotificationTime
(
notification
?.
sent_at
||
notification
?.
created_at
)
}
</
p
>
</
div
>
<
Chip
color=
{
isRead
?
undefined
:
'danger'
}
size=
"sm"
variant=
"soft"
>
{
isRead
?
(
category
===
'todo'
?
'已完成'
:
'已读'
)
:
category
===
'todo'
?
'待处理'
:
'未读'
}
</
Chip
>
</
div
>
<
p
className=
"mt-3 whitespace-pre-wrap break-words text-sm leading-6 text-foreground"
>
{
content
}
</
p
>
</
div
>
</
div
>
<
div
className=
"flex min-h-8 flex-wrap items-center justify-end gap-2"
>
{
!
isRead
?
(
<
Button
isDisabled=
{
isBusy
}
size=
"sm"
variant=
"ghost"
onPress=
{
()
=>
onMarkRead
(
notification
)
}
>
标为已读
</
Button
>
)
:
null
}
{
category
===
'todo'
?
actions
.
map
((
action
,
index
)
=>
(
<
Button
isDisabled=
{
isBusy
||
isRead
}
key=
{
`${action?.label || 'action'}-${index}`
}
size=
"sm"
variant=
"secondary"
onPress=
{
()
=>
onAction
(
notification
,
action
)
}
>
{
action
?.
label
||
'处理'
}
</
Button
>
))
:
null
}
{
target
?
(
<
Button
isDisabled=
{
isBusy
}
size=
"sm"
variant=
"primary"
onPress=
{
()
=>
onOpenTarget
(
notification
,
target
)
}
>
<
OpenExternal
size=
{
14
}
/>
进入流程
</
Button
>
)
:
null
}
</
div
>
</
Card
.
Content
>
</
Card
>
);
}
export
default
function
NotificationListPage
()
{
const
userInfo
=
useContext
(
UserContext
);
const
[
category
,
setCategory
]
=
useState
(
'message'
);
const
[
readStatus
,
setReadStatus
]
=
useState
(
'all'
);
const
[
searchText
,
setSearchText
]
=
useState
(
''
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
notifications
,
setNotifications
]
=
useState
([]);
const
[
total
,
setTotal
]
=
useState
(
0
);
const
[
unreadTotal
,
setUnreadTotal
]
=
useState
(
0
);
const
[
loading
,
setLoading
]
=
useState
(
true
);
const
[
error
,
setError
]
=
useState
(
''
);
const
[
busyKey
,
setBusyKey
]
=
useState
(
''
);
const
[
reloadKey
,
setReloadKey
]
=
useState
(
0
);
useEffect
(()
=>
{
const
timer
=
window
.
setTimeout
(()
=>
{
setKeyword
(
searchText
.
trim
());
setPage
(
0
);
},
300
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
searchText
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
setLoading
(
true
);
setError
(
''
);
loadNotificationPage
({
page
,
pageSize
:
NOTIFICATION_PAGE_SIZE
,
category
,
readStatus
,
keyword
,
})
.
then
((
result
)
=>
{
if
(
cancelled
)
return
;
setNotifications
(
result
.
list
);
setTotal
(
result
.
total
);
})
.
catch
((
requestError
)
=>
{
if
(
cancelled
)
return
;
setNotifications
([]);
setTotal
(
0
);
setError
(
requestError
?.
message
||
'通知列表加载失败,请稍后重试'
);
})
.
finally
(()
=>
{
if
(
!
cancelled
)
setLoading
(
false
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
category
,
keyword
,
page
,
readStatus
,
reloadKey
]);
useEffect
(()
=>
{
let
cancelled
=
false
;
loadUnreadNotificationCount
()
.
then
((
count
)
=>
{
if
(
!
cancelled
)
setUnreadTotal
(
count
);
})
.
catch
(()
=>
{
if
(
!
cancelled
)
setUnreadTotal
(
0
);
});
return
()
=>
{
cancelled
=
true
;
};
},
[
reloadKey
]);
const
refresh
=
useCallback
(()
=>
setReloadKey
((
value
)
=>
value
+
1
),
[]);
const
runItemOperation
=
useCallback
(
async
(
key
,
operation
,
successMessage
)
=>
{
setBusyKey
(
key
);
try
{
await
operation
();
toast
.
success
(
successMessage
,
{
timeout
:
3000
});
refresh
();
return
true
;
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'操作失败,请稍后重试'
,
{
timeout
:
4000
,
});
return
false
;
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
],
);
const
handleMarkRead
=
useCallback
(
(
notification
)
=>
runItemOperation
(
notification
.
name
,
()
=>
markNotificationRead
(
notification
.
name
),
'已标记为已读'
,
),
[
runItemOperation
],
);
const
handleMarkAllRead
=
useCallback
(
async
()
=>
{
await
runItemOperation
(
'all'
,
markAllNotificationsRead
,
'全部通知已标记为已读'
,
);
},
[
runItemOperation
]);
const
handleAction
=
useCallback
(
(
notification
,
action
)
=>
runItemOperation
(
notification
.
name
,
()
=>
executeNotificationAction
(
notification
,
action
),
'待办已处理'
,
),
[
runItemOperation
],
);
const
handleOpenTarget
=
useCallback
(
async
(
notification
,
target
)
=>
{
setBusyKey
(
notification
.
name
);
try
{
if
(
!
isNotificationRead
(
notification
,
userInfo
?.
_id
))
{
await
markNotificationRead
(
notification
.
name
);
}
if
(
target
.
external
)
{
window
.
open
(
target
.
href
,
'_blank'
,
'noopener,noreferrer'
);
}
else
{
history
.
push
(
target
.
href
);
}
refresh
();
}
catch
(
operationError
)
{
toast
.
danger
(
operationError
?.
message
||
'无法打开通知关联流程'
,
{
timeout
:
4000
,
});
}
finally
{
setBusyKey
(
''
);
}
},
[
refresh
,
userInfo
?.
_id
],
);
return
(
<
div
className=
"rc-page-stack"
>
<
div
className=
"rc-page-toolbar rc-page-toolbar-responsive"
>
<
Tabs
className=
"w-fit max-w-full min-w-0"
selectedKey=
{
category
}
onSelectionChange=
{
(
key
)
=>
{
setCategory
(
String
(
key
));
setPage
(
0
);
}
}
>
<
Tabs
.
ListContainer
className=
"w-fit max-w-full min-w-0"
>
<
Tabs
.
List
aria
-
label=
"通知分类"
className=
"w-max min-w-max"
>
{
CATEGORY_OPTIONS
.
map
((
option
)
=>
(
<
Tabs
.
Tab
id=
{
option
.
key
}
key=
{
option
.
key
}
>
{
option
.
label
}
<
Tabs
.
Indicator
/>
</
Tabs
.
Tab
>
))
}
</
Tabs
.
List
>
</
Tabs
.
ListContainer
>
</
Tabs
>
<
Button
isDisabled=
{
loading
||
unreadTotal
===
0
}
isLoading=
{
busyKey
===
'all'
}
variant=
"secondary"
onPress=
{
handleMarkAllRead
}
>
全部已读
</
Button
>
</
div
>
<
div
className=
"rc-page-toolbar"
>
<
div
className=
"w-full min-w-0 sm:w-80 lg:w-96"
>
<
SearchField
aria
-
label=
"搜索通知内容"
fullWidth
value=
{
searchText
}
variant=
"primary"
onChange=
{
setSearchText
}
>
<
SearchField
.
Group
>
<
SearchField
.
SearchIcon
>
<
MagnifyingGlassIcon
size=
{
16
}
/>
</
SearchField
.
SearchIcon
>
<
SearchField
.
Input
placeholder=
"搜索通知内容"
/>
<
SearchField
.
ClearButton
aria
-
label=
"清空通知搜索"
>
<
Close
size=
{
12
}
/>
</
SearchField
.
ClearButton
>
</
SearchField
.
Group
>
</
SearchField
>
</
div
>
<
Tabs
selectedKey=
{
readStatus
}
onSelectionChange=
{
(
key
)
=>
{
setReadStatus
(
String
(
key
));
setPage
(
0
);
}
}
>
<
Tabs
.
ListContainer
>
<
Tabs
.
List
aria
-
label=
"通知状态"
>
{
READ_STATUS_OPTIONS
.
map
((
option
)
=>
(
<
Tabs
.
Tab
id=
{
option
.
key
}
key=
{
option
.
key
}
>
{
option
.
label
}
<
Tabs
.
Indicator
/>
</
Tabs
.
Tab
>
))
}
</
Tabs
.
List
>
</
Tabs
.
ListContainer
>
</
Tabs
>
</
div
>
{
error
?
(
<
Alert
status=
"danger"
>
<
Alert
.
Content
>
<
Alert
.
Description
>
{
error
}
</
Alert
.
Description
>
</
Alert
.
Content
>
<
Button
size=
"sm"
variant=
"ghost"
onPress=
{
refresh
}
>
重试
</
Button
>
</
Alert
>
)
:
null
}
{
loading
&&
notifications
.
length
===
0
?
(
<
div
className=
"flex min-h-64 items-center justify-center"
aria
-
label=
"通知加载中"
>
<
Spinner
size=
"sm"
/>
</
div
>
)
:
null
}
{
!
loading
&&
!
error
&&
notifications
.
length
===
0
?
(
<
EmptyState
className=
"min-h-64"
size=
"md"
>
<
EmptyState
.
Media
>
<
Bell
size=
{
22
}
/>
</
EmptyState
.
Media
>
<
EmptyState
.
Header
>
<
EmptyState
.
Title
>
{
category
===
'todo'
?
'暂无待办通知'
:
'暂无通知消息'
}
</
EmptyState
.
Title
>
<
EmptyState
.
Description
>
{
keyword
?
'没有找到匹配的通知,请调整搜索条件。'
:
readStatus
===
'unread'
?
'当前没有未读通知。'
:
'新的通知会显示在这里。'
}
</
EmptyState
.
Description
>
</
EmptyState
.
Header
>
</
EmptyState
>
)
:
null
}
{
notifications
.
length
>
0
?
(
<
div
className=
"flex min-w-0 flex-col gap-3"
aria
-
busy=
{
loading
}
>
{
notifications
.
map
((
notification
)
=>
(
<
NotificationItem
category=
{
category
}
currentUserId=
{
userInfo
?.
_id
}
isBusy=
{
busyKey
===
notification
.
name
}
key=
{
notification
.
name
||
notification
.
_id
}
notification=
{
notification
}
onAction=
{
handleAction
}
onMarkRead=
{
handleMarkRead
}
onOpenTarget=
{
handleOpenTarget
}
/>
))
}
</
div
>
)
:
null
}
{
!
error
&&
total
>
0
?
(
<
DataTablePagination
isDisabled=
{
loading
}
itemLabel=
"条通知"
page=
{
page
}
pageSize=
{
NOTIFICATION_PAGE_SIZE
}
total=
{
total
}
onPageChange=
{
setPage
}
/>
)
:
null
}
</
div
>
);
}
src/pages/problem_rectification/notifications/notificationService.js
0 → 100644
View file @
fa6182cc
import
{
exacteActionFunction
,
queryUserNotice
,
readUserNotice
,
unReadNoticeCount
,
}
from
'@/services/DataService'
;
export
const
NOTIFICATION_PAGE_SIZE
=
10
;
export
const
NOTIFICATION_STATE_EVENT
=
'problem-rectification:notification-state'
;
function
toArray
(
value
)
{
return
Array
.
isArray
(
value
)
?
value
:
[];
}
function
toId
(
value
)
{
if
(
value
&&
typeof
value
===
'object'
)
{
return
String
(
value
.
_id
||
value
.
id
||
value
.
name
||
''
);
}
return
String
(
value
||
''
);
}
function
escapeRegExp
(
value
)
{
return
String
(
value
||
''
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
}
export
async
function
loadNotificationPage
({
page
=
0
,
pageSize
=
NOTIFICATION_PAGE_SIZE
,
category
=
'message'
,
readStatus
=
'all'
,
keyword
=
''
,
}
=
{})
{
const
filter
=
{
category
};
const
normalizedKeyword
=
String
(
keyword
||
''
).
trim
();
if
(
normalizedKeyword
)
{
filter
.
content
=
{
$regex
:
escapeRegExp
(
normalizedKeyword
),
$options
:
'i'
,
};
}
const
response
=
await
queryUserNotice
(
page
,
pageSize
,
filter
,
readStatus
===
'unread'
?
'unread'
:
null
,
);
return
{
list
:
toArray
(
response
?.
list
),
page
:
Number
(
response
?.
current_page
??
page
)
||
0
,
pageSize
:
Number
(
response
?.
page_size
??
pageSize
)
||
pageSize
,
total
:
Number
(
response
?.
total_notices
??
response
?.
total
??
0
)
||
0
,
totalPages
:
Number
(
response
?.
total_pages
??
0
)
||
0
,
};
}
export
async
function
loadUnreadNotificationCount
()
{
const
response
=
await
unReadNoticeCount
();
return
Math
.
max
(
0
,
Number
(
response
?.
total_notices
??
response
?.
count
??
response
?.
total
??
response
??
0
,
)
||
0
,
);
}
export
async
function
markNotificationRead
(
name
)
{
if
(
!
name
)
{
throw
new
Error
(
'通知缺少已读标识'
);
}
await
readUserNotice
(
name
);
notifyNotificationStateChanged
();
}
export
async
function
markAllNotificationsRead
()
{
await
readUserNotice
();
notifyNotificationStateChanged
();
}
export
async
function
executeNotificationAction
(
notification
,
action
)
{
const
actionName
=
typeof
action
?.
action
===
'string'
?
action
.
action
:
action
?.
action
?.
name
||
action
?.
name
;
const
methodName
=
action
?.
method_name
||
action
?.
methodName
;
if
(
!
actionName
||
!
methodName
)
{
throw
new
Error
(
'该待办操作缺少执行配置'
);
}
const
result
=
await
exacteActionFunction
(
actionName
,
methodName
,
{
notice_id
:
notification
?.
_id
,
});
notifyNotificationStateChanged
();
return
result
;
}
export
function
isNotificationRead
(
notification
,
userId
)
{
const
normalizedUserId
=
toId
(
userId
);
if
(
!
normalizedUserId
)
return
false
;
return
toArray
(
notification
?.
data
?.
logs
).
some
(
(
log
)
=>
toId
(
log
?.
user_id
)
===
normalizedUserId
,
);
}
export
function
getNotificationSender
(
notification
)
{
return
(
notification
?.
from
?.
display_name
||
notification
?.
from
?.
username
||
notification
?.
created_by
?.
display_name
||
notification
?.
created_by
?.
username
||
'系统'
);
}
export
function
getNotificationContent
(
notification
)
{
return
String
(
notification
?.
content
||
notification
?.
content_compile
||
''
).
trim
();
}
function
trimTarget
(
target
)
{
return
String
(
target
||
''
).
replace
(
/
[
),.;!?,。;!?)
]
+$/
u
,
''
);
}
export
function
extractNotificationTarget
(
notification
)
{
const
configuredTarget
=
notification
?.
settings
?.
url
||
notification
?.
settings
?.
link
||
notification
?.
data
?.
url
||
notification
?.
data
?.
link
;
if
(
configuredTarget
)
return
trimTarget
(
configuredTarget
);
const
content
=
getNotificationContent
(
notification
);
const
match
=
content
.
match
(
/https
?
:
\/\/[^\s
<>"'
]
+|
\/
problem_rectification_new
\/[^\s
<>"'
]
+|
\/
problem_rectification
(?:
_flow_table|_form
)(?:\?[^\s
<>"'
]
*
)?
/
,
);
return
trimTarget
(
match
?.[
0
]);
}
export
function
resolveNotificationTarget
(
target
)
{
const
normalizedTarget
=
trimTarget
(
target
);
if
(
!
normalizedTarget
)
return
null
;
if
(
/^https
?
:
\/\/
/i
.
test
(
normalizedTarget
))
{
try
{
const
url
=
new
URL
(
normalizedTarget
);
if
(
url
.
pathname
.
startsWith
(
'/problem_rectification_new/'
))
{
return
resolveNotificationTarget
(
`
${
url
.
pathname
}${
url
.
search
}${
url
.
hash
}
`
);
}
return
{
href
:
url
.
toString
(),
external
:
true
};
}
catch
(
error
)
{
return
null
;
}
}
const
appPrefix
=
'/problem_rectification_new'
;
if
(
!
normalizedTarget
.
startsWith
(
`
${
appPrefix
}
/`
))
{
return
normalizedTarget
.
startsWith
(
'/'
)
?
{
href
:
normalizedTarget
,
external
:
true
}
:
null
;
}
return
{
href
:
normalizedTarget
.
slice
(
appPrefix
.
length
),
external
:
false
,
};
}
export
function
notifyNotificationStateChanged
()
{
if
(
typeof
window
!==
'undefined'
)
{
window
.
dispatchEvent
(
new
CustomEvent
(
NOTIFICATION_STATE_EVENT
));
}
}
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
fa6182cc
...
...
@@ -61,13 +61,14 @@
-
`type = "grid"`
的视图用于主列表顶部视图切换,替代原先写死的 tab
-
`settings.columns`
决定列表列顺序、宽度、固定列位置和排序优先级,列表页不得在视图列后追加自定义业务字段列
-
`settings.groups`
决定列表列分组,例如把多个问题分类字段归入同一分组;前端需展示分组表头,同一分组下的列即使在
`settings.columns`
中不连续,也应在表格内聚拢展示,并支持每个分组独立折叠 / 展开,折叠后隐藏该分组子列
-
`settings.shortTitle = true`
仅隐藏列表中的
`title`
展示列,不删除记录标题数据,也不改变标题字段参与的关键词查询、视图排序、详情标题或权限判断;标题列原本承载的部门明细数量 / 展开、延期申请阶段 / 详情和锁定状态迁移到固定的“问题动态”上下文列,详情、延期申请、锁定、复制和删除继续由固定行操作列提供
-
`settings.quickSelect`
决定列表是否展示勾选列,用于批量推送、删除等操作
-
`settings.restrictions`
与
`settings.filters.conditions`
共同决定该视图下可见的数据范围
-
`settings.viewAuth`
决定当前用户是否能看到该视图
-
`type = "detail"`
的视图用于详情抽屉字段编排,按
`columns`
顺序渲染字段
-
表格 Tab 的
`settings.auth`
按字段路径控制详情
抽屉内对应字段是否允许编辑,规则结构与多维表格一致:
`field`
使用
`title`
或
`metadata.<fieldName>`
,
`type`
支持
`group`
、
`role`
、
`permission`
,授权值分别从
`groups`
、
`roles`
、
`permissions`
读取
-
表格 Tab 的
`settings.disableColumns`
与
`settings.auth`
、当前详情视图自身的同名配置叠加生效;任一层禁止编辑时字段保留展示但进入只读状态,并显示“当前视图无编辑权限”,不隐藏列或字段
-
管理员、系统账号拥有字段编辑权限旁路;普通用户按当前登录用户的用户组、应用用户组、角色或权限集合匹配,未命中配置时不得编辑。切换表格 Tab 后,详情抽屉
必须立即改用新 Tab 的权限配置
-
Grid 视图的
`settings.viewAuth`
决定当前用户是否能看到该视图;普通用户未配置
`viewAuth`
时默认不可见,仅系统管理员和问题整改历史应用
`integrity_risk_manager`
的“管理”角色可见。全部 Grid 视图均无权限时不得生成绕过授权的兜底视图、查询记录或展示新增/批量/行操作入口
-
`type = "detail"`
的视图用于详情抽屉字段编排,按
`columns`
顺序渲染字段
;当前详情视图
`settings.shortTitle = true`
时,同时隐藏弹框 Header 中的记录标题和字段区中的
`title`
字段,未配置或为
`false`
时正常显示,切换详情视图页签后立即按新视图更新
-
表格 Tab 的
`settings.auth`
按字段路径控制详情
弹框内对应字段是否允许编辑,规则结构与多维表格一致:
`field`
使用
`title`
或
`metadata.<fieldName>`
,
`type`
支持
`group`
、
`role`
、
`permission`
,授权值分别从
`groups`
、
`roles`
、
`permissions`
读取;普通用户遇到未配置
`auth`
的字段时默认只读
-
表格 Tab 的
`settings.disableColumns`
始终优先,命中后字段保留展示但进入只读状态,即使系统管理员或“管理”角色也不得编辑;未命中时,系统管理员、系统账号和问题整改历史应用
`integrity_risk_manager`
的“管理”角色旁路字段
`auth`
-
普通用户只使用
`applications[]`
中
`name = "integrity_risk_manager"`
的用户组、角色和权限进行匹配,不合并顶层用户组或其他应用授权。多维表格配置转换只接受
`group`
、
`role`
、
`permission`
,缺失或其他类型均按“管理”角色处理。进入详情弹框后,当前 Grid Tab 是唯一的字段编辑权限来源,Detail 视图只负责字段编排、条件显示与
`shortTitle`
,两套字段权限不得叠加。切换 Grid Tab 后,详情弹框
必须立即改用新 Tab 的权限配置
因此前端新增字段展示、部门视图调整、列顺序变化时,应优先改 Subject View 配置,而不是直接修改页面常量。
...
...
@@ -79,12 +80,12 @@
-
主列表提供“检查报告出具月份”筛选,用户选择
`YYYY-MM`
后,仅查询
`metadata.inspection_report_date`
位于该月份的记录;该筛选不使用其它检查或整改日期兜底
-
主列表固定每页 10 条,不显示页容量选择器;切换视图、修改筛选或清空筛选时回到第 1 页。表头勾选只选择当前页记录,跨页批量处理需用户逐页确认选择范围。
-
主列表视图页签数量通过
`/record/count`
按各视图限制和当前筛选条件计算,表格标题区展示服务端筛选后的总数与当前页记录数
-
主列表
标题列直接展示可点击的延期申请状态:存在历史问题级延期或部门级延期明细时,按当前阶段显示“延期已发起 / 延期待审批 / 延期待终审 / 延期处理中 / 延期已生效 / 延期已退回”日历徽标,不能等用户再次点击推送后才提示已发起;状态同时使用图标和文字,不只依赖颜色
-
主列表
显示标题列时,标题单元格直接展示可点击的延期申请状态:存在历史问题级延期或部门级延期明细时,按当前阶段显示“延期已发起 / 延期待审批 / 延期待终审 / 延期处理中 / 延期已生效 / 延期已退回”日历徽标,不能等用户再次点击推送后才提示已发起;状态同时使用图标和文字,不只依赖颜色。视图启用
`shortTitle`
时,“问题动态”列按相同规则直接展示延期状态和问题明细数量;没有明细或延期时明确显示“暂无问题明细”“未发起延期”,操作列仍保留“查看延期申请”入口
-
点击延期状态后打开“延期申请详情”弹窗,按责任部门展示原预计完成时间、申请完成时间、延期事由、填写人、一级审批人、最终审批人以及三级流程状态;申请完成时间或延期事由尚未提交时明确显示“待填写”,历史问题级延期按“整条问题”展示;行操作区同时提供“查看延期申请”日历按钮,保证窄屏下标题列被固定列压缩时仍可进入详情
-
用户单选已有延期申请的记录时,顶部“推送延期申请”按钮改为显示当前延期状态并禁用,避免形成看似可以重复推送的入口
-
主列表滚动到表格区域后,表格工作台吸顶展示;标题栏、操作区、列分组和分页固定在工作台内,数据表格区域独立滚动,表头列名在数据区域内继续吸顶,折叠 / 展开列分组时保留当前表格滚动位置
-
详情侧栏统一复用
`RecordDetailDrawer`
,通过
`recordName`
、
`subjectName`
、
`preferredViewName`
驱动,不再在各页面重复实现抽屉骨架
-
详情
侧栏顶部不展示记录标题、状态和摘要卡片,只保留详情视图切换与全屏 / 关闭操作
,字段主体作为主要阅读区域
-
详情
弹框顶部始终保留功能标题和全屏 / 关闭操作;记录标题是否展示由当前
`detail`
视图的
`settings.shortTitle`
决定,不展示额外状态或摘要卡片
,字段主体作为主要阅读区域
-
详情侧栏内
`reference`
类型字段只读态统一展示为关联明细表:按目标 Subject 的
`title`
与全部 metadata 字段展开,字段值按各自类型格式化;编辑态仍使用引用选择器,保存时只写入关联记录
`_id`
或
`_id[]`
-
列表行操作列使用图标按钮横向排列,查看、复制、删除等常用操作通过 tooltip / aria-label 标明含义,避免文字按钮纵向堆叠
-
新增同类列表页时,优先复用工作台与详情抽屉组合,避免再次复制问题整改页 JSX
...
...
@@ -261,7 +262,7 @@
-
列表页采用分页加载,默认每页 20 条,可切换为 10、50、100;分页、筛选、视图切换均由服务端查询结果驱动
-
字段编辑统一进入右侧详情抽屉
-
长文本、附件、状态类字段在详情抽屉中直接维护
-
详情
抽屉内字段顺序与显示权限跟随
`detail view`
配置
-
详情
弹框内字段顺序与条件显示跟随
`detail view`
配置;从 Grid 进入时,字段编辑权限只跟随当前 Grid 视图
配置
---
...
...
@@ -956,3 +957,37 @@ const result = await queryUsers(0, 20, {
对应对象定义已同步更新至:
-
[
subjectsDefinition.md
](
/Users/yangkang/Desktop/xy/problem_rectification/src/services/subjectsDefinition.md
)
---
## 17. 通知消息流程
### 17.1 功能入口
“通知消息”作为问题整改应用侧边栏一级入口,路由为
`/notifications`
。侧边栏通过
`GET /notice/customer/count`
展示当前用户未读总数,超过 99 条显示
`99+`
;未读数为 0 时不展示角标。页面不在顶部导航重复提供通知弹窗。
### 17.2 查询与筛选
1.
进入页面后默认查询
`message`
分类的全部通知,每页 10 条。
2.
用户可在“消息 / 待办”之间切换,并使用“全部 / 未读”状态筛选。
3.
搜索框按通知正文查询,输入停止 300 毫秒后重新从第 1 页请求;正则特殊字符在请求前转义。
4.
未读筛选必须向服务端传递
`status: "unread"`
,避免只过滤当前页导致总数和分页错误。
5.
加载中显示进度状态;无数据时区分消息、待办、未读和搜索无结果;请求失败时保留明确错误信息和重试入口。
### 17.3 已读与待办状态
-
通知的
`data.logs`
中存在当前用户
`_id`
时视为已读;
`message`
显示“已读 / 未读”,
`todo`
显示“已完成 / 待处理”。
-
“标为已读”调用
`PUT /notice/customer/read`
并传通知
`name`
。
-
“全部已读”调用同一接口但不传
`name`
,处理当前用户全部通知;未读总数为 0 时按钮禁用。
-
待办按钮完全由通知
`actions`
配置生成,执行时只提交
`{notice_id: 通知 _id}`
。操作期间禁用当前通知的重复操作,成功后刷新列表和侧边栏角标,失败时保留原状态并提示错误。
### 17.4 进入关联流程
通知正文或设置中存在业务链接时显示“进入流程”:
1.
未读通知先标记为已读。
2.
`/problem_rectification_new/*`
地址去除应用 base 前缀后通过 Umi 路由打开,例如
`/problem_rectification_new/problem_rectification_flow_table?id=...`
转为
`/problem_rectification_flow_table?id=...`
。
3.
只有
`/problem_rectification_new/*`
归入本应用路由;其他应用和外部 HTTP(S) 链接保留原地址,以新窗口打开并设置
`noopener,noreferrer`
。
4.
已读请求失败时不执行跳转,防止通知状态和业务访问脱节。
通知正文始终按纯文本渲染,不执行服务端返回的 HTML,避免脚本注入或伪造交互控件。
src/pages/problem_rectification/problemRectificationListService.js
View file @
fa6182cc
...
...
@@ -13,6 +13,7 @@ import {
import
{
calculateProblemSeverityLevel
}
from
'./problemSeverity'
;
import
{
buildViewColumns
,
buildViewSearchColumns
,
checkViewCondition
,
emptyValueForField
,
formatFieldValue
,
...
...
@@ -1144,28 +1145,24 @@ export async function loadProblemRectificationViewCountMap({ filters = {}, views
}
export
function
buildGridViews
(
views
=
[])
{
const
gridViews
=
views
.
filter
((
view
)
=>
view
.
type
===
'grid'
)
.
filter
((
view
)
=>
isViewAccessible
(
view
));
const
configuredGridViews
=
views
.
filter
((
view
)
=>
view
.
type
===
'grid'
);
const
gridViews
=
configuredGridViews
.
filter
((
view
)
=>
isViewAccessible
(
view
));
if
(
gridViews
.
length
>
0
)
{
if
(
gridViews
.
length
>
0
||
configuredGridViews
.
length
>
0
)
{
return
gridViews
;
}
return
[
{
const
fallbackView
=
{
title
:
'全部'
,
type
:
'grid'
,
name
:
'all'
,
settings
:
{},
},
];
};
return
isViewAccessible
(
fallbackView
)
?
[
fallbackView
]
:
[
];
}
export
function
buildViewColumnsForList
(
view
,
subject
)
{
return
buildViewColumns
(
view
,
subject
).
filter
(
(
column
)
=>
column
.
field
!==
'title'
||
!
column
.
hide
,
);
return
buildViewColumns
(
view
,
subject
);
}
export
function
getRecordCellText
(
record
,
column
)
{
...
...
@@ -1180,6 +1177,21 @@ export function getRecordCellText(record, column) {
}
export
function
buildProblemRectificationConfiguredListView
(
records
,
filters
,
gridView
,
subject
)
{
if
(
!
gridView
)
{
return
{
filteredRecords
:
[],
columns
:
[],
progressOptions
:
[],
auditStatusOptions
:
[],
severityOptions
:
[],
overallRatingOptions
:
[],
primaryResponsibleDepartmentOptions
:
[],
problemFromOptions
:
[],
rootCauseOptions
:
[],
focusOptions
:
[],
};
}
const
nextKeyword
=
filters
?.
keyword
||
''
;
const
nextInspectionReportMonth
=
normalizeText
(
filters
?.
inspectionReportMonth
);
const
nextProgress
=
filters
?.
progress
||
''
;
...
...
@@ -1192,7 +1204,8 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
const
nextFocusFlag
=
filters
?.
focusFlag
||
''
;
const
columns
=
buildViewColumnsForList
(
gridView
,
subject
);
const
searchableFields
=
columns
.
map
((
column
)
=>
column
.
field
);
const
searchColumns
=
buildViewSearchColumns
(
gridView
,
subject
);
const
searchableFields
=
searchColumns
.
map
((
column
)
=>
column
.
field
);
const
filteredRecords
=
records
.
filter
((
record
)
=>
{
if
(
...
...
@@ -1296,7 +1309,7 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
const
keyword
=
normalizeText
(
nextKeyword
).
toLowerCase
();
return
searchableFields
.
map
((
fieldPath
)
=>
normalizeText
(
formatFieldValue
(
getFieldValue
(
rawRecord
,
fieldPath
),
c
olumns
.
find
((
column
)
=>
column
.
field
===
fieldPath
)?.
fieldDef
)))
.
map
((
fieldPath
)
=>
normalizeText
(
formatFieldValue
(
getFieldValue
(
rawRecord
,
fieldPath
),
searchC
olumns
.
find
((
column
)
=>
column
.
field
===
fieldPath
)?.
fieldDef
)))
.
join
(
' '
)
.
toLowerCase
()
.
includes
(
keyword
);
...
...
src/services/record-api.md
View file @
fa6182cc
...
...
@@ -331,3 +331,49 @@ Query:`calculated=true|false`
3.
所有待建档材料创建成功后,再调用
`PUT /record/:name`
更新问题记录的
`metadata.rectification_proof_materials`
,单值写对象,多值写对象数组。
4.
任一材料创建失败时不更新问题记录;一批材料中已创建成功的条目和“材料已创建、问题更新失败”的条目都在当前编辑会话中保留建档标记,重试保存只处理尚未建档的条目,不得重复创建。
5.
用户从草稿逐项删除本地附件后,该附件不再参与材料建档;取消详情编辑不创建
`am_material`
。已经上传但未建档的二进制文件遵循上传服务自身的清理策略。
## 6. 当前用户通知接口
通知中心使用平台通知接口,不使用 Record API,也不包裹
`metadata`
。所有接口均要求当前登录用户身份。
### 6.1 分页查询
调用
`POST /notice/customer/query`
:
```
json
{
"page"
:
0
,
"page_size"
:
10
,
"filter"
:
{
"category"
:
"message"
,
"content"
:
{
"$regex"
:
"整改"
,
"$options"
:
"i"
}
},
"status"
:
"unread"
}
```
-
`category`
为
`message`
或
`todo`
。
-
内容搜索通过
`filter.content`
正则完成,前端必须转义用户输入中的正则特殊字符。
-
查询全部通知时
`status`
传
`null`
;查询未读时传
`unread`
,不得仅对当前页做前端未读过滤。
-
响应包含
`list`
、
`current_page`
、
`page_size`
、
`total_notices`
和
`total_pages`
。
### 6.2 已读与未读数量
-
`GET /notice/customer/count`
:返回当前用户的未读通知数量,侧边栏角标和“全部已读”按钮以此为准。
-
`PUT /notice/customer/read`
,请求体
`{ "name": "通知 name" }`
:标记单条通知已读。
-
`PUT /notice/customer/read`
,请求体
`{}`
:标记当前用户全部通知已读。
### 6.3 待办动作
调用
`PUT /notice/customer/:action/action/:fname`
,请求体固定传递通知记录 ID:
```
json
{
"notice_id"
:
"通知 _id"
}
```
其中
`:action`
、
`:fname`
和按钮标签均来自通知的
`actions`
配置,前端不得硬编码业务动作名称。执行成功后重新查询通知列表和未读数量;失败时保留当前列表并显示错误提示。
src/services/subjectsDefinition.md
View file @
fa6182cc
...
...
@@ -524,3 +524,26 @@ type Record = {
]
}
```
---
## 4. 当前用户通知对象
通知是平台
`/notice/customer/*`
接口返回的领域对象,不属于 Subject Record,因此不使用
`metadata`
封装。前端按以下结构消费:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
|
`_id`
| string | 通知记录 ID,执行待办动作时作为
`notice_id`
|
|
`name`
| string | 通知业务标识,标记单条已读时使用 |
|
`title`
| string | 通知标题 |
|
`category`
|
`message \| todo`
| 消息或待办分类 |
|
`content`
| string | 通知正文;前端按纯文本安全渲染 |
|
`content_compile`
| string | 编译后正文,仅在
`content`
为空时作为降级内容 |
|
`sent_at`
| ISO 8601 string | 发送时间 |
|
`from`
| object | 发送人,优先读取
`display_name`
,其次
`username`
|
|
`created_by`
| object | 创建人;发送人缺失时作为降级来源 |
|
`data.logs`
| array | 用户已读日志;
`user_id`
与当前用户
`_id`
匹配表示已读 |
|
`settings`
| object | 可选通知设置,可包含业务链接 |
|
`actions`
| array | 待办操作配置,包含按钮标签、动作名和方法名 |
通知内部业务链接可以位于
`settings.url`
、
`settings.link`
、
`data.url`
、
`data.link`
或正文中。指向
`/problem_rectification_new/*`
的站内地址进入 Umi 路由前应去除应用 base 前缀;其他应用和外部 HTTP(S) 地址使用新窗口打开。
src/utils/subjectView.js
View file @
fa6182cc
import
dayjs
from
'dayjs'
;
import
{
AUTHORIZATION_APPLICATION_NAME
}
from
'@/config/application'
;
export
function
normalizeText
(
value
)
{
return
String
(
value
??
''
).
trim
();
...
...
@@ -34,62 +35,106 @@ export function getCurrentUserFromStorage() {
}
}
function
collectUserGroups
(
user
)
{
const
directGroups
=
toArray
(
user
?.
groups
)
.
flatMap
((
group
)
=>
[
group
?.
_id
,
group
?.
name
,
group
?.
display_name
,
group
])
function
collectIdentityValues
(
values
)
{
return
toArray
(
values
)
.
flatMap
((
value
)
=>
{
if
(
value
&&
typeof
value
===
'object'
)
{
return
[
value
.
_id
,
value
.
name
,
value
.
display_name
,
value
.
displayName
,
value
.
username
,
];
}
return
value
;
})
.
map
(
normalizeText
)
.
filter
(
Boolean
);
const
appGroups
=
toArray
(
user
?.
applications
).
flatMap
((
app
)
=>
app
?.
groups
||
[]);
return
[...
directGroups
,
...
appGroups
].
map
(
normalizeText
).
filter
(
Boolean
);
}
function
collectUserRoles
(
user
)
{
const
directRoles
=
toArray
(
user
?.
roles
);
const
appRoles
=
toArray
(
user
?.
applications
).
flatMap
((
app
)
=>
app
?.
roles
||
[]);
return
[...
directRoles
,
...
appRoles
].
map
(
normalizeText
).
filter
(
Boolean
)
;
function
getUserApplication
(
user
,
applicationName
=
AUTHORIZATION_APPLICATION_NAME
)
{
return
toArray
(
user
?.
applications
).
find
(
(
application
)
=>
normalizeText
(
application
?.
name
)
===
normalizeText
(
applicationName
),
)
||
null
;
}
function
collectUserPermissions
(
user
)
{
const
directPermissions
=
toArray
(
user
?.
permissions
);
const
appPermissions
=
toArray
(
user
?.
applications
).
flatMap
((
app
)
=>
app
?.
permissions
||
[]);
return
[...
directPermissions
,
...
appPermissions
].
map
(
normalizeText
).
filter
(
Boolean
);
function
collectApplicationAuthorization
(
user
,
applicationName
)
{
const
application
=
getUserApplication
(
user
,
applicationName
);
return
{
groups
:
collectIdentityValues
(
application
?.
groups
),
roles
:
collectIdentityValues
(
application
?.
roles
),
permissions
:
collectIdentityValues
(
application
?.
permissions
),
};
}
function
intersects
(
left
,
right
)
{
const
rightSet
=
new
Set
(
toArray
(
right
).
map
(
normalizeText
).
filter
(
Boolean
));
return
toArray
(
left
).
some
((
item
)
=>
rightSet
.
has
(
normalizeText
(
item
)));
const
rightSet
=
new
Set
(
collectIdentityValues
(
right
));
return
collectIdentityValues
(
left
).
some
((
item
)
=>
rightSet
.
has
(
item
));
}
function
getMultidimensionalTableAuthRule
(
authRule
)
{
return
[
'group'
,
'role'
,
'permission'
].
includes
(
authRule
?.
type
)
?
authRule
:
'管理'
;
}
export
function
hasAuthAccess
(
authRule
,
user
=
getCurrentUserFromStorage
())
{
if
(
!
authRule
)
{
export
function
isSystemAdministrator
(
user
=
getCurrentUserFromStorage
())
{
return
user
?.
type
===
'admin'
||
user
?.
type
===
'system'
;
}
export
function
hasApplicationManagementAccess
(
user
=
getCurrentUserFromStorage
(),
applicationName
=
AUTHORIZATION_APPLICATION_NAME
,
)
{
if
(
isSystemAdministrator
(
user
))
{
return
true
;
}
const
{
roles
}
=
collectApplicationAuthorization
(
user
,
applicationName
);
return
intersects
(
roles
,
[
'管理'
]);
}
export
function
hasAuthAccess
(
authRule
,
user
=
getCurrentUserFromStorage
(),
applicationName
=
AUTHORIZATION_APPLICATION_NAME
,
)
{
if
(
!
user
)
{
return
false
;
}
if
(
user
.
type
===
'admin'
||
user
.
type
===
'system'
)
{
if
(
hasApplicationManagementAccess
(
user
,
applicationName
)
)
{
return
true
;
}
if
(
authRule
.
type
===
'group'
)
{
return
intersects
(
collectUserGroups
(
user
),
authRule
.
groups
);
const
{
groups
,
roles
,
permissions
}
=
collectApplicationAuthorization
(
user
,
applicationName
,
);
if
(
typeof
authRule
===
'string'
)
{
return
intersects
(
roles
,
[
authRule
]);
}
if
(
authRule
?.
type
===
'group'
)
{
return
intersects
(
groups
,
authRule
.
groups
);
}
if
(
authRule
.
type
===
'role'
)
{
return
intersects
(
collectUserRoles
(
user
)
,
authRule
.
roles
);
if
(
authRule
?
.
type
===
'role'
)
{
return
intersects
(
roles
,
authRule
.
roles
);
}
if
(
authRule
.
type
===
'permission'
)
{
return
intersects
(
collectUserPermissions
(
user
)
,
authRule
.
permissions
);
if
(
authRule
?
.
type
===
'permission'
)
{
return
intersects
(
permissions
,
authRule
.
permissions
);
}
if
(
authRule
.
type
===
'username'
)
{
return
intersects
([
user
.
username
,
user
.
display_name
,
user
.
_id
],
authRule
.
usernames
);
if
(
authRule
?
.
type
===
'username'
)
{
return
intersects
([
user
.
username
],
authRule
.
usernames
);
}
return
intersects
(
collectUserRoles
(
user
),
[
'管理'
])
;
return
false
;
}
export
function
getFieldValue
(
record
,
fieldPath
)
{
...
...
@@ -237,12 +282,21 @@ export function normalizeRecordLabel(value) {
return
normalizeText
(
value
);
}
export
function
shouldHideViewTitleColumn
(
view
)
{
return
view
?.
settings
?.
shortTitle
===
true
;
}
export
function
buildViewColumns
(
view
,
subject
,
{
includeHidden
=
false
}
=
{})
{
const
configuredColumns
=
view
?.
settings
?.
columns
||
[];
const
hideTitleColumn
=
!
includeHidden
&&
shouldHideViewTitleColumn
(
view
);
if
(
configuredColumns
.
length
>
0
)
{
return
configuredColumns
.
filter
((
column
)
=>
includeHidden
||
!
column
.
hide
)
.
filter
(
(
column
)
=>
(
includeHidden
||
!
column
.
hide
)
&&
(
!
hideTitleColumn
||
column
.
field
!==
'title'
),
)
.
map
((
column
)
=>
{
const
fieldDef
=
getFieldDefinition
(
column
.
field
,
subject
);
return
{
...
...
@@ -255,12 +309,14 @@ export function buildViewColumns(view, subject, { includeHidden = false } = {})
}
const
defaultColumns
=
[];
if
(
!
hideTitleColumn
)
{
defaultColumns
.
push
({
field
:
'title'
,
width
:
180
,
label
:
'标题'
,
fieldDef
:
{
name
:
'title'
,
label
:
'标题'
,
type
:
'text'
},
});
}
toArray
(
subject
?.
fields
).
forEach
((
field
)
=>
{
defaultColumns
.
push
({
...
...
@@ -274,6 +330,23 @@ export function buildViewColumns(view, subject, { includeHidden = false } = {})
return
defaultColumns
;
}
export
function
buildViewSearchColumns
(
view
,
subject
)
{
const
displayColumns
=
buildViewColumns
(
view
,
subject
);
if
(
displayColumns
.
some
((
column
)
=>
column
.
field
===
'title'
))
{
return
displayColumns
;
}
return
[
{
field
:
'title'
,
width
:
180
,
label
:
'标题'
,
fieldDef
:
getFieldDefinition
(
'title'
,
subject
),
},
...
displayColumns
,
];
}
function
compareRuleValue
(
recordValue
,
comparator
,
expectedValue
)
{
const
expectedValues
=
toArray
(
expectedValue
).
map
(
normalizeText
);
const
recordValues
=
toArray
(
recordValue
).
map
((
item
)
=>
{
...
...
@@ -332,18 +405,39 @@ export function checkViewCondition(condition, record) {
:
results
.
every
(
Boolean
);
}
export
function
isViewAccessible
(
view
,
user
=
getCurrentUserFromStorage
())
{
return
hasAuthAccess
(
view
?.
settings
?.
viewAuth
,
user
);
export
function
isViewAccessible
(
view
,
user
=
getCurrentUserFromStorage
(),
applicationName
=
AUTHORIZATION_APPLICATION_NAME
,
)
{
return
hasAuthAccess
(
getMultidimensionalTableAuthRule
(
view
?.
settings
?.
viewAuth
),
user
,
applicationName
,
);
}
export
function
isFieldDisabledByView
(
fieldPath
,
view
,
user
=
getCurrentUserFromStorage
())
{
export
function
isFieldDisabledByView
(
fieldPath
,
view
,
user
=
getCurrentUserFromStorage
(),
applicationName
=
AUTHORIZATION_APPLICATION_NAME
,
)
{
const
disabledColumn
=
toArray
(
view
?.
settings
?.
disableColumns
).
some
((
column
)
=>
column
.
field
===
fieldPath
);
if
(
disabledColumn
)
{
return
true
;
}
if
(
hasApplicationManagementAccess
(
user
,
applicationName
))
{
return
false
;
}
const
fieldAuthRule
=
toArray
(
view
?.
settings
?.
auth
).
find
((
rule
)
=>
rule
.
field
===
fieldPath
);
return
fieldAuthRule
?
!
hasAuthAccess
(
fieldAuthRule
,
user
)
:
false
;
return
!
hasAuthAccess
(
getMultidimensionalTableAuthRule
(
fieldAuthRule
),
user
,
applicationName
,
);
}
export
function
recordMatchesRestrictions
(
record
,
restrictions
=
[])
{
...
...
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