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
87cf7c70
Commit
87cf7c70
authored
Sep 01, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: sync view filters and reference selection
parent
da2b0019
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
115 additions
and
92 deletions
+115
-92
SKILL.md
.agents/skills/subject-schema-manager/SKILL.md
+27
-0
openai.yaml
.agents/skills/subject-schema-manager/agents/openai.yaml
+4
-0
ReferenceSearchSelect.jsx
src/components/ReferenceSearchSelect.jsx
+12
-10
XinyuanAutocomplete.jsx
src/components/XinyuanAutocomplete.jsx
+19
-43
index.jsx
src/components/record-workbench/index.jsx
+8
-5
SubjectWorkbench.jsx
src/pages/integrity_practice_management/SubjectWorkbench.jsx
+14
-21
integrity-practice-management.business-flow.md
...management/integrity-practice-management.business-flow.md
+3
-3
integrityPracticeWorkbenchService.js
..._practice_management/integrityPracticeWorkbenchService.js
+24
-4
subjectView.js
src/utils/subjectView.js
+4
-6
No files found.
.agents/skills/subject-schema-manager/SKILL.md
0 → 100644
View file @
87cf7c70
---
name
:
subject-schema-manager
description
:
同步 Stream Subject 对象文档,并在明确确认后创建、更新或删除对象与字段。
---
# Subject Schema Manager
本项目的 Subject 结构以线上 Subject API 为准,
`src/services/subjectsDefinition.md`
是版本化快照。
## 文档同步
使用项目内脚本或全局脚本执行同步:
```
bash
node /Users/drigle/.codex/skills/subject-schema-manager/scripts/subject_schema_manager.mjs
sync
\
--project
/Users/drigle/Desktop/xy/integrity_practice_management_new
--ticket-stdin
```
脚本从源代码发现 Subject 根对象,递归跟踪字段
`settings.subject`
,只更新
`subjectsDefinition.md`
的托管区块,不修改线上对象。
## 对象与字段变更
创建、更新或删除对象/字段前,必须先读取
`src/services/record-api.md`
与
`src/services/subjectsDefinition.md`
,生成包含对象名、字段别名、payload 和依赖顺序的计划,并获得用户明确确认。确认后使用脚本
`apply --plan-file ... --confirmed`
执行限定操作。
引用字段只保存目标
`_id`
或
`_id[]`
;日期使用 ISO 8601;业务字段始终放在
`metadata`
中。文档清理不等同于远程删除,删除必须在确认计划中显式列出。
Subject schema 同步或变更不改变业务流程;若流程发生变化,必须同步更新对应
`*.business-flow.md`
。
.agents/skills/subject-schema-manager/agents/openai.yaml
0 → 100644
View file @
87cf7c70
interface
:
display_name
:
Subject Schema Manager
short_description
:
同步对象文档并管理 Subject 对象与字段
default_prompt
:
同步当前项目 Subject 定义,或按确认计划创建、更新、删除对象与字段。
src/components/ReferenceSearchSelect.jsx
View file @
87cf7c70
...
@@ -15,6 +15,16 @@ function normalizeText(value) {
...
@@ -15,6 +15,16 @@ function normalizeText(value) {
return
String
(
value
||
''
).
trim
();
return
String
(
value
||
''
).
trim
();
}
}
function
normalizeRecordId
(
value
)
{
if
(
value
===
null
||
value
===
undefined
||
value
===
''
)
return
''
;
if
(
typeof
value
===
'object'
)
{
const
candidate
=
value
.
_id
??
value
.
id
??
value
.
$oid
??
value
.
name
;
if
(
candidate
===
value
)
return
''
;
return
normalizeRecordId
(
candidate
);
}
return
normalizeText
(
value
);
}
function
escapeMongoRegex
(
value
)
{
function
escapeMongoRegex
(
value
)
{
return
normalizeText
(
value
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
return
normalizeText
(
value
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
}
}
...
@@ -74,19 +84,11 @@ function resolveRecordId(record) {
...
@@ -74,19 +84,11 @@ function resolveRecordId(record) {
}
}
// Reference 写入协议:只接受 _id / id
// Reference 写入协议:只接受 _id / id
return
record
.
id
||
record
.
_id
;
return
normalizeRecordId
(
record
.
id
??
record
.
_id
)
;
}
}
function
resolveReferenceValueId
(
value
)
{
function
resolveReferenceValueId
(
value
)
{
if
(
!
value
)
{
return
normalizeRecordId
(
value
);
return
''
;
}
if
(
typeof
value
===
'object'
)
{
return
normalizeText
(
resolveRecordId
(
value
));
}
return
normalizeText
(
value
);
}
}
function
resolveRecordSubjectName
(
record
,
fallbackSubject
=
''
)
{
function
resolveRecordSubjectName
(
record
,
fallbackSubject
=
''
)
{
...
...
src/components/XinyuanAutocomplete.jsx
View file @
87cf7c70
...
@@ -20,10 +20,10 @@ function defaultFilter(textValue, inputValue) {
...
@@ -20,10 +20,10 @@ function defaultFilter(textValue, inputValue) {
function
getSelectedKeySet
(
selectionMode
,
selectedKey
,
selectedKeys
)
{
function
getSelectedKeySet
(
selectionMode
,
selectedKey
,
selectedKeys
)
{
if
(
selectionMode
===
'multiple'
)
{
if
(
selectionMode
===
'multiple'
)
{
return
new
Set
(
selectedKeys
||
[]);
if
(
selectedKeys
===
'all'
)
return
new
Set
();
return
new
Set
(
Array
.
from
(
selectedKeys
||
[]).
map
(
toSelectionKey
).
filter
(
Boolean
));
}
}
return
new
Set
(
selectedKey
===
null
||
selectedKey
===
undefined
?
[]
:
[
toSelectionKey
(
selectedKey
)].
filter
(
Boolean
));
return
new
Set
(
selectedKey
===
null
||
selectedKey
===
undefined
?
[]
:
[
selectedKey
]);
}
}
function
toSelectionKey
(
value
)
{
function
toSelectionKey
(
value
)
{
...
@@ -76,42 +76,20 @@ export default function XinyuanAutocomplete({
...
@@ -76,42 +76,20 @@ export default function XinyuanAutocomplete({
(
selectedOptions
.
length
>
0
(
selectedOptions
.
length
>
0
?
selectedOptions
.
map
((
option
)
=>
option
.
label
).
join
(
'、'
)
?
selectedOptions
.
map
((
option
)
=>
option
.
label
).
join
(
'、'
)
:
placeholder
);
:
placeholder
);
const
rootSelectionProps
=
const
isMultiple
=
selectionMode
===
'multiple'
;
selectionMode
===
'multiple'
const
rootSelectionProps
=
isMultiple
?
{
selectedKeys
:
selectedKeyStringSet
}
?
{
selectedKey
:
null
}
:
{
selectedKey
};
:
{
selectedKey
:
selectedKeyStringSet
.
values
().
next
().
value
||
null
};
const
listBoxSelectionProps
=
const
listBoxSelectionProps
=
isMultiple
selectionMode
===
'multiple'
?
{
selectionMode
:
'single'
,
onAction
:
(
key
)
=>
{
?
{
const
optionKey
=
toSelectionKey
(
key
);
selectionMode
,
if
(
!
optionKey
||
!
onSelectionChange
)
return
;
selectedKeys
:
selectedKeyStringSet
,
const
nextKeys
=
new
Set
(
selectedKeyStringSet
);
}
if
(
nextKeys
.
has
(
optionKey
))
nextKeys
.
delete
(
optionKey
);
else
nextKeys
.
add
(
optionKey
);
:
{};
const
visibleOptions
=
isLoading
||
errorText
?
[]
:
options
;
const
handleMultipleOptionPress
=
(
option
)
=>
{
if
(
selectionMode
!==
'multiple'
||
!
onSelectionChange
||
option
?.
isDisabled
)
{
return
;
}
const
optionKey
=
toSelectionKey
(
option
.
id
);
if
(
!
optionKey
)
{
return
;
}
const
nextKeys
=
new
Set
(
Array
.
from
(
selectedKeyStringSet
).
filter
(
Boolean
),
);
if
(
nextKeys
.
has
(
optionKey
))
{
nextKeys
.
delete
(
optionKey
);
}
else
{
nextKeys
.
add
(
optionKey
);
}
onSelectionChange
(
nextKeys
);
onSelectionChange
(
nextKeys
);
};
}
}
:
{
selectionMode
:
'single'
,
selectedKeys
:
selectedKeyStringSet
,
onSelectionChange
};
const
visibleOptions
=
isLoading
||
errorText
?
[]
:
options
.
map
((
option
)
=>
({
...
option
,
isSelected
:
selectedKeyStringSet
.
has
(
toSelectionKey
(
option
.
id
))
}));
return
(
return
(
<
Autocomplete
<
Autocomplete
...
@@ -123,7 +101,7 @@ export default function XinyuanAutocomplete({
...
@@ -123,7 +101,7 @@ export default function XinyuanAutocomplete({
isOpen=
{
isOpen
}
isOpen=
{
isOpen
}
isRequired=
{
isRequired
}
isRequired=
{
isRequired
}
name=
{
name
}
name=
{
name
}
selectionMode=
{
selectionMode
}
selectionMode=
{
isMultiple
?
'single'
:
selectionMode
}
variant=
{
variant
}
variant=
{
variant
}
onOpenChange=
{
onOpenChange
}
onOpenChange=
{
onOpenChange
}
onSelectionChange=
{
onSelectionChange
}
onSelectionChange=
{
onSelectionChange
}
...
@@ -180,16 +158,14 @@ export default function XinyuanAutocomplete({
...
@@ -180,16 +158,14 @@ export default function XinyuanAutocomplete({
)
}
)
}
>
>
{
(
option
)
=>
{
{
(
option
)
=>
{
const
isSelected
=
selectedKeyStringSet
.
has
(
toSelectionKey
(
option
.
id
)
);
const
isSelected
=
Boolean
(
option
.
isSelected
);
return
(
return
(
<
ListBox
.
Item
<
ListBox
.
Item
id=
{
option
.
id
}
id=
{
option
.
id
}
isDisabled=
{
option
.
isDisabled
}
isDisabled=
{
option
.
isDisabled
}
isSelected=
{
isSelected
}
textValue=
{
option
.
textValue
||
option
.
label
}
textValue=
{
option
.
textValue
||
option
.
label
}
{
...
(
selectionMode
===
'
multiple
'
?
{
onPress
:
()
=
>
handleMultipleOptionPress(option) }
:
{}
)}
>
>
{
renderOption
?
(
{
renderOption
?
(
renderOption
(
option
)
renderOption
(
option
)
...
...
src/components/record-workbench/index.jsx
View file @
87cf7c70
...
@@ -6,6 +6,7 @@ import { DataGrid } from '@heroui-pro/react/data-grid';
...
@@ -6,6 +6,7 @@ import { DataGrid } from '@heroui-pro/react/data-grid';
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
EmptyState
}
from
'@heroui-pro/react/empty-state'
;
import
{
Alert
,
Button
,
Dropdown
,
Label
,
Pagination
,
SearchField
,
Spinner
,
Surface
,
Tabs
,
Tooltip
,
buttonVariants
}
from
'@heroui/react'
;
import
{
Alert
,
Button
,
Dropdown
,
Label
,
Pagination
,
SearchField
,
Spinner
,
Surface
,
Tabs
,
Tooltip
,
buttonVariants
}
from
'@heroui/react'
;
import
{
useLayoutEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
{
useLayoutEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
XinyuanAutocomplete
from
'@/components/XinyuanAutocomplete'
;
const
EMPTY_ARRAY
=
[];
const
EMPTY_ARRAY
=
[];
const
ACTION_ICON_CLASS
=
'!grid size-4 shrink-0 place-items-center !leading-none [&>svg]:!m-0 [&>svg]:block [&>svg]:!size-4'
;
const
ACTION_ICON_CLASS
=
'!grid size-4 shrink-0 place-items-center !leading-none [&>svg]:!m-0 [&>svg]:block [&>svg]:!size-4'
;
...
@@ -313,8 +314,8 @@ export default function RecordWorkbench({
...
@@ -313,8 +314,8 @@ export default function RecordWorkbench({
else onSelectionChange?.(Array.from(selection).map(String));
else onSelectionChange?.(Array.from(selection).map(String));
};
};
const listSearchField =
filterSchema.find((field) => field?.type === 'text') ||
null;
const listSearchField = null;
const filterCardSchema = filterSchema
.filter((field) => field !== listSearchField && field?.type === 'text')
;
const filterCardSchema = filterSchema;
const hasViewSwitch = views.length > 1;
const hasViewSwitch = views.length > 1;
return (
return (
...
@@ -340,11 +341,13 @@ export default function RecordWorkbench({
...
@@ -340,11 +341,13 @@ export default function RecordWorkbench({
<Surface className="p-6">
<Surface className="p-6">
<div className="flex flex-col gap-3 md:flex-row md:items-end">
<div className="flex flex-col gap-3 md:flex-row md:items-end">
<div className="grid min-w-0 flex-1 gap-3 md:grid-cols-3">
<div className="grid min-w-0 flex-1 gap-3 md:grid-cols-3">
{filterCardSchema.map((field) => field.type === 'text' ? (
{filterCardSchema.map((field) => field.type === 'select' ? (
<XinyuanAutocomplete key={field.key} aria-label={field.label} options={field.options || []} selectedKey={filters[field.key] || null} placeholder={field.placeholder} valueText={(field.options || []).find((item) => item.value === filters[field.key])?.label || field.placeholder} onSelectionChange={(key) => onFiltersChange?.(field.key, key ? String(key) : '')} />
) : (
<SearchField aria-label={field.label} fullWidth key={field.key} value={filters[field.key] || ''} onChange={(value) => onFiltersChange?.(field.key, value)}>
<SearchField aria-label={field.label} fullWidth key={field.key} value={filters[field.key] || ''} onChange={(value) => onFiltersChange?.(field.key, value)}>
<SearchField.Group><SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon><SearchField.Input placeholder={field.placeholder} /><SearchField.ClearButton aria-label="清空搜索" /></SearchField.Group>
<SearchField.Group><SearchField.SearchIcon><Search size={16} /></SearchField.SearchIcon><SearchField.Input
type={field.type === 'date' ? 'date' : 'text'}
placeholder={field.placeholder} /><SearchField.ClearButton aria-label="清空搜索" /></SearchField.Group>
</SearchField>
</SearchField>
)
: null
)}
))}
</div>
</div>
{onClearFilters ? (
{onClearFilters ? (
<div className="flex shrink-0 items-center">
<div className="flex shrink-0 items-center">
...
...
src/pages/integrity_practice_management/SubjectWorkbench.jsx
View file @
87cf7c70
...
@@ -13,6 +13,7 @@ import {
...
@@ -13,6 +13,7 @@ import {
import
{
import
{
actionNeedsAssignment
,
actionNeedsAssignment
,
buildSubjectColumns
,
buildSubjectColumns
,
buildSubjectFilterSchema
,
DEFAULT_PAGE_SIZE
,
DEFAULT_PAGE_SIZE
,
executeConfiguredAction
,
executeConfiguredAction
,
isConfiguredActionVisible
,
isConfiguredActionVisible
,
...
@@ -78,8 +79,7 @@ export default function SubjectWorkbench() {
...
@@ -78,8 +79,7 @@ export default function SubjectWorkbench() {
const
[
activeViewName
,
setActiveViewName
]
=
useState
(
''
);
const
[
activeViewName
,
setActiveViewName
]
=
useState
(
''
);
const
[
records
,
setRecords
]
=
useState
(
EMPTY_ARRAY
);
const
[
records
,
setRecords
]
=
useState
(
EMPTY_ARRAY
);
const
[
selectedIds
,
setSelectedIds
]
=
useState
(
EMPTY_ARRAY
);
const
[
selectedIds
,
setSelectedIds
]
=
useState
(
EMPTY_ARRAY
);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
filters
,
setFilters
]
=
useState
({});
const
[
debouncedKeyword
,
setDebouncedKeyword
]
=
useState
(
''
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
page
,
setPage
]
=
useState
(
0
);
const
pageSize
=
DEFAULT_PAGE_SIZE
;
const
pageSize
=
DEFAULT_PAGE_SIZE
;
const
[
pagination
,
setPagination
]
=
useState
(
null
);
const
[
pagination
,
setPagination
]
=
useState
(
null
);
...
@@ -100,10 +100,7 @@ export default function SubjectWorkbench() {
...
@@ -100,10 +100,7 @@ export default function SubjectWorkbench() {
[
activeView
,
subject
],
[
activeView
,
subject
],
);
);
useEffect
(()
=>
{
const
filterSchema
=
useMemo
(()
=>
buildSubjectFilterSchema
(
activeView
,
subject
),
[
activeView
,
subject
]);
const
timer
=
window
.
setTimeout
(()
=>
setDebouncedKeyword
(
keyword
),
250
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
keyword
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
cancelled
=
false
;
let
cancelled
=
false
;
...
@@ -111,7 +108,7 @@ export default function SubjectWorkbench() {
...
@@ -111,7 +108,7 @@ export default function SubjectWorkbench() {
setError
(
''
);
setError
(
''
);
setRecords
(
EMPTY_ARRAY
);
setRecords
(
EMPTY_ARRAY
);
setSelectedIds
(
EMPTY_ARRAY
);
setSelectedIds
(
EMPTY_ARRAY
);
set
Keyword
(
''
);
set
Filters
({}
);
setPage
(
0
);
setPage
(
0
);
loadSubjectResources
(
subjectName
,
subjectConfig
)
loadSubjectResources
(
subjectName
,
subjectConfig
)
...
@@ -144,7 +141,10 @@ export default function SubjectWorkbench() {
...
@@ -144,7 +141,10 @@ export default function SubjectWorkbench() {
view
:
activeView
,
view
:
activeView
,
page
,
page
,
pageSize
,
pageSize
,
keyword
:
debouncedKeyword
,
filters
:
{
...
filters
,
__fieldDefs
:
Object
.
fromEntries
(
columns
.
map
((
column
)
=>
[
column
.
field
,
column
.
fieldDef
])),
},
pagination
:
paginationEnabled
,
pagination
:
paginationEnabled
,
})
})
.
then
((
result
)
=>
{
.
then
((
result
)
=>
{
...
@@ -162,7 +162,7 @@ export default function SubjectWorkbench() {
...
@@ -162,7 +162,7 @@ export default function SubjectWorkbench() {
return
()
=>
{
return
()
=>
{
cancelled
=
true
;
cancelled
=
true
;
};
};
},
[
subjectName
,
activeView
,
page
,
pageSize
,
debouncedKeyword
,
paginationEnabled
,
refreshKey
]);
},
[
subjectName
,
activeView
,
page
,
pageSize
,
filters
,
columns
,
paginationEnabled
,
refreshKey
]);
const
runAction
=
useCallback
(
const
runAction
=
useCallback
(
async
(
actionName
,
context
,
assignment
=
null
)
=>
{
async
(
actionName
,
context
,
assignment
=
null
)
=>
{
...
@@ -283,21 +283,14 @@ export default function SubjectWorkbench() {
...
@@ -283,21 +283,14 @@ export default function SubjectWorkbench() {
setSelectedIds
(
EMPTY_ARRAY
);
setSelectedIds
(
EMPTY_ARRAY
);
setPage
(
0
);
setPage
(
0
);
}
}
}
}
filters=
{
{
keyword
}
}
filters=
{
filters
}
filterSchema=
{
[
filterSchema=
{
filterSchema
}
{
onFiltersChange=
{
(
key
,
value
)
=>
{
key
:
'keyword'
,
setFilters
((
current
)
=>
({
...
current
,
[
key
]:
value
}));
label
:
'查找记录'
,
type
:
'text'
,
placeholder
:
'搜索标题'
,
},
]
}
onFiltersChange=
{
(
_
,
value
)
=>
{
setKeyword
(
value
);
setPage
(
0
);
setPage
(
0
);
}
}
}
}
onClearFilters=
{
()
=>
{
onClearFilters=
{
()
=>
{
set
Keyword
(
''
);
set
Filters
({}
);
setPage
(
0
);
setPage
(
0
);
}
}
}
}
title=
{
subjectConfig
.
title
||
subject
?.
title
||
subjectName
}
title=
{
subjectConfig
.
title
||
subject
?.
title
||
subjectName
}
...
...
src/pages/integrity_practice_management/integrity-practice-management.business-flow.md
View file @
87cf7c70
...
@@ -23,11 +23,11 @@
...
@@ -23,11 +23,11 @@
1.
进入页面后并行调用
`GET /subject/:name`
和
`GET /subject/:name/view`
;打开详情后再按 View 名调用
`GET /subject/:subjectName/view/:viewName`
获取完整 Detail 配置。
1.
进入页面后并行调用
`GET /subject/:name`
和
`GET /subject/:name/view`
;打开详情后再按 View 名调用
`GET /subject/:subjectName/view/:viewName`
获取完整 Detail 配置。
2.
只展示当前用户有权访问的
`grid`
视图;授权只读取
`user.applications[]`
中
`name = integrity_practice_management`
的项目。系统管理员、系统账号和该应用“管理”角色可旁路,但不能旁路
`disableColumns`
。配置包含
`filterView`
且非空时,只展示指定视图。
2.
只展示当前用户有权访问的
`grid`
视图;授权只读取
`user.applications[]`
中
`name = integrity_practice_management`
的项目。系统管理员、系统账号和该应用“管理”角色可旁路,但不能旁路
`disableColumns`
。配置包含
`filterView`
且非空时,只展示指定视图。
3.
切换视图后,根据视图的
`settings.restrictions`
、
`settings.filters.conditions`
、列排序配置调用
`POST /record/query`
。
3.
切换视图后,根据视图的
`settings.restrictions`
、
`settings.filters.conditions`
、列排序配置调用
`POST /record/query`
;筛选控件由当前 View 的可见字段自动生成,文本字段使用远程模糊查询,Select/日期字段使用字段配置值
。
4.
`gridRecordConfig.pagination = false`
时单次最多读取 1000 条;其余页面使用服务端分页。
4.
`gridRecordConfig.pagination = false`
时单次最多读取 1000 条;其余页面使用服务端分页。
5.
关键词查找通过
`/record/query`
的
`keyword`
查询标题;
清空筛选后回到第 1 页。
5.
列表不再提供独立关键词搜索;用户通过当前视图字段筛选,筛选条件直接写入
`/record/query`
的
`filter`
,
清空筛选后回到第 1 页。
6.
列定义、列顺序、初始排序、固定列和字段显示格式由 Subject View 与 Subject 字段定义共同决定;HeroUI Pro DataGrid 仅在真实横向溢出时固定首列和操作列,并允许当前页原生列排序和列宽调整,不改变服务端分页口径。
6.
列定义、列顺序、初始排序、固定列和字段显示格式由 Subject View 与 Subject 字段定义共同决定;HeroUI Pro DataGrid 仅在真实横向溢出时固定首列和操作列,并允许当前页原生列排序和列宽调整,不改变服务端分页口径。
`settings.shortTitle === true`
时只隐藏 title 展示列,标题仍
参与查询、记录身份和保存;false 或未配置时正常展
示。
`settings.shortTitle === true`
时只隐藏 title 展示列,标题仍
作为记录身份和保存字段;false 或未配置时正常展示。若 View 配置包含标题列,标题也会作为可筛选字段显
示。
7.
DataGrid 使用原生 Checkbox 选择;选中后显示 HeroUI Pro ActionBar。行操作 1-3 项直接展示,4 项以上展示前两项与“更多”菜单,删除等危险动作始终排在最后。
7.
DataGrid 使用原生 Checkbox 选择;选中后显示 HeroUI Pro ActionBar。行操作 1-3 项直接展示,4 项以上展示前两项与“更多”菜单,删除等危险动作始终排在最后。
8.
点击列表操作列的预览图标打开 HeroUI Modal 并进入只读查看态;首个业务列只展示记录文本,不再承担预览交互。点击弹窗统一的“编辑”按钮或列表操作列铅笔图标进入整体编辑态,保存后返回查看态。字段权限、锁定规则和 Subject 字段类型保持不变。
8.
点击列表操作列的预览图标打开 HeroUI Modal 并进入只读查看态;首个业务列只展示记录文本,不再承担预览交互。点击弹窗统一的“编辑”按钮或列表操作列铅笔图标进入整体编辑态,保存后返回查看态。字段权限、锁定规则和 Subject 字段类型保持不变。
9.
当前 Detail 有有效
`settings.auth`
或
`settings.disableColumns`
时,字段权限整套使用 Detail;两项都未配置或没有实际 Detail 时,整体回退打开详情的当前 Grid。两套权限不合并、不逐字段回退;
`disableColumns`
对管理员也始终只读,普通用户在选定权限来源中缺少字段
`auth`
时只读。切换 Detail 后立即重算字段状态和编辑入口,编辑入口只检查当前 Detail 展示字段。Grid
`viewAuth`
只控制 Tab 可见性,不隐藏详情字段。
9.
当前 Detail 有有效
`settings.auth`
或
`settings.disableColumns`
时,字段权限整套使用 Detail;两项都未配置或没有实际 Detail 时,整体回退打开详情的当前 Grid。两套权限不合并、不逐字段回退;
`disableColumns`
对管理员也始终只读,普通用户在选定权限来源中缺少字段
`auth`
时只读。切换 Detail 后立即重算字段状态和编辑入口,编辑入口只检查当前 Detail 展示字段。Grid
`viewAuth`
只控制 Tab 可见性,不隐藏详情字段。
...
...
src/pages/integrity_practice_management/integrityPracticeWorkbenchService.js
View file @
87cf7c70
...
@@ -17,6 +17,10 @@ import {
...
@@ -17,6 +17,10 @@ import {
import
DocumentService
from
'@/services/DocumentService'
;
import
DocumentService
from
'@/services/DocumentService'
;
import
{
import
{
buildViewColumns
,
buildViewColumns
,
getSelectOptions
,
isDateFieldType
,
isReferenceFieldType
,
getReferenceSubjectName
,
checkViewCondition
,
checkViewCondition
,
isViewAccessible
,
isViewAccessible
,
mergeRestrictionsIntoFilter
,
mergeRestrictionsIntoFilter
,
...
@@ -31,7 +35,7 @@ import * as XLSX from 'xlsx';
...
@@ -31,7 +35,7 @@ import * as XLSX from 'xlsx';
export
const
DEFAULT_PAGE_SIZE
=
10
;
export
const
DEFAULT_PAGE_SIZE
=
10
;
function
buildViewFilter
(
view
)
{
function
buildViewFilter
(
view
,
filters
=
{}
)
{
const
restrictions
=
view
?.
settings
?.
restrictions
||
[];
const
restrictions
=
view
?.
settings
?.
restrictions
||
[];
const
filter
=
mergeRestrictionsIntoFilter
({},
restrictions
);
const
filter
=
mergeRestrictionsIntoFilter
({},
restrictions
);
const
conditions
=
view
?.
settings
?.
filters
?.
conditions
||
[];
const
conditions
=
view
?.
settings
?.
filters
?.
conditions
||
[];
...
@@ -46,6 +50,14 @@ function buildViewFilter(view) {
...
@@ -46,6 +50,14 @@ function buildViewFilter(view) {
filter
[
condition
.
field
]
=
comparator
===
'$eq'
?
value
:
{
[
comparator
]:
value
};
filter
[
condition
.
field
]
=
comparator
===
'$eq'
?
value
:
{
[
comparator
]:
value
};
});
});
Object
.
entries
(
filters
).
forEach
(([
field
,
value
])
=>
{
if
(
!
field
||
field
===
'__fieldDefs'
||
value
===
undefined
||
value
===
null
||
String
(
value
).
trim
()
===
''
)
return
;
const
type
=
String
(
filters
.
__fieldDefs
?.[
field
]?.
type
||
''
).
toLowerCase
();
filter
[
field
]
=
[
'text'
,
'long_text'
,
'rich_text'
].
includes
(
type
)
?
{
$regex
:
String
(
value
).
trim
(),
$options
:
'i'
}
:
Array
.
isArray
(
value
)
?
{
$in
:
value
}
:
value
;
});
return
filter
;
return
filter
;
}
}
...
@@ -90,7 +102,7 @@ export async function loadSubjectRecords({
...
@@ -90,7 +102,7 @@ export async function loadSubjectRecords({
view
,
view
,
page
=
0
,
page
=
0
,
pageSize
=
DEFAULT_PAGE_SIZE
,
pageSize
=
DEFAULT_PAGE_SIZE
,
keyword
=
''
,
filters
=
{}
,
pagination
=
true
,
pagination
=
true
,
})
{
})
{
const
requestPageSize
=
pagination
?
pageSize
:
1000
;
const
requestPageSize
=
pagination
?
pageSize
:
1000
;
...
@@ -98,9 +110,8 @@ export async function loadSubjectRecords({
...
@@ -98,9 +110,8 @@ export async function loadSubjectRecords({
subject
:
subjectName
,
subject
:
subjectName
,
page
:
pagination
?
page
:
0
,
page
:
pagination
?
page
:
0
,
page_size
:
requestPageSize
,
page_size
:
requestPageSize
,
filter
:
buildViewFilter
(
view
),
filter
:
buildViewFilter
(
view
,
filters
),
sort
:
buildViewSort
(
view
),
sort
:
buildViewSort
(
view
),
keyword
:
normalizeText
(
keyword
)
||
undefined
,
mode
:
'full'
,
mode
:
'full'
,
});
});
const
records
=
sortRecordsByView
(
toArray
(
result
?.
records
),
view
);
const
records
=
sortRecordsByView
(
toArray
(
result
?.
records
),
view
);
...
@@ -123,6 +134,15 @@ export function buildSubjectColumns(view, subject) {
...
@@ -123,6 +134,15 @@ export function buildSubjectColumns(view, subject) {
return
buildViewColumns
(
view
,
subject
);
return
buildViewColumns
(
view
,
subject
);
}
}
export
function
buildSubjectFilterSchema
(
view
,
subject
)
{
return
buildViewColumns
(
view
,
subject
).
filter
((
column
)
=>
column
?.
field
).
map
((
column
)
=>
{
const
fieldDef
=
column
.
fieldDef
||
{};
const
type
=
String
(
fieldDef
.
type
||
'text'
).
toLowerCase
();
const
options
=
getSelectOptions
(
fieldDef
);
return
{
key
:
column
.
field
,
field
:
column
.
field
,
label
:
column
.
label
||
fieldDef
.
label
||
column
.
field
,
type
:
type
===
'select'
&&
options
.
length
?
'select'
:
isDateFieldType
(
type
)
?
'date'
:
isReferenceFieldType
(
type
)
&&
getReferenceSubjectName
(
fieldDef
)
?
'reference'
:
'text'
,
options
,
fieldDef
,
placeholder
:
`筛选
${
column
.
label
||
fieldDef
.
label
||
column
.
field
}
`
};
});
}
export
function
isConfiguredActionVisible
(
action
,
record
)
{
export
function
isConfiguredActionVisible
(
action
,
record
)
{
return
checkViewCondition
(
action
?.
visabled
,
record
?.
raw
||
record
);
return
checkViewCondition
(
action
?.
visabled
,
record
?.
raw
||
record
);
}
}
...
...
src/utils/subjectView.js
View file @
87cf7c70
...
@@ -252,14 +252,12 @@ export function isDateFieldType(fieldType) {
...
@@ -252,14 +252,12 @@ export function isDateFieldType(fieldType) {
}
}
export
function
normalizeRecordId
(
value
)
{
export
function
normalizeRecordId
(
value
)
{
if
(
!
value
)
{
if
(
value
===
null
||
value
===
undefined
||
value
===
''
)
return
''
;
return
''
;
}
if
(
typeof
value
===
'object'
)
{
if
(
typeof
value
===
'object'
)
{
return
value
.
_id
||
value
.
id
||
value
.
name
||
''
;
const
candidate
=
value
.
_id
??
value
.
id
??
value
.
$oid
??
value
.
name
;
if
(
candidate
===
value
)
return
''
;
return
normalizeRecordId
(
candidate
);
}
}
return
normalizeText
(
value
);
return
normalizeText
(
value
);
}
}
...
...
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