feat: 添加热词组筛选和分页功能

- 在 `HotWords.tsx` 中添加热词组筛选选项和分页逻辑
- 更新 `hotwordGroup.ts` 和后端相关控制器及服务以支持新的筛选参数
- 优化前端热词组列表的展示和交互逻辑
dev_na
chenhao 2026-04-24 09:04:16 +08:00
parent 0b8014d1af
commit 99f5fd1cbd
5 changed files with 160 additions and 76 deletions

View File

@ -74,10 +74,11 @@ public class HotWordGroupController {
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String name,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long tenantId) {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Long targetTenantId = resolveTargetTenantId(loginUser, tenantId);
return ApiResponse.ok(hotWordGroupService.pageGroups(current, size, name, targetTenantId));
return ApiResponse.ok(hotWordGroupService.pageGroups(current, size, name, status, targetTenantId));
}
@Operation(summary = "查询热词组选项")

View File

@ -16,7 +16,7 @@ public interface HotWordGroupService extends IService<HotWordGroup> {
boolean removeGroupById(Long id, Long tenantId);
PageResult<List<HotWordGroupVO>> pageGroups(Integer current, Integer size, String name, Long tenantId);
PageResult<List<HotWordGroupVO>> pageGroups(Integer current, Integer size, String name, Integer status, Long tenantId);
List<HotWordGroupVO> listVisibleOptions(Long tenantId);
}

View File

@ -80,9 +80,10 @@ public class HotWordGroupServiceImpl extends ServiceImpl<HotWordGroupMapper, Hot
}
@Override
public PageResult<List<HotWordGroupVO>> pageGroups(Integer current, Integer size, String name, Long tenantId) {
public PageResult<List<HotWordGroupVO>> pageGroups(Integer current, Integer size, String name, Integer status, Long tenantId) {
LambdaQueryWrapper<HotWordGroup> wrapper = new LambdaQueryWrapper<HotWordGroup>()
.like(name != null && !name.isBlank(), HotWordGroup::getGroupName, name)
.eq(status != null, HotWordGroup::getStatus, status)
.orderByDesc(HotWordGroup::getCreatedAt);
wrapper.eq(tenantId != null, HotWordGroup::getTenantId, tenantId);
Page<HotWordGroup> page = this.page(new Page<>(current, size), wrapper);

View File

@ -24,6 +24,7 @@ export const getHotWordGroupPage = (params: {
current: number;
size: number;
name?: string;
status?: number;
tenantId?: number;
}) => {
return http.get<{ code: string; data: { records: HotWordGroupVO[]; total: number }; msg: string }>(

View File

@ -63,6 +63,8 @@ type HotWordGroupFormValues = {
remark?: string;
};
type GroupListItem = HotWordGroupVO | { id?: undefined; groupName: string; remark?: string; hotWordCount?: number; status?: number };
const HotWords: React.FC = () => {
const { message } = App.useApp();
const { t } = useTranslation();
@ -94,7 +96,14 @@ const HotWords: React.FC = () => {
const [groupLoading, setGroupLoading] = useState(false);
const [groupSubmitLoading, setGroupSubmitLoading] = useState(false);
const [groupData, setGroupData] = useState<HotWordGroupVO[]>([]);
const [groupTotal, setGroupTotal] = useState(0);
const [groupCurrent, setGroupCurrent] = useState(1);
const [groupSize, setGroupSize] = useState(8);
const [groupSearchInput, setGroupSearchInput] = useState("");
const [groupSearchName, setGroupSearchName] = useState("");
const [groupSearchStatus, setGroupSearchStatus] = useState<number | undefined>(undefined);
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
const [selectedGroupName, setSelectedGroupName] = useState<string | undefined>(undefined);
const [filterVisible, setFilterVisible] = useState(false);
@ -109,8 +118,11 @@ const HotWords: React.FC = () => {
useEffect(() => {
void loadGroupOptions();
}, [isPlatformAdmin, activeTenantId]);
useEffect(() => {
void loadGroupPage();
}, []);
}, [groupCurrent, groupSearchName, groupSearchStatus, groupSize, isPlatformAdmin, activeTenantId]);
const fetchData = async () => {
setLoading(true);
@ -140,18 +152,29 @@ const HotWords: React.FC = () => {
const loadGroupPage = async () => {
setGroupLoading(true);
try {
const res = await getHotWordGroupPage({ current: 1, size: 200 });
if (isPlatformAdmin) {
const scoped = await getHotWordGroupPage({ current: 1, size: 200, tenantId: activeTenantId });
setGroupData(scoped.data?.data?.records || []);
return;
}
const res = await getHotWordGroupPage({
current: groupCurrent,
size: groupSize,
name: groupSearchName || undefined,
status: groupSearchStatus,
tenantId: isPlatformAdmin ? activeTenantId : undefined,
});
setGroupData(res.data?.data?.records || []);
setGroupTotal(res.data?.data?.total || 0);
} finally {
setGroupLoading(false);
}
};
const reloadGroupList = async (resetToFirstPage = false) => {
await loadGroupOptions();
if (resetToFirstPage && groupCurrent !== 1) {
setGroupCurrent(1);
return;
}
await loadGroupPage();
};
const handleOpenModal = (record?: HotWordVO) => {
if (record) {
setEditingId(record.id);
@ -248,7 +271,7 @@ const HotWords: React.FC = () => {
message.success("热词组创建成功");
}
setGroupEditorVisible(false);
await Promise.all([loadGroupOptions(), loadGroupPage()]);
await reloadGroupList(true);
} finally {
setGroupSubmitLoading(false);
}
@ -260,10 +283,23 @@ const HotWords: React.FC = () => {
message.success("热词组删除成功");
if (searchGroupId === id) {
setSearchGroupId(undefined);
setSelectedGroupName(undefined);
}
await Promise.all([loadGroupOptions(), loadGroupPage(), fetchData()]);
await Promise.all([reloadGroupList(), fetchData()]);
};
const handleSelectGroup = (item: GroupListItem) => {
setSearchGroupId(item.id);
setSelectedGroupName(item.id ? item.groupName : undefined);
setCurrent(1);
};
const hotWordGroupTitle = searchGroupId
? selectedGroupName || groupData.find((item) => item.id === searchGroupId)?.groupName || groupNameMap[searchGroupId] || "热词列表"
: "全部热词";
const groupListData: GroupListItem[] = [{ id: undefined, groupName: "全部热词" }, ...groupData];
const columns = [
{
title: "热词原文",
@ -359,7 +395,7 @@ const HotWords: React.FC = () => {
className="shadow-sm"
title="热词组"
style={{ width: '25%', display: 'flex', flexDirection: 'column', minWidth: 280 }}
styles={{ body: { padding: 0, flex: 1, overflow: 'auto' } }}
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", minHeight: 0 } }}
extra={
<Button
type="primary"
@ -369,69 +405,114 @@ const HotWords: React.FC = () => {
/>
}
>
<List
loading={groupLoading}
dataSource={[{ id: undefined, groupName: '全部热词' } as any, ...groupData]}
renderItem={(item) => {
const isSelected = searchGroupId === item.id;
const actions = [];
if (item.id) {
actions.push(
<Button
type="text"
icon={<EditOutlined />}
onClick={(e) => openGroupEditor(item, e)}
size="small"
/>
);
actions.push(
<Popconfirm
title="确定删除这个热词组吗?"
description="删除前必须先解除模板引用并清空组内热词。"
onConfirm={(e) => handleDeleteGroup(item.id, e)}
onCancel={e => e?.stopPropagation()}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={e => e.stopPropagation()}
size="small"
/>
</Popconfirm>
);
}
<div style={{ padding: "16px 16px 12px", borderBottom: "1px solid var(--ant-color-border-secondary, #f0f0f0)" }}>
<Space direction="vertical" size={12} style={{ width: "100%" }}>
<Input.Search
placeholder="搜索热词组名称"
allowClear
value={groupSearchInput}
onChange={(e) => setGroupSearchInput(e.target.value)}
onSearch={(value) => {
setGroupSearchInput(value);
setGroupSearchName(value.trim());
setGroupCurrent(1);
}}
/>
<Select
placeholder="按状态筛选"
allowClear
value={groupSearchStatus}
style={{ width: "100%" }}
options={[
{ label: "启用", value: 1 },
{ label: "禁用", value: 0 },
]}
onChange={(value) => {
setGroupSearchStatus(value);
setGroupCurrent(1);
}}
/>
</Space>
</div>
<div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
<List
loading={groupLoading}
dataSource={groupListData}
renderItem={(item) => {
const isSelected = searchGroupId === item.id;
const actions = [];
if (item.id) {
actions.push(
<Button
key={`edit-${item.id}`}
type="text"
icon={<EditOutlined />}
onClick={(e) => openGroupEditor(item, e)}
size="small"
/>
);
actions.push(
<Popconfirm
key={`delete-${item.id}`}
title="确定删除这个热词组吗?"
description="删除前必须先解除模板引用并清空组内热词。"
onConfirm={(e) => handleDeleteGroup(item.id, e)}
onCancel={(e) => e?.stopPropagation()}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={(e) => e.stopPropagation()}
size="small"
/>
</Popconfirm>
);
}
return (
<List.Item
onClick={() => {
setSearchGroupId(item.id);
setCurrent(1);
}}
style={{
cursor: 'pointer',
padding: '12px 24px',
background: isSelected ? 'var(--ant-color-primary-bg, #e6f4ff)' : 'transparent',
borderBottom: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
transition: 'background 0.3s'
}}
actions={actions}
>
<List.Item.Meta
title={
<Text strong style={{ color: isSelected ? 'var(--ant-color-primary, #1677ff)' : 'inherit' }}>
{item.groupName}
</Text>
}
description={
item.id
? <><Tag color={item.hotWordCount >= 200 ? "red" : "processing"}>{item.hotWordCount}/200</Tag> {item.remark}</>
: '查看所有热词'
}
/>
</List.Item>
);
return (
<List.Item
onClick={() => handleSelectGroup(item)}
style={{
cursor: 'pointer',
padding: '12px 24px',
background: isSelected ? 'var(--ant-color-primary-bg, #e6f4ff)' : 'transparent',
borderBottom: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
transition: 'background 0.3s'
}}
actions={actions}
>
<List.Item.Meta
title={
<Text strong style={{ color: isSelected ? 'var(--ant-color-primary, #1677ff)' : 'inherit' }}>
{item.groupName}
</Text>
}
description={
item.id
? (
<>
<Tag color={item.hotWordCount >= 200 ? "red" : item.status === 1 ? "processing" : "default"}>
{item.hotWordCount}/200
</Tag>
{item.remark}
</>
)
: '查看所有热词'
}
/>
</List.Item>
);
}}
/>
</div>
<AppPagination
current={groupCurrent}
pageSize={groupSize}
total={groupTotal}
onChange={(page, pageSize) => {
setGroupCurrent(page);
setGroupSize(pageSize);
}}
/>
</Card>
@ -439,7 +520,7 @@ const HotWords: React.FC = () => {
{/* Right Panel: Hotwords */}
<Card
className="shadow-sm"
title={searchGroupId ? groupData.find(g => g.id === searchGroupId)?.groupName || '热词列表' : '全部热词'}
title={hotWordGroupTitle}
style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
>