Commit 06f0d7dd authored by David Yang's avatar David Yang

update: 筛选优化

parent 7b7c281e
......@@ -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` 接口
---
......
......@@ -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);
......
......@@ -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.keywordList || 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;
};
}, [activeCategory, deferredQuery, pageSize, tagFilter]);
}, [activeFilters, 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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment