Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
A
audit_project_manager
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
audit_project_manager
Commits
936e5fa0
Commit
936e5fa0
authored
Sep 01, 2026
by
drigle
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: generate remote filters from project views
parent
230b53ca
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
343 additions
and
76 deletions
+343
-76
SubjectRecordWorkbench.jsx
src/components/SubjectRecordWorkbench.jsx
+81
-40
index.jsx
src/components/record-workbench/index.jsx
+21
-3
SubjectWorkbenchService.js
src/pages/project_manager_xy/SubjectWorkbenchService.js
+239
-31
project-management.business-flow.md
...es/project_manager_xy/project-management.business-flow.md
+2
-2
No files found.
src/components/SubjectRecordWorkbench.jsx
View file @
936e5fa0
...
...
@@ -6,19 +6,64 @@ import {
cloneRecord
,
}
from
'@/services/DataService'
;
import
{
Copy
,
Delete
,
Edit
,
PlusIcon
}
from
'@/components/AppIcons'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
getReferenceSubjectName
,
getSelectOptions
,
isDateFieldType
,
isReferenceFieldType
}
from
'@/utils/subjectView'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
,
useState
}
from
'react'
;
import
DynamicRecordDialog
from
'./DynamicRecordDialog'
;
import
{
buildConfiguredWorkbenchView
,
buildGridViews
,
buildViewCountMap
,
collectProjectYears
,
loadSubjectWorkbenchResources
,
}
from
'@/pages/project_manager_xy/SubjectWorkbenchService'
;
const
DEFAULT_PAGE_SIZE
=
20
;
const
PAGE_SIZE_OPTIONS
=
[
20
,
50
,
100
];
const
BOOLEAN_FILTER_OPTIONS
=
[
{
label
:
'是'
,
value
:
'true'
},
{
label
:
'否'
,
value
:
'false'
},
];
function
buildFilterSchema
(
columns
=
[])
{
return
columns
.
reduce
((
fields
,
column
)
=>
{
const
fieldDef
=
column
?.
fieldDef
||
{};
const
fieldType
=
String
(
fieldDef
.
type
||
'text'
).
toLowerCase
();
const
field
=
column
?.
field
;
if
(
!
field
)
return
fields
;
let
controlType
=
null
;
let
options
;
if
(
fieldType
===
'select'
)
{
options
=
getSelectOptions
(
fieldDef
);
controlType
=
options
.
length
>
0
?
'select'
:
'text'
;
}
else
if
(
fieldType
===
'boolean'
)
{
controlType
=
'select'
;
options
=
BOOLEAN_FILTER_OPTIONS
;
}
else
if
(
isDateFieldType
(
fieldType
))
{
controlType
=
'date'
;
}
else
if
(
fieldType
===
'number'
)
{
controlType
=
'number'
;
}
else
if
(
fieldType
===
'user'
)
{
controlType
=
'user'
;
}
else
if
(
isReferenceFieldType
(
fieldType
))
{
controlType
=
getReferenceSubjectName
(
fieldDef
)
?
'reference'
:
'text'
;
}
else
if
([
'text'
,
'long_text'
,
'rich_text'
].
includes
(
fieldType
))
{
controlType
=
'text'
;
}
if
(
!
controlType
)
return
fields
;
fields
.
push
({
key
:
`field:
${
field
}
`
,
field
,
fieldDef
,
label
:
column
.
label
||
field
,
type
:
controlType
,
options
,
placeholder
:
`筛选
${
column
.
label
||
field
}
`
,
});
return
fields
;
},
[]);
}
function
normalizeActionContext
(
context
,
helpers
)
{
return
{
...
context
,
...
...
@@ -52,16 +97,20 @@ export default function SubjectRecordWorkbench({
const
[
error
,
setError
]
=
useState
(
''
);
const
[
success
,
setSuccess
]
=
useState
(
''
);
const
[
activeViewName
,
setActiveViewName
]
=
useState
(
''
);
const
[
filters
,
setFilters
]
=
useState
({
keyword
:
''
,
status
:
''
,
year
:
''
});
const
[
filters
,
setFilters
]
=
useState
({});
const
[
selectedIds
,
setSelectedIds
]
=
useState
([]);
const
[
page
,
setPage
]
=
useState
(
0
);
const
[
pageSize
,
setPageSize
]
=
useState
(
DEFAULT_PAGE_SIZE
);
const
[
totalRecords
,
setTotalRecords
]
=
useState
(
0
);
const
[
recordDialogOpen
,
setRecordDialogOpen
]
=
useState
(
false
);
const
[
editingRecord
,
setEditingRecord
]
=
useState
(
null
);
const
[
deleteTargets
,
setDeleteTargets
]
=
useState
([]);
const
[
deleting
,
setDeleting
]
=
useState
(
false
);
const
requestSequence
=
useRef
(
0
);
const
reload
=
useCallback
(
async
({
keepFeedback
=
false
}
=
{})
=>
{
const
requestId
=
requestSequence
.
current
+
1
;
requestSequence
.
current
=
requestId
;
setLoading
(
true
);
setError
(
''
);
if
(
!
keepFeedback
)
{
...
...
@@ -69,34 +118,47 @@ export default function SubjectRecordWorkbench({
}
try
{
const
resources
=
await
loadSubjectWorkbenchResources
(
subjectName
,
query
);
const
resources
=
await
loadSubjectWorkbenchResources
(
subjectName
,
query
,
{
viewName
:
activeViewName
,
filters
,
page
,
pageSize
,
});
if
(
requestId
!==
requestSequence
.
current
)
return
;
setRecords
(
resources
.
records
||
[]);
setTotalRecords
(
resources
.
totalCount
||
0
);
setSubject
(
resources
.
subject
||
null
);
setViews
(
resources
.
views
||
[]);
}
catch
(
requestError
)
{
if
(
requestId
!==
requestSequence
.
current
)
return
;
setRecords
([]);
setTotalRecords
(
0
);
setSubject
(
null
);
setViews
([]);
setError
(
requestError
?.
message
||
'列表加载失败,请稍后重试。'
);
}
finally
{
if
(
requestId
===
requestSequence
.
current
)
{
setLoading
(
false
);
}
},
[
subjectName
,
query
]);
}
},
[
activeViewName
,
filters
,
page
,
pageSize
,
query
,
subjectName
]);
useEffect
(()
=>
{
reload
();
},
[
reload
]);
const
delay
=
Object
.
values
(
filters
).
some
((
value
)
=>
String
(
value
||
''
).
trim
())
?
300
:
0
;
const
timer
=
window
.
setTimeout
(()
=>
reload
(),
delay
);
return
()
=>
window
.
clearTimeout
(
timer
);
},
[
filters
,
reload
]);
const
handleViewChange
=
useCallback
(
async
(
viewName
)
=>
{
const
handleViewChange
=
useCallback
((
viewName
)
=>
{
if
(
!
viewName
||
viewName
===
activeViewName
)
{
return
;
}
setActiveViewName
(
viewName
);
setFilters
({});
setSelectedIds
([]);
setPage
(
0
);
await
reload
({
keepFeedback
:
true
});
},
[
activeViewName
,
reload
]);
},
[
activeViewName
]);
const
gridViews
=
useMemo
(()
=>
buildGridViews
(
views
),
[
views
]);
...
...
@@ -116,21 +178,17 @@ export default function SubjectRecordWorkbench({
);
const
hasAccessibleView
=
Boolean
(
activeView
);
const
configuredView
=
useMemo
(
()
=>
buildConfiguredWorkbenchView
(
records
,
filters
,
activeView
,
subject
),
[
records
,
filters
,
activeView
,
subject
],
()
=>
buildConfiguredWorkbenchView
(
records
,
{}
,
activeView
,
subject
),
[
records
,
activeView
,
subject
],
);
const
filteredRecords
=
configuredView
.
filteredRecords
||
[];
const
visibleColumns
=
useMemo
(()
=>
{
const
hiddenFields
=
new
Set
(
hiddenColumnFields
);
return
(
configuredView
.
columns
||
[]).
filter
((
column
)
=>
!
hiddenFields
.
has
(
column
.
field
));
},
[
configuredView
.
columns
,
hiddenColumnFields
]);
const
totalPages
=
Math
.
max
(
1
,
Math
.
ceil
(
filteredRecords
.
length
/
pageSize
));
const
totalPages
=
Math
.
max
(
1
,
Math
.
ceil
(
totalRecords
/
pageSize
));
const
safePage
=
Math
.
min
(
page
,
totalPages
-
1
);
const
pageRecords
=
filteredRecords
.
slice
(
safePage
*
pageSize
,
(
safePage
+
1
)
*
pageSize
,
);
const
displayRecords
=
loading
?
[]
:
pageRecords
;
const
displayRecords
=
loading
?
[]
:
filteredRecords
;
const
displaySelectedIds
=
loading
?
[]
:
selectedIds
;
const
displayPage
=
loading
?
0
:
safePage
;
...
...
@@ -145,26 +203,9 @@ export default function SubjectRecordWorkbench({
setSelectedIds
((
current
)
=>
current
.
filter
((
id
)
=>
visibleIds
.
has
(
id
)));
},
[
filteredRecords
]);
const
viewCounts
=
useMemo
(
()
=>
buildViewCountMap
(
records
,
filters
,
gridViews
,
subject
),
[
records
,
filters
,
gridViews
,
subject
],
);
const
filterSchema
=
useMemo
(()
=>
{
const
fields
=
[
{
key
:
'keyword'
,
label
:
'关键词搜索'
,
type
:
'text'
,
icon
:
'search'
,
placeholder
:
'请输入关键词'
,
},
];
const
yearOptions
=
collectProjectYears
(
records
);
if
(
yearOptions
.
length
>
0
)
{
fields
.
push
({
key
:
'year'
,
label
:
'项目年度'
,
type
:
'select'
,
options
:
yearOptions
});
}
return
fields
;
},
[
records
]);
return
buildFilterSchema
(
visibleColumns
);
},
[
visibleColumns
]);
const
feedbackHelpers
=
useMemo
(()
=>
({
reload
,
...
...
@@ -300,7 +341,7 @@ export default function SubjectRecordWorkbench({
<>
<
RecordWorkbench
summaryItems=
{
summaryItems
}
views=
{
gridViews
.
map
((
view
)
=>
({
...
view
,
count
:
viewCounts
[
view
.
name
]
||
0
}))
}
views=
{
gridViews
}
activeViewName=
{
activeView
?.
name
||
''
}
onViewChange=
{
handleViewChange
}
filters=
{
filters
}
...
...
@@ -351,7 +392,7 @@ export default function SubjectRecordWorkbench({
page
:
displayPage
,
pageSize
,
pageSizeOptions
:
PAGE_SIZE_OPTIONS
,
totalRecords
:
loading
?
0
:
filteredRecords
.
length
,
totalRecords
:
loading
?
0
:
totalRecords
,
totalPages
,
onPageChange
:
(
nextPage
)
=>
{
setPage
(
nextPage
);
...
...
src/components/record-workbench/index.jsx
View file @
936e5fa0
...
...
@@ -24,6 +24,8 @@ import {
}
from
'@/components/AppIcons'
;
import
FilterSelect
from
'@/components/FilterSelect'
;
import
FormDatePicker
from
'@/components/FormDatePicker'
;
import
ReferenceSearchSelect
from
'@/pages/RecordDetailDrawer/components/ReferenceSearchSelect'
;
import
UserSearchSelect
from
'@/components/UserSearchSelect'
;
import
{
formatFieldValue
,
getFieldValue
,
normalizeText
}
from
'@/utils/subjectView'
;
import
{
Alert
,
...
...
@@ -970,19 +972,35 @@ export default function RecordWorkbench({
placeholder={field.placeholder || `
请选择
$
{
field
.
label
}
`}
variant={filterFieldVariant}
/>
) : field.type === 'month' ? (
) : field.type === '
date' || field.type === 'datetime' || field.type === '
month' ? (
<FormDatePicker
aria-label={field.label}
className="w-full"
mode="month"
mode={field.type === 'month' ? 'month' : field.type === 'datetime' ? 'datetime' : 'date'}
type={field.type === 'datetime' ? 'datetime-local' : 'date'}
variant={filterFieldVariant}
value={filters[field.key] || ''}
onChange={(event) => onFiltersChange?.(field.key, event.target.value)}
/>
) : field.type === 'reference' ? (
<ReferenceSearchSelect
fieldDef={field.fieldDef}
placeholder={field.placeholder || `
筛选
$
{
field
.
label
}
`}
value={filters[field.key] || ''}
onChange={(value) => onFiltersChange?.(field.key, value)}
/>
) : field.type === 'user' ? (
<UserSearchSelect
ariaLabel={field.label}
multiple={Boolean(field.fieldDef?.multiple)}
placeholder={field.placeholder || `
筛选
$
{
field
.
label
}
`}
value={filters[field.key] || ''}
onChange={(value) => onFiltersChange?.(field.key, value)}
/>
) : (
<Input
className="w-full"
type=
"text"
type=
{field.type === 'number' ? 'number' : 'text'}
value={filters[field.key] || ''}
onChange={(event) => onFiltersChange?.(field.key, event.target.value)}
placeholder={field.placeholder}
...
...
src/pages/project_manager_xy/SubjectWorkbenchService.js
View file @
936e5fa0
...
...
@@ -5,18 +5,200 @@ import {
}
from
'@/services/DataService'
;
import
{
buildViewColumns
,
buildViewSearchColumns
,
checkViewCondition
,
formatFieldValue
,
getFieldValue
,
isViewAccessible
,
normalizeText
,
normalizeProjectYearDisplay
,
recordMatchesRestrictions
,
sortRecordsByView
,
toArray
,
}
from
'@/utils/subjectView'
;
const
QUERY_PAGE_SIZE
=
1000
;
const
DEFAULT_RECORD_SORT
=
'-updated_at -created_at'
;
const
RECORD_LEVEL_FIELDS
=
new
Set
([
'_id'
,
'name'
,
'title'
,
'status'
,
'subject'
,
'content'
,
'content_type'
,
'order'
,
'created_at'
,
'updated_at'
,
]);
function
normalizeFilterField
(
field
)
{
const
normalized
=
String
(
field
||
''
).
trim
();
if
(
!
normalized
||
RECORD_LEVEL_FIELDS
.
has
(
normalized
)
||
normalized
.
startsWith
(
'metadata.'
)
)
{
return
normalized
;
}
return
`metadata.
${
normalized
}
`
;
}
function
escapeRegex
(
value
)
{
return
String
(
value
??
''
).
replace
(
/
[
.*+?^${}()|[
\]\\]
/g
,
'
\\
$&'
);
}
function
conditionToMongoClause
(
rule
)
{
const
field
=
normalizeFilterField
(
rule
?.
field
);
if
(
!
field
||
rule
?.
value
===
undefined
)
{
return
null
;
}
const
comparator
=
rule
.
comparator
||
'$eq'
;
const
value
=
rule
.
value
;
if
(
comparator
===
'$contains'
)
{
const
values
=
Array
.
isArray
(
value
)
?
value
:
[
value
];
const
pattern
=
values
.
map
((
item
)
=>
escapeRegex
(
item
)).
filter
(
Boolean
).
join
(
'|'
);
return
pattern
?
{
[
field
]:
{
$regex
:
pattern
,
$options
:
'i'
}
}
:
null
;
}
if
([
'$in'
,
'$nin'
].
includes
(
comparator
))
{
return
{
[
field
]:
{
[
comparator
]:
Array
.
isArray
(
value
)
?
value
:
[
value
]
}
};
}
if
([
'$ne'
,
'$exists'
].
includes
(
comparator
))
{
return
{
[
field
]:
{
[
comparator
]:
value
}
};
}
return
{
[
field
]:
value
};
}
function
getFieldDefinitionForPath
(
field
,
subject
)
{
const
fieldName
=
String
(
field
||
''
).
replace
(
/^metadata
\.
/
,
''
);
if
(
fieldName
===
'title'
)
{
return
{
name
:
'title'
,
type
:
'text'
};
}
return
(
subject
?.
fields
||
[]).
find
((
item
)
=>
item
.
name
===
fieldName
)
||
null
;
}
function
dateFilterClause
(
field
,
value
)
{
const
dateText
=
String
(
value
||
''
).
slice
(
0
,
10
);
if
(
!
/^
\d{4}
-
\d{2}
-
\d{2}
$/
.
test
(
dateText
))
{
return
null
;
}
const
start
=
new
Date
(
`
${
dateText
}
T00:00:00.000Z`
);
if
(
Number
.
isNaN
(
start
.
getTime
()))
{
return
null
;
}
const
end
=
new
Date
(
start
);
end
.
setUTCDate
(
end
.
getUTCDate
()
+
1
);
return
{
[
field
]:
{
$gte
:
start
.
toISOString
(),
$lt
:
end
.
toISOString
(),
},
};
}
function
buildFieldFilterClause
(
field
,
value
,
fieldDef
)
{
if
(
value
===
undefined
||
value
===
null
||
value
===
''
)
{
return
null
;
}
const
fieldType
=
String
(
fieldDef
?.
type
||
'text'
).
toLowerCase
();
if
([
'date'
,
'datetime'
,
'date_time'
].
includes
(
fieldType
))
{
return
dateFilterClause
(
field
,
value
);
}
if
(
fieldType
===
'number'
)
{
const
numberValue
=
Number
(
value
);
return
Number
.
isFinite
(
numberValue
)
?
{
[
field
]:
numberValue
}
:
null
;
}
if
(
fieldType
===
'boolean'
)
{
if
(
value
===
true
||
value
===
false
)
return
{
[
field
]:
value
};
if
(
value
===
'true'
||
value
===
'false'
)
return
{
[
field
]:
value
===
'true'
};
return
null
;
}
const
values
=
(
Array
.
isArray
(
value
)
?
value
:
[
value
])
.
map
((
item
)
=>
typeof
item
===
'string'
?
item
.
trim
()
:
item
)
.
filter
((
item
)
=>
item
!==
''
&&
item
!==
null
&&
item
!==
undefined
);
if
(
values
.
length
===
0
)
return
null
;
if
([
'reference'
,
'document'
,
'view'
,
'template'
,
'task'
,
'tag'
,
'group'
,
'category'
,
'user'
].
includes
(
fieldType
))
{
return
fieldDef
?.
multiple
?
{
[
field
]:
{
$in
:
values
}
}
:
{
[
field
]:
values
[
0
]
};
}
if
(
fieldType
===
'select'
)
{
return
fieldDef
?.
multiple
?
{
[
field
]:
{
$in
:
values
}
}
:
{
[
field
]:
values
[
0
]
};
}
const
pattern
=
values
.
map
(
escapeRegex
).
filter
(
Boolean
).
join
(
'|'
);
return
pattern
?
{
[
field
]:
{
$regex
:
pattern
,
$options
:
'i'
}
}
:
null
;
}
function
viewSort
(
view
)
{
const
sortColumns
=
toArray
(
view
?.
settings
?.
columns
)
.
filter
((
column
)
=>
column
?.
sort
)
.
sort
((
left
,
right
)
=>
(
left
.
sortIndex
??
0
)
-
(
right
.
sortIndex
??
0
));
if
(
sortColumns
.
length
===
0
)
{
return
DEFAULT_RECORD_SORT
;
}
return
sortColumns
.
map
((
column
)
=>
`
${
column
.
sort
===
'desc'
?
'-'
:
''
}${
normalizeFilterField
(
column
.
field
)}
`
)
.
join
(
' '
);
}
function
buildRemoteFilter
(
view
,
filters
=
{},
baseFilter
=
{},
subject
)
{
const
clauses
=
[];
if
(
baseFilter
&&
Object
.
keys
(
baseFilter
).
length
>
0
)
{
clauses
.
push
(
baseFilter
);
}
toArray
(
view
?.
settings
?.
restrictions
)
.
filter
((
restriction
)
=>
restriction
?.
type
===
'field'
)
.
map
((
restriction
)
=>
conditionToMongoClause
({
...
restriction
,
comparator
:
restriction
.
comparator
||
'$in'
,
}))
.
filter
(
Boolean
)
.
forEach
((
clause
)
=>
clauses
.
push
(
clause
));
const
conditions
=
toArray
(
view
?.
settings
?.
filters
?.
conditions
)
.
map
(
conditionToMongoClause
)
.
filter
(
Boolean
);
if
(
conditions
.
length
>
0
)
{
const
relationship
=
view
?.
settings
?.
filters
?.
relationship
||
'all'
;
clauses
.
push
(
relationship
===
'any'
||
relationship
===
'or'
?
{
$or
:
conditions
}
:
{
$and
:
conditions
},
);
}
Object
.
entries
(
filters
||
{})
.
filter
(([
key
])
=>
key
.
startsWith
(
'field:'
))
.
map
(([
key
,
value
])
=>
buildFieldFilterClause
(
normalizeFilterField
(
key
.
slice
(
'field:'
.
length
)),
value
,
getFieldDefinitionForPath
(
key
.
slice
(
'field:'
.
length
),
subject
),
))
.
filter
(
Boolean
)
.
forEach
((
clause
)
=>
clauses
.
push
(
clause
));
if
(
clauses
.
length
===
0
)
{
return
undefined
;
}
if
(
clauses
.
length
===
1
)
{
return
clauses
[
0
];
}
return
{
$and
:
clauses
};
}
export
function
normalizeWorkbenchRecord
(
record
)
{
return
{
...
...
@@ -57,18 +239,66 @@ export async function queryAllSubjectRecords(subjectName, query = {}) {
return
records
;
}
export
async
function
loadSubjectWorkbenchResources
(
subjectName
,
query
=
{})
{
export
async
function
querySubjectWorkbenchRecords
(
subjectName
,
subject
,
view
,
filters
=
{},
query
=
{},
page
=
0
,
pageSize
=
20
,
)
{
const
response
=
await
queryRecords
({
...
query
,
subject
:
subjectName
,
page
,
page_size
:
pageSize
,
mode
:
query
.
mode
||
'full'
,
sort
:
query
.
sort
||
viewSort
(
view
),
filter
:
buildRemoteFilter
(
view
,
filters
,
query
.
filter
,
subject
),
});
const
records
=
response
?.
records
||
[];
const
totalCount
=
Number
(
response
?.
total_count
??
response
?.
total_records
??
response
?.
total
??
response
?.
count
??
records
.
length
,
);
return
{
records
,
totalCount
:
Number
.
isFinite
(
totalCount
)
?
totalCount
:
records
.
length
,
};
}
export
async
function
loadSubjectWorkbenchResources
(
subjectName
,
query
=
{},
{
viewName
=
''
,
filters
=
{},
page
=
0
,
pageSize
=
20
}
=
{},
)
{
const
[
subject
,
views
]
=
await
Promise
.
all
([
loadSubject
(
subjectName
),
getSubjectView
(
subjectName
),
]);
const
accessibleGridViews
=
buildGridViews
(
views
||
[]);
const
records
=
accessibleGridViews
.
length
>
0
?
await
queryAllSubjectRecords
(
subjectName
,
query
)
:
[];
const
activeView
=
accessibleGridViews
.
find
((
view
)
=>
view
.
name
===
viewName
)
||
accessibleGridViews
[
0
]
||
null
;
const
pageResult
=
activeView
?
await
querySubjectWorkbenchRecords
(
subjectName
,
subject
,
activeView
,
filters
,
query
,
page
,
pageSize
,
)
:
{
records
:
[],
totalCount
:
0
};
return
{
records
:
records
.
map
(
normalizeWorkbenchRecord
),
records
:
pageResult
.
records
.
map
(
normalizeWorkbenchRecord
),
totalCount
:
pageResult
.
totalCount
,
subject
,
views
:
views
||
[],
};
...
...
@@ -86,17 +316,12 @@ export function buildGridViews(views = []) {
return
isViewAccessible
(
fallbackView
)
?
[
fallbackView
]
:
[];
}
export
function
buildConfiguredWorkbenchView
(
records
,
filters
,
view
,
subject
)
{
export
function
buildConfiguredWorkbenchView
(
records
,
_
filters
,
view
,
subject
)
{
if
(
!
view
)
return
{
columns
:
[],
filteredRecords
:
[]
};
const
keyword
=
normalizeText
(
filters
?.
keyword
).
toLowerCase
();
const
status
=
normalizeText
(
filters
?.
status
);
const
year
=
normalizeText
(
filters
?.
year
);
const
columns
=
buildViewColumns
(
view
,
subject
);
const
searchColumns
=
buildViewSearchColumns
(
view
,
subject
);
const
filteredRecords
=
records
.
filter
((
record
)
=>
{
const
rawRecord
=
record
.
raw
||
record
;
const
metadata
=
rawRecord
?.
metadata
||
{};
if
(
!
recordMatchesRestrictions
(
rawRecord
,
view
?.
settings
?.
restrictions
))
{
return
false
;
...
...
@@ -114,24 +339,7 @@ export function buildConfiguredWorkbenchView(records, filters, view, subject) {
return
false
;
}
if
(
status
&&
normalizeText
(
metadata
.
project_status
)
!==
status
)
{
return
false
;
}
if
(
year
&&
normalizeText
(
metadata
.
project_year
)
!==
year
)
{
return
false
;
}
if
(
!
keyword
)
{
return
true
;
}
const
searchText
=
searchColumns
.
map
((
column
)
=>
formatFieldValue
(
getFieldValue
(
rawRecord
,
column
.
field
),
column
.
fieldDef
))
.
join
(
' '
)
.
toLowerCase
();
return
searchText
.
includes
(
keyword
);
});
return
{
...
...
src/pages/project_manager_xy/project-management.business-flow.md
View file @
936e5fa0
...
...
@@ -27,8 +27,8 @@
## 2. 项目列表与详情
1.
页面通过
`loadSubject`
、
`getSubjectView`
和
`POST /record/query`
读取字段、视图和记录。
2.
表格列、视图限制、排序、选择字段选项和字段分组来自服务端配置;字段分组读取当前 Grid View 的
`settings.groups`
,按配置展示分组表头并支持展开或收起。当前 Grid View 的
`settings.shortTitle === true`
时只隐藏通用
`title`
展示列;
`false`
或未配置时正常展示。
`title`
数据
仍参与关键词搜索、排序、详情和保存,详情入口保留在项目名称和固定操作列
。
切换 Grid View 时立即切换当前视图、清空跨视图选择并回到第一页,同时重新读取 Subject、视图配置和
`/record/query`
数据;
项目状态不再单独提供下拉筛选,按顶部 Grid View Tabs 切换
;刷新期间不展示上一次请求的记录行,表格在当前可视区域中心显示加载状态,失败时保留错误反馈并清空不可确认的旧数据。
2.
表格列、视图限制、排序、选择字段选项和字段分组来自服务端配置;字段分组读取当前 Grid View 的
`settings.groups`
,按配置展示分组表头并支持展开或收起。当前 Grid View 的
`settings.shortTitle === true`
时只隐藏通用
`title`
展示列;
`false`
或未配置时正常展示。
`title`
数据
参与排序、详情和保存,详情入口保留在项目名称和固定操作列。筛选清单根据当前 View 的字段配置和类型自动生成,文本类字段使用模糊匹配,选择/布尔字段使用枚举值,日期使用当天范围,数字使用精确值,引用/用户字段使用 ID;所有字段筛选与分页均通过
`POST /record/query`
远程执行,浏览器不再加载全部记录后本地过滤
。
切换 Grid View 时立即切换当前视图、清空跨视图选择并回到第一页,同时重新读取 Subject、视图配置和
`/record/query`
数据;
不再额外写死项目状态筛选,当前视图配置包含该字段时才自动生成对应的选择筛选,视图标签仍用于切换服务端配置的视图限制
;刷新期间不展示上一次请求的记录行,表格在当前可视区域中心显示加载状态,失败时保留错误反馈并清空不可确认的旧数据。
项目列表表格使用限定高度的内部滚动区域,支持纵向和横向滚动;表头在表格滚动区域内吸顶,分页位于滚动区域外,避免页面因记录行过多持续向下延伸。
View 中兼容历史字段
`metadata.project_finish_time`
时按数据定义显示为“项目完成时间”;
内部
`metadata.*`
路径不作为可见表头,字段取值路径和服务端配置保持不变。
...
...
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