Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
R
risk_case_library
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
risk_case_library
Commits
06f0d7dd
Commit
06f0d7dd
authored
Jun 08, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update: 筛选优化
parent
7b7c281e
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
320 additions
and
134 deletions
+320
-134
audit-material-center.business-flow.md
...project_manager_xy/audit-material-center.business-flow.md
+35
-19
materialService.js
src/pages/project_manager_xy/materialService.js
+51
-46
index.jsx
src/pages/project_manager_xy/resource-booking/index.jsx
+234
-69
No files found.
src/pages/project_manager_xy/audit-material-center.business-flow.md
View file @
06f0d7dd
...
...
@@ -7,7 +7,7 @@
-
导入线下 Excel 维护的案例数据
-
支持手动创建、编辑、删除案例
-
提供更直观的案例浏览体验
-
支持按
关键词、案例性质、案例来源、案例等级快速
检索典型案例
-
支持按
全文关键词、案例来源、案例类型、问题属性、分类标签、案例性质等多字段组合
检索典型案例
-
为培训、复盘、经验沉淀提供统一入口
当前阶段的核心原则是:
...
...
@@ -33,7 +33,7 @@
-
展示关键字词云与最近更新案例
-
提供对库内内容密度和结构完整度的快速判断
-
`案例管理`
-
搜索、
筛选、浏览案例卡片
-
多字段
筛选、浏览案例卡片
-
手动创建案例
-
Excel 批量导入案例
-
查看案例详情并编辑
...
...
@@ -101,7 +101,7 @@
1.
用户进入
`案例管理`
2.
点击
`Excel 批量导出`
3.
前端按当前
搜索词、案例性质与关键字
筛选条件查询全部案例
3.
前端按当前
全文搜索、案例性质、案例来源、案例类型、问题属性、分类标签等
筛选条件查询全部案例
4.
导出文件统一输出以下 11 列:
-
`标题`
-
`分类`
...
...
@@ -115,25 +115,28 @@
-
`案例等级`
-
`案例年份`
5.
浏览器直接下载
`.xlsx`
文件
6.
`metadata.keyword`
导出到
`关键字`
列时
恢复为逗号
拼接字符串,便于线下继续维护
6.
`metadata.keyword`
导出到
`关键字`
列时
统一使用英文逗号
`,`
拼接字符串,便于线下继续维护
### 4.4 浏览与检索案例
1.
用户进入
`案例管理`
2.
通过搜索框检索标题、案例类型、案例属性、案例详情、案例警示、关键字、案例来源、案例等级
3.
通过案例性质标签快速切换当前视图
4.
通过关键字输入框进一步缩小范围,前端同时兼容历史
`metadata.tag`
5.
页面顶部提供
`Segment`
视图切换,可在“卡片视图 / 表格视图”之间切换
6.
卡片视图用于浏览案例摘要,点击卡片后以弹框方式查看并编辑详情
7.
表格视图展示标题、分类、标签、案例类型、关键字、来源、等级、年份、更新时间等字段
8.
表格视图按页加载案例,支持选择每页 20、50、100 条,并显示页码、当前页范围和匹配总数
9.
表格视图支持单选、全选当前页案例,并显示当前勾选数量
2.
通过全文搜索框检索标题、案例类型、案例属性、案例详情、案例警示、关键字、案例来源、案例等级
3.
通过案例来源、案例类型、问题属性、分类标签输入框填写组合筛选条件,输入框仅录入文本条件,不提供下拉候选
4.
点击
`查询`
后,前端将当前输入条件提交到
`POST /record/query`
做远程查询;仅输入文字不触发请求
5.
通过案例性质标签快速切换当前视图
6.
分类标签筛选优先匹配
`metadata.tag`
,同时兼容多值
`metadata.keyword`
7.
页面不对当前页案例做本地二次搜索
8.
页面顶部提供
`Segment`
视图切换,可在“卡片视图 / 表格视图”之间切换
9.
卡片视图用于浏览案例摘要,点击卡片后以弹框方式查看并编辑详情
10.
表格视图展示标题、分类、标签、案例类型、关键字、来源、等级、年份、更新时间等字段
11.
表格视图按页加载案例,支持选择每页 20、50、100 条,并显示页码、当前页范围和匹配总数
12.
表格视图支持单选、全选当前页案例,并显示当前勾选数量
### 4.5 编辑案例
1.
用户在列表中打开某个案例详情
2.
修改标题、分类、标签、案例类型、案例属性、案例详情、案例警示、关键字、案例来源、案例等级、案例年份
3.
关键字编辑区
支持逗号输入,并实时预览为关键字标签;保存时拆分为
`metadata.keyword`
字符串数组
3.
关键字编辑区
的输入框仅用于新增关键字,已有关键字以标签展示并支持逐个删除;保存时写入
`metadata.keyword`
字符串数组
4.
前端调用
`PUT /record/:name`
5.
保存成功后刷新列表与统计数据
...
...
@@ -181,15 +184,22 @@
-
空行直接跳过
-
`标题`
为空时,优先使用
`案例类型`
,再回退为
`案例详情`
摘要自动生成
`title`
-
普通文本字段统一按字符串保存
-
`关键字`
是多值字段,导入时
将逗号拼接文本拆分为字符串数组保存
-
`关键字`
是多值字段,导入时
识别英文逗号
`,`
与中文逗号
`,`
,拆分、去空、去重后保存为字符串数组
-
`案例年份`
优先按数字写入,不可解析时保留原字符串
-
导出模板固定为 11 列标准表头,便于线下统一维护
### 5.3 筛选规则
-
案例性质筛选使用
`metadata.category`
-
关键字筛选优先使用多值数组
`metadata.keyword`
,同时兼容历史字符串型
`metadata.keyword`
与
`metadata.tag`
-
案例来源筛选使用
`metadata.case_source`
,支持模糊匹配
-
案例类型筛选使用
`metadata.case_type`
,支持模糊匹配
-
问题属性筛选使用
`metadata.case_attribute`
,支持模糊匹配
-
分类标签筛选优先匹配
`metadata.tag`
,同时兼容多值数组或历史字符串型
`metadata.keyword`
-
关键字检索优先使用多值数组
`metadata.keyword`
,同时兼容历史字符串型
`metadata.keyword`
与
`metadata.tag`
-
关键词检索同时覆盖
`title`
、
`metadata.label`
、
`metadata.case_type`
、
`metadata.case_attribute`
、
`metadata.case_details`
、
`metadata.case_warning`
、
`metadata.keyword`
、
`metadata.case_source`
、
`metadata.case_level`
、
`metadata.category`
、
`metadata.tag`
-
筛选统一走远程
`POST /record/query`
;页面不使用本地数据搜索替代远程结果
-
筛选条件输入后必须点击
`查询`
才发起请求,输入过程不自动请求
-
筛选输入使用普通文本输入框,不使用
`select`
、
`datalist`
或其它下拉候选控件
---
...
...
@@ -223,9 +233,14 @@
建议至少包含:
-
搜索框
-
全文
搜索框
-
案例性质切换
-
关键字筛选
-
案例来源筛选
-
案例类型筛选
-
问题属性筛选
-
分类标签筛选
-
查询按钮
-
筛选项为普通文本输入框,不提供下拉框
-
`Segment`
视图切换(卡片 / 表格)
-
手动新建按钮
-
Excel 导入按钮
...
...
@@ -251,7 +266,7 @@
-
案例详情全文
-
案例性质
-
关键字
-
关键字以标签形式展示;编辑时
保留逗号输入并即时预览拆分后的标签
-
关键字以标签形式展示;编辑时
输入框仅用于新增关键字,已有标签支持逐个删除
-
案例警示
-
案例来源 / 案例等级 / 案例年份
-
案例详情字数
...
...
@@ -286,7 +301,8 @@
-
首屏只加载第一页,默认每页 20 条
-
表格与卡片列表通过页码分页加载,不使用下滑自动加载
-
用户可选择每页 20、50、100 条;切换每页数量时从第一页重新加载
-
当用户切换分类或搜索条件时,从第一页重新加载
-
当用户点击
`查询`
、重置筛选或切换案例性质时,从第一页重新加载
-
搜索与筛选输入变化时不自动请求;点击
`查询`
后必须发起远程查询,不在当前页结果中做本地搜索
-
总数优先读取
`POST /record/query`
返回的
`total_records`
;接口未返回总数时再回退到
`count`
接口
---
...
...
src/pages/project_manager_xy/materialService.js
View file @
06f0d7dd
...
...
@@ -281,7 +281,7 @@ function createSummary(content = '', maxLength = 140) {
export
function
normalizeKeywordList
(
value
)
{
const
sourceValues
=
(
Array
.
isArray
(
value
)
?
value
:
[
value
]).
flatMap
((
item
)
=>
String
(
item
||
''
).
split
(
/
[
,,
]
/
),
String
(
item
||
''
).
split
(
/
[
,,
;;
\n
]
/
),
);
const
seenKeywords
=
new
Set
();
...
...
@@ -300,7 +300,7 @@ export function normalizeKeywordList(value) {
}
export
function
formatKeywordList
(
value
)
{
return
normalizeKeywordList
(
value
).
join
(
',
'
);
return
normalizeKeywordList
(
value
).
join
(
','
);
}
export
function
formatDateTime
(
value
)
{
...
...
@@ -365,6 +365,20 @@ function buildKeywordFilter(keyword = '') {
};
}
function
buildMetadataTextFilter
(
fieldName
,
value
=
''
)
{
const
normalizedValue
=
String
(
value
||
''
).
trim
();
if
(
!
normalizedValue
)
{
return
null
;
}
return
{
[
`metadata.
${
fieldName
}
`
]:
{
$regex
:
escapeRegExp
(
normalizedValue
),
$options
:
'i'
,
},
};
}
function
buildRecordFilter
(
filters
=
{})
{
const
clauses
=
[];
const
keywordFilter
=
buildKeywordFilter
(
filters
.
keyword
);
...
...
@@ -380,14 +394,28 @@ function buildRecordFilter(filters = {}) {
}
if
(
filters
.
tag
)
{
clauses
.
push
({
'metadata.tag'
:
{
const
tagRegex
=
{
$regex
:
escapeRegExp
(
String
(
filters
.
tag
).
trim
()),
$options
:
'i'
,
},
};
clauses
.
push
({
$or
:
[{
'metadata.tag'
:
tagRegex
},
{
'metadata.keyword'
:
tagRegex
}],
});
}
[
[
'case_source'
,
filters
.
caseSource
],
[
'case_type'
,
filters
.
caseType
],
[
'case_attribute'
,
filters
.
caseAttribute
],
].
forEach
(([
fieldName
,
value
])
=>
{
const
fieldFilter
=
buildMetadataTextFilter
(
fieldName
,
value
);
if
(
fieldFilter
)
{
clauses
.
push
(
fieldFilter
);
}
});
if
(
clauses
.
length
===
0
)
{
return
{};
}
...
...
@@ -405,7 +433,10 @@ function hasActiveFilters(filters = {}) {
return
Boolean
(
String
(
filters
.
keyword
||
''
).
trim
()
||
String
(
filters
.
category
||
''
).
trim
()
||
String
(
filters
.
tag
||
''
).
trim
(),
String
(
filters
.
tag
||
''
).
trim
()
||
String
(
filters
.
caseSource
||
''
).
trim
()
||
String
(
filters
.
caseType
||
''
).
trim
()
||
String
(
filters
.
caseAttribute
||
''
).
trim
(),
);
}
...
...
@@ -461,36 +492,6 @@ export function normalizeCaseRecord(record = {}) {
};
}
function
applyClientFilters
(
cases
,
filters
=
{})
{
const
keyword
=
String
(
filters
.
keyword
||
''
).
trim
().
toLowerCase
();
const
category
=
String
(
filters
.
category
||
''
).
trim
();
const
tag
=
String
(
filters
.
tag
||
''
).
trim
().
toLowerCase
();
return
cases
.
filter
((
item
)
=>
{
const
matchesKeyword
=
!
keyword
||
[
item
.
label
,
item
.
caseType
,
item
.
caseAttribute
,
item
.
caseDetails
,
item
.
caseWarning
,
item
.
keyword
,
item
.
caseSource
,
item
.
caseLevel
,
item
.
category
,
item
.
tag
,
]
.
filter
(
Boolean
)
.
some
((
value
)
=>
String
(
value
).
toLowerCase
().
includes
(
keyword
));
const
matchesCategory
=
!
category
||
item
.
category
===
category
;
const
matchesTag
=
!
tag
||
String
(
item
.
tag
||
''
).
toLowerCase
().
includes
(
tag
);
return
matchesKeyword
&&
matchesCategory
&&
matchesTag
;
});
}
export
function
buildCaseDashboard
(
cases
=
[],
options
=
{})
{
const
totalCount
=
Number
.
isFinite
(
options
.
totalCount
)
?
options
.
totalCount
...
...
@@ -572,8 +573,6 @@ export function buildCaseDashboard(cases = [], options = {}) {
caseSourceGroups
,
recentCases
,
attentionCases
,
categorySuggestions
:
categoryGroups
.
map
((
item
)
=>
item
.
label
),
tagSuggestions
:
tagGroups
.
map
((
item
)
=>
item
.
label
),
};
}
...
...
@@ -620,10 +619,7 @@ export async function loadCasePage(filters = {}) {
page
===
0
&&
!
hasActiveFilters
(
filters
)
&&
total
===
0
&&
cases
.
length
===
0
;
if
(
shouldUseDemo
)
{
const
demoCases
=
applyClientFilters
(
DEMO_CASES
.
map
((
item
)
=>
normalizeCaseRecord
(
item
)),
filters
,
);
const
demoCases
=
DEMO_CASES
.
map
((
item
)
=>
normalizeCaseRecord
(
item
));
const
start
=
page
*
pageSize
;
const
pageCases
=
demoCases
.
slice
(
start
,
start
+
pageSize
);
const
demoTotal
=
demoCases
.
length
;
...
...
@@ -662,10 +658,19 @@ export async function loadCasePage(filters = {}) {
total
=
0
;
}
const
demoCases
=
applyClientFilters
(
DEMO_CASES
.
map
((
item
)
=>
normalizeCaseRecord
(
item
)),
filters
,
);
if
(
hasActiveFilters
(
filters
))
{
return
{
cases
:
[],
total
:
0
,
page
,
pageSize
,
hasMore
:
false
,
isDemoMode
:
false
,
error
:
error
?.
message
||
'远程案例查询失败,请稍后重试。'
,
};
}
const
demoCases
=
DEMO_CASES
.
map
((
item
)
=>
normalizeCaseRecord
(
item
));
const
start
=
page
*
pageSize
;
const
pageCases
=
demoCases
.
slice
(
start
,
start
+
pageSize
);
...
...
src/pages/project_manager_xy/resource-booking/index.jsx
View file @
06f0d7dd
...
...
@@ -37,11 +37,11 @@ import {
Squares2X2Icon
,
TableCellsIcon
,
TrashIcon
,
XMarkIcon
,
}
from
'@heroicons/react/24/outline'
;
import
clsx
from
'clsx'
;
import
{
startTransition
,
useDeferredValue
,
useEffect
,
useMemo
,
useRef
,
...
...
@@ -180,6 +180,93 @@ function KeywordChips({ value, emptyText = '暂无关键字', limit = 0 }) {
);
}
function
KeywordEditor
({
value
,
onChange
,
placeholder
=
'输入关键字后按 Enter 新增'
,
emptyText
=
'暂无关键字'
,
})
{
const
[
draft
,
setDraft
]
=
useState
(
''
);
const
keywords
=
normalizeKeywordList
(
value
);
function
commitKeyword
(
nextDraft
=
draft
)
{
const
nextKeywords
=
normalizeKeywordList
([...
keywords
,
nextDraft
]);
onChange
(
nextKeywords
);
setDraft
(
''
);
}
function
removeKeyword
(
keyword
)
{
onChange
(
keywords
.
filter
((
item
)
=>
item
!==
keyword
));
}
function
handleInputChange
(
event
)
{
const
nextValue
=
event
.
target
.
value
;
if
(
/
[
,,;;
\n]
/
.
test
(
nextValue
))
{
commitKeyword
(
nextValue
);
return
;
}
setDraft
(
nextValue
);
}
function
handleInputKeyDown
(
event
)
{
if
(
event
.
key
!==
'Enter'
&&
event
.
key
!==
'Tab'
)
{
return
;
}
const
normalizedDraft
=
String
(
draft
||
''
).
trim
();
if
(
!
normalizedDraft
)
{
return
;
}
event
.
preventDefault
();
commitKeyword
(
normalizedDraft
);
}
return
(
<
div
className=
"rounded-[22px] border border-zinc-200 bg-white p-3 shadow-sm"
>
<
Input
value=
{
draft
}
onChange=
{
handleInputChange
}
onKeyDown=
{
handleInputKeyDown
}
onBlur=
{
()
=>
{
if
(
String
(
draft
||
''
).
trim
())
{
commitKeyword
();
}
}
}
placeholder=
{
placeholder
}
/>
<
div
className=
"mt-3 min-h-8"
>
{
keywords
.
length
>
0
?
(
<
div
className=
"flex flex-wrap gap-2"
>
{
keywords
.
map
((
keyword
)
=>
(
<
span
key=
{
keyword
}
className=
"inline-flex max-w-full items-center gap-1.5 rounded-full border border-[color:color-mix(in_srgb,var(--pm-brand)_18%,white)] bg-[color:color-mix(in_srgb,var(--pm-brand)_7%,white)] px-2.5 py-1 text-xs font-medium text-[var(--pm-brand-strong)] shadow-sm"
title=
{
keyword
}
>
<
span
className=
"truncate"
>
{
keyword
}
</
span
>
<
button
type=
"button"
onClick=
{
()
=>
removeKeyword
(
keyword
)
}
className=
"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-[var(--pm-brand)] transition hover:bg-[color:color-mix(in_srgb,var(--pm-brand)_12%,white)]"
aria
-
label=
{
`删除关键字 ${keyword}`
}
>
<
XMarkIcon
className=
"size-3"
/>
</
button
>
</
span
>
))
}
</
div
>
)
:
(
<
span
className=
"text-sm text-zinc-400"
>
{
emptyText
}
</
span
>
)
}
</
div
>
</
div
>
);
}
function
CategoryTab
({
active
,
label
,
count
,
onClick
})
{
return
(
<
button
...
...
@@ -265,7 +352,7 @@ function buildEmptyCaseForm() {
caseAttribute
:
''
,
caseDetails
:
''
,
caseWarning
:
''
,
keyword
:
''
,
keyword
:
[]
,
caseSource
:
''
,
caseLevel
:
''
,
caseYear
:
''
,
...
...
@@ -285,7 +372,7 @@ function buildCaseForm(caseItem) {
caseAttribute
:
caseItem
.
caseAttribute
||
''
,
caseDetails
:
caseItem
.
caseDetails
||
''
,
caseWarning
:
caseItem
.
caseWarning
||
''
,
keyword
:
caseItem
.
keyword
||
''
,
keyword
:
caseItem
.
keyword
List
||
caseItem
.
keyword
||
[]
,
caseSource
:
caseItem
.
caseSource
||
''
,
caseLevel
:
caseItem
.
caseLevel
||
''
,
caseYear
:
caseItem
.
caseYear
||
''
,
...
...
@@ -298,7 +385,7 @@ function buildEmptyBatchEditForm() {
tag
:
''
,
caseType
:
''
,
caseAttribute
:
''
,
keyword
:
''
,
keyword
:
[]
,
caseSource
:
''
,
caseLevel
:
''
,
caseYear
:
''
,
...
...
@@ -307,11 +394,25 @@ function buildEmptyBatchEditForm() {
}
function
hasBatchEditValues
(
values
=
{})
{
return
Object
.
values
(
values
).
some
((
value
)
=>
String
(
value
??
''
).
trim
()
!==
''
);
return
Object
.
values
(
values
).
some
((
value
)
=>
Array
.
isArray
(
value
)
?
normalizeKeywordList
(
value
).
length
>
0
:
String
(
value
??
''
).
trim
()
!==
''
,
);
}
function
buildBatchEditPayload
(
values
=
{})
{
return
Object
.
entries
(
values
).
reduce
((
accumulator
,
[
key
,
value
])
=>
{
if
(
Array
.
isArray
(
value
))
{
const
normalizedList
=
normalizeKeywordList
(
value
);
if
(
normalizedList
.
length
>
0
)
{
accumulator
[
key
]
=
normalizedList
;
}
return
accumulator
;
}
const
normalizedValue
=
String
(
value
??
''
).
trim
();
if
(
normalizedValue
!==
''
)
{
...
...
@@ -329,8 +430,17 @@ function buildImportPreviewRows(preview) {
export
default
function
ResourceBookingPage
()
{
const
importFileInputRef
=
useRef
(
null
);
const
[
query
,
setQuery
]
=
useState
(
''
);
const
deferredQuery
=
useDeferredValue
(
query
);
const
[
tagFilter
,
setTagFilter
]
=
useState
(
''
);
const
[
caseSourceFilter
,
setCaseSourceFilter
]
=
useState
(
''
);
const
[
caseTypeFilter
,
setCaseTypeFilter
]
=
useState
(
''
);
const
[
caseAttributeFilter
,
setCaseAttributeFilter
]
=
useState
(
''
);
const
[
appliedFilters
,
setAppliedFilters
]
=
useState
({
keyword
:
''
,
tag
:
''
,
caseSource
:
''
,
caseType
:
''
,
caseAttribute
:
''
,
});
const
[
activeCategory
,
setActiveCategory
]
=
useState
(
ALL_CATEGORY_KEY
);
const
[
viewMode
,
setViewMode
]
=
useState
(
CARD_VIEW_MODE
);
const
[
cases
,
setCases
]
=
useState
([]);
...
...
@@ -371,7 +481,7 @@ export default function ResourceBookingPage() {
},
[
activeCaseId
,
cases
]);
const
categoryTabs
=
useMemo
(()
=>
{
return
[
const
tabs
=
[
{
key
:
ALL_CATEGORY_KEY
,
label
:
'全部案例'
,
...
...
@@ -383,15 +493,32 @@ export default function ResourceBookingPage() {
count
:
item
.
count
,
})),
];
},
[
dashboard
.
categoryGroups
,
total
]);
if
(
activeCategory
!==
ALL_CATEGORY_KEY
&&
!
tabs
.
some
((
item
)
=>
item
.
key
===
activeCategory
)
)
{
tabs
.
push
({
key
:
activeCategory
,
label
:
activeCategory
,
count
:
total
,
});
}
return
tabs
;
},
[
activeCategory
,
dashboard
.
categoryGroups
,
total
]);
const
visibleCases
=
useMemo
(()
=>
{
if
(
activeCategory
===
ALL_CATEGORY_KEY
)
{
return
cases
;
}
},
[
cases
]);
return
cases
.
filter
((
item
)
=>
item
.
category
===
activeCategory
);
},
[
activeCategory
,
cases
]);
const
activeFilters
=
useMemo
(
()
=>
({
...
appliedFilters
,
category
:
activeCategory
===
ALL_CATEGORY_KEY
?
''
:
activeCategory
,
}),
[
activeCategory
,
appliedFilters
],
);
const
selectedCaseIdSet
=
useMemo
(
()
=>
new
Set
(
selectedCaseIds
),
...
...
@@ -442,14 +569,43 @@ export default function ResourceBookingPage() {
);
},
[
visibleCases
]);
function
buildAppliedFilterValues
()
{
return
{
keyword
:
String
(
query
||
''
).
trim
(),
tag
:
String
(
tagFilter
||
''
).
trim
(),
caseSource
:
String
(
caseSourceFilter
||
''
).
trim
(),
caseType
:
String
(
caseTypeFilter
||
''
).
trim
(),
caseAttribute
:
String
(
caseAttributeFilter
||
''
).
trim
(),
};
}
function
handleApplyFilters
(
event
)
{
event
?.
preventDefault
();
setAppliedFilters
(
buildAppliedFilterValues
());
}
function
handleResetFilters
()
{
setQuery
(
''
);
setTagFilter
(
''
);
setCaseSourceFilter
(
''
);
setCaseTypeFilter
(
''
);
setCaseAttributeFilter
(
''
);
setAppliedFilters
({
keyword
:
''
,
tag
:
''
,
caseSource
:
''
,
caseType
:
''
,
caseAttribute
:
''
,
});
setActiveCategory
(
ALL_CATEGORY_KEY
);
}
async
function
loadCaseList
(
nextPage
=
0
,
nextPageSize
=
pageSize
)
{
setLoading
(
true
);
setError
(
''
);
const
result
=
await
loadCasePage
({
keyword
:
deferredQuery
,
category
:
activeCategory
===
ALL_CATEGORY_KEY
?
''
:
activeCategory
,
tag
:
tagFilter
,
...
activeFilters
,
page
:
nextPage
,
pageSize
:
nextPageSize
,
});
...
...
@@ -487,9 +643,7 @@ export default function ResourceBookingPage() {
setError
(
''
);
const
result
=
await
loadCasePage
({
keyword
:
deferredQuery
,
category
:
activeCategory
===
ALL_CATEGORY_KEY
?
''
:
activeCategory
,
tag
:
tagFilter
,
...
activeFilters
,
page
:
0
,
pageSize
,
});
...
...
@@ -526,7 +680,7 @@ export default function ResourceBookingPage() {
return
()
=>
{
cancelled
=
true
;
};
},
[
active
Category
,
deferredQuery
,
pageSize
,
tagFilter
]);
},
[
active
Filters
,
pageSize
]);
async
function
refreshCurrentList
(
preferredPage
=
page
)
{
const
totalPages
=
Math
.
max
(
1
,
Math
.
ceil
(
total
/
pageSize
));
...
...
@@ -849,9 +1003,7 @@ export default function ResourceBookingPage() {
try {
const result = await exportCaseWorkbook({
keyword: deferredQuery,
category: activeCategory === ALL_CATEGORY_KEY ? '' : activeCategory,
tag: tagFilter,
...activeFilters,
});
setNotice(`
已导出
$
{
result
.
count
}
条案例到
$
{
result
.
fileName
}
。
`);
} catch (exportError) {
...
...
@@ -957,11 +1109,7 @@ export default function ResourceBookingPage() {
<Button
plain
className="w-full justify-center whitespace-nowrap text-zinc-600 cursor-pointer"
onClick={() => {
setQuery('');
setTagFilter('');
setActiveCategory(ALL_CATEGORY_KEY);
}}
onClick={handleResetFilters}
>
<ArrowPathIcon className="size-4" />
重置当前筛选
...
...
@@ -993,9 +1141,12 @@ export default function ResourceBookingPage() {
</Panel>
<Panel className="p-5 sm:p-6">
<div className="grid gap-4 xl:grid-cols-[1.4fr_0.8fr]">
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">搜索案例</div>
<form
className="grid gap-4 lg:grid-cols-2 xl:grid-cols-[1.15fr_repeat(4,minmax(0,1fr))_auto]"
onSubmit={handleApplyFilters}
>
<div className="lg:col-span-2 xl:col-span-1">
<div className="mb-2 text-sm font-medium text-zinc-600">全文搜索</div>
<InputGroup>
<MagnifyingGlassIcon data-slot="icon" />
<Input
...
...
@@ -1007,15 +1158,53 @@ export default function ResourceBookingPage() {
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">标签筛选</div>
<div className="mb-2 text-sm font-medium text-zinc-600">案例来源</div>
<Input
value={caseSourceFilter}
onChange={(event) => setCaseSourceFilter(event.target.value)}
placeholder="例如:监管通报"
/>
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">案例类型</div>
<Input
value={caseTypeFilter}
onChange={(event) => setCaseTypeFilter(event.target.value)}
placeholder="例如:审批绕行"
/>
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">问题属性</div>
<Input
value={caseAttributeFilter}
onChange={(event) => setCaseAttributeFilter(event.target.value)}
placeholder="例如:流程绕行"
/>
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">分类标签</div>
<Input
list="case-tag-suggestions"
value={tagFilter}
onChange={(event) => setTagFilter(event.target.value)}
placeholder="例如:采购审批"
/>
</div>
<div className="flex items-end">
<Button
color="blue"
type="submit"
className="w-full cursor-pointer whitespace-nowrap"
disabled={loading}
>
<MagnifyingGlassIcon className="size-4" />
查询
</Button>
</div>
</form>
<div className="mt-6 flex flex-wrap gap-2">
{categoryTabs.map((item) => (
...
...
@@ -1360,17 +1549,6 @@ export default function ResourceBookingPage() {
</div>
)}
<datalist id="case-tag-suggestions">
{(dashboard.tagSuggestions || []).map((item) => (
<option key={item} value={item} />
))}
</datalist>
<datalist id="case-category-suggestions">
{(dashboard.categorySuggestions || []).map((item) => (
<option key={item} value={item} />
))}
</datalist>
<Dialog open={isCreateOpen} onClose={setIsCreateOpen} size="4xl">
<DialogTitle>手动新建案例</DialogTitle>
<DialogBody>
...
...
@@ -1391,7 +1569,6 @@ export default function ResourceBookingPage() {
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">分类</div>
<Input
list="case-category-suggestions"
value={createForm.category}
onChange={(event) =>
setCreateForm((prev) => ({
...
...
@@ -1405,7 +1582,6 @@ export default function ResourceBookingPage() {
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">标签</div>
<Input
list="case-tag-suggestions"
value={createForm.tag}
onChange={(event) =>
setCreateForm((prev) => ({
...
...
@@ -1444,15 +1620,15 @@ export default function ResourceBookingPage() {
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">关键字</div>
<
Input
<
KeywordEditor
value={createForm.keyword}
onChange={(
event
) =>
onChange={(
nextKeywords
) =>
setCreateForm((prev) => ({
...prev,
keyword:
event.target.value
,
keyword:
nextKeywords
,
}))
}
placeholder="
例如:采购审批, 拆单
"
placeholder="
输入关键字后按 Enter 新增
"
/>
</div>
<div>
...
...
@@ -1640,7 +1816,6 @@ export default function ResourceBookingPage() {
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">分类</div>
<Input
list="case-category-suggestions"
value={detailForm.category}
onChange={(event) =>
setDetailForm((prev) => ({
...
...
@@ -1653,7 +1828,6 @@ export default function ResourceBookingPage() {
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">标签</div>
<Input
list="case-tag-suggestions"
value={detailForm.tag}
onChange={(event) =>
setDetailForm((prev) => ({
...
...
@@ -1689,25 +1863,17 @@ export default function ResourceBookingPage() {
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">关键字</div>
<div className="rounded-[22px] border border-zinc-200 bg-white p-3 shadow-sm">
<Input
<KeywordEditor
value={detailForm.keyword}
onChange={(event
) =>
onChange={(nextKeywords
) =>
setDetailForm((prev) => ({
...prev,
keyword: event.target.value
,
keyword: nextKeywords
,
}))
}
placeholder="例如:采购审批, 拆单"
/>
<div className="mt-3 min-h-8">
<KeywordChips
value={detailForm.keyword}
emptyText="暂无关键字"
placeholder="输入关键字后按 Enter 新增"
/>
</div>
</div>
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">案例来源</div>
<Input
...
...
@@ -1819,7 +1985,6 @@ export default function ResourceBookingPage() {
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">分类</div>
<Input
list="case-category-suggestions"
value={batchEditForm.category}
onChange={(event) =>
setBatchEditForm((prev) => ({
...
...
@@ -1833,7 +1998,6 @@ export default function ResourceBookingPage() {
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">标签</div>
<Input
list="case-tag-suggestions"
value={batchEditForm.tag}
onChange={(event) =>
setBatchEditForm((prev) => ({
...
...
@@ -1872,15 +2036,16 @@ export default function ResourceBookingPage() {
</div>
<div>
<div className="mb-2 text-sm font-medium text-zinc-600">关键字</div>
<
Input
<
KeywordEditor
value={batchEditForm.keyword}
onChange={(
event
) =>
onChange={(
nextKeywords
) =>
setBatchEditForm((prev) => ({
...prev,
keyword:
event.target.value
,
keyword:
nextKeywords
,
}))
}
placeholder="填写后将批量覆盖关键字"
placeholder="输入后按 Enter,填写后将批量覆盖关键字"
emptyText="不覆盖关键字"
/>
</div>
<div>
...
...
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