feat: 新增热词组分组筛选编辑
parent
85ed5e37fd
commit
093438e73a
|
|
@ -2,6 +2,7 @@ package com.imeeting.controller.biz;
|
|||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.imeeting.dto.biz.HotWordBatchGroupDTO;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
import com.imeeting.entity.biz.HotWord;
|
||||
|
|
@ -64,6 +65,16 @@ public class HotWordController {
|
|||
return ApiResponse.ok(hotWordService.updateHotWord(hotWordDTO, loginUser.getUserId(), existing.getTenantId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "批量修改热词分组")
|
||||
@PutMapping("/group/batch")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
@Log(value = "批量修改热词分组", type = "热词管理")
|
||||
public ApiResponse<Integer> updateGroupBatch(@RequestBody HotWordBatchGroupDTO dto) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = resolveTargetTenantId(loginUser, dto.getTenantId());
|
||||
return ApiResponse.ok(hotWordService.updateHotWordGroupBatch(dto.getIds(), dto.getHotWordGroupId(), targetTenantId));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除热词")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
@ -85,6 +96,7 @@ public class HotWordController {
|
|||
@RequestParam(required = false) String word,
|
||||
@RequestParam(required = false) String category,
|
||||
@RequestParam(required = false) Long hotWordGroupId,
|
||||
@RequestParam(required = false) Boolean ungrouped,
|
||||
@RequestParam(required = false) Long tenantId) {
|
||||
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
Long targetTenantId = Boolean.TRUE.equals(loginUser.getIsPlatformAdmin()) ? tenantId : null;
|
||||
|
|
@ -93,6 +105,7 @@ public class HotWordController {
|
|||
.like(word != null && !word.isEmpty(), HotWord::getWord, word)
|
||||
.eq(category != null && !category.isEmpty(), HotWord::getCategory, category)
|
||||
.eq(hotWordGroupId != null, HotWord::getHotWordGroupId, hotWordGroupId)
|
||||
.isNull(Boolean.TRUE.equals(ungrouped), HotWord::getHotWordGroupId)
|
||||
.orderByDesc(HotWord::getCreatedAt);
|
||||
wrapper.eq(targetTenantId != null, HotWord::getTenantId, targetTenantId);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.imeeting.dto.biz;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Schema(description = "热词批量分组请求参数")
|
||||
public class HotWordBatchGroupDTO {
|
||||
|
||||
@Schema(description = "租户 ID,平台管理员可传 0 表示平台范围")
|
||||
private Long tenantId;
|
||||
|
||||
@Schema(description = "热词 ID 列表")
|
||||
private List<Long> ids;
|
||||
|
||||
@Schema(description = "目标热词组 ID,为空表示移出分组")
|
||||
private Long hotWordGroupId;
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import java.util.List;
|
|||
public interface HotWordService extends IService<HotWord> {
|
||||
HotWordVO saveHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId);
|
||||
HotWordVO updateHotWord(HotWordDTO hotWordDTO, Long userId, Long tenantId);
|
||||
Integer updateHotWordGroupBatch(List<Long> ids, Long hotWordGroupId, Long tenantId);
|
||||
List<String> generatePinyin(String word);
|
||||
List<HotWord> listEnabledByGroupIdIgnoreTenant(Long groupId);
|
||||
List<HotWord> listEnabledByGroupIdAndWordsIgnoreTenant(Long groupId, List<String> words);
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.common.MeetingProgressStage;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.dto.biz.AiModelVO;
|
||||
import com.imeeting.dto.biz.MeetingSummarySource;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptSourceVO;
|
||||
|
|
@ -21,40 +20,25 @@ import com.imeeting.mapper.biz.AiTaskMapper;
|
|||
import com.imeeting.mapper.biz.MeetingMapper;
|
||||
import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
||||
import com.imeeting.service.android.AndroidMeetingPushService;
|
||||
import com.imeeting.support.TaskSecurityContextRunner;
|
||||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.imeeting.service.biz.MeetingProgressService;
|
||||
import com.imeeting.service.biz.MeetingPointsService;
|
||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptChapterService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||
import com.imeeting.support.retry.RetryExecutor;
|
||||
import com.imeeting.support.retry.RetryOptions;
|
||||
|
||||
import com.imeeting.service.biz.*;
|
||||
import com.imeeting.support.TaskSecurityContextRunner;
|
||||
import com.imeeting.support.redis.MeetingAsrPermitCache;
|
||||
import com.imeeting.support.redis.MeetingLockCache;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import com.imeeting.support.retry.RetryExecutor;
|
||||
import com.imeeting.support.retry.RetryOptions;
|
||||
import com.tencentcloudapi.asr.v20190614.AsrClient;
|
||||
import com.tencentcloudapi.asr.v20190614.models.CreateRecTaskRequest;
|
||||
import com.tencentcloudapi.asr.v20190614.models.CreateRecTaskResponse;
|
||||
import com.tencentcloudapi.asr.v20190614.models.DescribeTaskStatusRequest;
|
||||
import com.tencentcloudapi.asr.v20190614.models.DescribeTaskStatusResponse;
|
||||
import com.tencentcloudapi.asr.v20190614.models.SentenceDetail;
|
||||
import com.tencentcloudapi.asr.v20190614.models.TaskStatus;
|
||||
import com.tencentcloudapi.asr.v20190614.models.*;
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import com.unisbase.service.SysParamService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
|
@ -770,7 +754,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
int unchangedCount = 0;
|
||||
|
||||
for (int i = 0; i < 600; i++) {
|
||||
Thread.sleep(2000);
|
||||
Thread.sleep(5000);
|
||||
String queryResp = retryExecutor.execute(
|
||||
RetryOptions.builder()
|
||||
.operation("asr-query")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.imeeting.service.biz.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.imeeting.dto.biz.HotWordDTO;
|
||||
import com.imeeting.dto.biz.HotWordVO;
|
||||
|
|
@ -21,7 +22,9 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
|
|
@ -69,6 +72,37 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
return toVO(hotWord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Integer updateHotWordGroupBatch(List<Long> ids, Long hotWordGroupId, Long tenantId) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
throw new IllegalArgumentException("请选择热词");
|
||||
}
|
||||
Set<Long> uniqueIds = ids.stream()
|
||||
.filter(id -> id != null)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (uniqueIds.isEmpty()) {
|
||||
throw new IllegalArgumentException("请选择热词");
|
||||
}
|
||||
|
||||
List<HotWord> hotWords = this.list(new LambdaQueryWrapper<HotWord>()
|
||||
.in(HotWord::getId, uniqueIds)
|
||||
.eq(HotWord::getTenantId, tenantId));
|
||||
if (hotWords.size() != uniqueIds.size()) {
|
||||
throw new IllegalArgumentException("部分热词不存在或无权操作");
|
||||
}
|
||||
|
||||
if (hotWordGroupId != null) {
|
||||
validateGroupCapacity(hotWordGroupId, tenantId, hotWords);
|
||||
}
|
||||
|
||||
boolean updated = this.update(new LambdaUpdateWrapper<HotWord>()
|
||||
.in(HotWord::getId, uniqueIds)
|
||||
.eq(HotWord::getTenantId, tenantId)
|
||||
.set(HotWord::getHotWordGroupId, hotWordGroupId));
|
||||
return updated ? uniqueIds.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> generatePinyin(String word) {
|
||||
if (word == null || word.isEmpty()) {
|
||||
|
|
@ -140,6 +174,26 @@ public class HotWordServiceImpl extends ServiceImpl<HotWordMapper, HotWord> impl
|
|||
return group.getId();
|
||||
}
|
||||
|
||||
private void validateGroupCapacity(Long groupId, Long tenantId, List<HotWord> movingHotWords) {
|
||||
HotWordGroup group = hotWordGroupMapper.selectById(groupId);
|
||||
if (group == null || !tenantId.equals(group.getTenantId())) {
|
||||
throw new IllegalArgumentException("热词组不存在");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(group.getStatus())) {
|
||||
throw new IllegalArgumentException("热词组已禁用");
|
||||
}
|
||||
Set<Long> movingIds = movingHotWords.stream().map(HotWord::getId).collect(Collectors.toSet());
|
||||
long currentCount = this.count(new LambdaQueryWrapper<HotWord>()
|
||||
.eq(HotWord::getHotWordGroupId, groupId)
|
||||
.notIn(!movingIds.isEmpty(), HotWord::getId, movingIds));
|
||||
long incomingCount = movingHotWords.stream()
|
||||
.filter(item -> !groupId.equals(item.getHotWordGroupId()))
|
||||
.count();
|
||||
if (currentCount + incomingCount > MAX_HOT_WORDS_PER_GROUP) {
|
||||
throw new IllegalArgumentException("热词组最多只能包含 200 个热词");
|
||||
}
|
||||
}
|
||||
|
||||
private void generateCombinations(List<List<String>> matrix, int index, String current, List<String> result) {
|
||||
if (index == matrix.size()) {
|
||||
result.add(current.trim());
|
||||
|
|
|
|||
|
|
@ -30,12 +30,19 @@ export interface HotWordDTO {
|
|||
remark?: string;
|
||||
}
|
||||
|
||||
export interface HotWordBatchGroupDTO {
|
||||
tenantId?: number;
|
||||
ids: number[];
|
||||
hotWordGroupId?: number;
|
||||
}
|
||||
|
||||
export const getHotWordPage = (params: {
|
||||
current: number;
|
||||
size: number;
|
||||
word?: string;
|
||||
category?: string;
|
||||
hotWordGroupId?: number;
|
||||
ungrouped?: boolean;
|
||||
tenantId?: number;
|
||||
}) => {
|
||||
return http.get<{ code: string; data: { records: HotWordVO[]; total: number }; msg: string }>(
|
||||
|
|
@ -64,6 +71,14 @@ export const updateHotWord = (data: HotWordDTO) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const updateHotWordGroupBatch = (data: HotWordBatchGroupDTO, options?: { suppressErrorToast?: boolean }) => {
|
||||
return http.put<{ code: string; data: number; msg: string }>(
|
||||
"/api/biz/hotword/group/batch",
|
||||
data,
|
||||
{ suppressErrorToast: options?.suppressErrorToast }
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteHotWord = (id: number) => {
|
||||
return http.delete<{ code: string; data: boolean; msg: string }>(
|
||||
`/api/biz/hotword/${id}`
|
||||
|
|
|
|||
|
|
@ -265,6 +265,10 @@
|
|||
width: 120px;
|
||||
}
|
||||
|
||||
.hotwords-search__group {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.hotwords-modal-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
|
@ -319,7 +323,7 @@
|
|||
height: 65px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@media (max-width: 640px) {
|
||||
.hotwords-page > .page-container__body,
|
||||
.hotwords-page .section-card,
|
||||
.hotwords-page .section-card__content {
|
||||
|
|
@ -346,7 +350,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: 640px) {
|
||||
.hotwords-list-panel .data-list-panel__left-actions,
|
||||
.hotwords-list-panel .data-list-panel__right-actions,
|
||||
.hotwords-list-panel .data-list-panel__right-actions .ant-space,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
getPinyinSuggestion,
|
||||
saveHotWord,
|
||||
updateHotWord,
|
||||
updateHotWordGroupBatch,
|
||||
type HotWordVO,
|
||||
} from "../../api/business/hotword";
|
||||
import {
|
||||
|
|
@ -70,13 +71,31 @@ type HotWordGroupFormValues = {
|
|||
remark?: string;
|
||||
};
|
||||
|
||||
type BulkGroupFormValues = {
|
||||
hotWordGroupId: number;
|
||||
};
|
||||
|
||||
type HotWordGroupFilter = "all" | "ungrouped" | number;
|
||||
|
||||
type GroupListItem = HotWordGroupVO | { id?: undefined; groupName: string; remark?: string; hotWordCount?: number; status?: number };
|
||||
|
||||
const getBatchGroupErrorMessage = (error: unknown) => {
|
||||
const err = error as { response?: { status?: number; data?: { msg?: string } }; message?: string; msg?: string };
|
||||
const messageText = err.response?.data?.msg || err.msg || err.message || "";
|
||||
if (err.response?.status === 404 || messageText.includes("No static resource") || messageText.includes("group/batch")) {
|
||||
return "批量修改分组接口暂未生效,请更新或重启后端服务后再试";
|
||||
}
|
||||
return messageText || "批量修改分组失败,请稍后重试";
|
||||
};
|
||||
|
||||
const countAssignedHotWords = (records: HotWordVO[]) => records.filter((item) => item.hotWordGroupId).length;
|
||||
|
||||
const HotWords: React.FC = () => {
|
||||
const { message } = App.useApp();
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<HotWordFormValues>();
|
||||
const [groupForm] = Form.useForm<HotWordGroupFormValues>();
|
||||
const [bulkGroupForm] = Form.useForm<BulkGroupFormValues>();
|
||||
const { items: categories } = useDict("biz_hotword_category");
|
||||
const userProfile = useMemo(() => {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
|
|
@ -93,6 +112,7 @@ const HotWords: React.FC = () => {
|
|||
const [searchWord, setSearchWord] = useState("");
|
||||
const [searchCategory, setSearchCategory] = useState<string | undefined>(undefined);
|
||||
const [searchGroupId, setSearchGroupId] = useState<number | undefined>(undefined);
|
||||
const [hotWordGroupFilter, setHotWordGroupFilter] = useState<HotWordGroupFilter>("all");
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
|
|
@ -111,15 +131,23 @@ const HotWords: React.FC = () => {
|
|||
const [groupSearchStatus, setGroupSearchStatus] = useState<number | undefined>(undefined);
|
||||
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
|
||||
const [selectedGroupName, setSelectedGroupName] = useState<string | undefined>(undefined);
|
||||
const [selectedHotWordIds, setSelectedHotWordIds] = useState<number[]>([]);
|
||||
const [bulkGroupEditorVisible, setBulkGroupEditorVisible] = useState(false);
|
||||
const [bulkSubmitLoading, setBulkSubmitLoading] = useState(false);
|
||||
|
||||
const groupNameMap = useMemo(
|
||||
() => Object.fromEntries(groupOptions.map((item) => [item.id, item.groupName])) as Record<number, string>,
|
||||
[groupOptions]
|
||||
);
|
||||
|
||||
const selectedHotWords = useMemo(
|
||||
() => data.filter((item) => selectedHotWordIds.includes(item.id)),
|
||||
[data, selectedHotWordIds]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchData();
|
||||
}, [current, searchCategory, searchGroupId, size]);
|
||||
}, [current, hotWordGroupFilter, searchCategory, searchGroupId, size]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadGroupOptions();
|
||||
|
|
@ -129,15 +157,33 @@ const HotWords: React.FC = () => {
|
|||
void loadGroupPage();
|
||||
}, [groupCurrent, groupSearchName, groupSearchStatus, groupSize, isPlatformAdmin, activeTenantId]);
|
||||
|
||||
const fetchData = async () => {
|
||||
useEffect(() => {
|
||||
setSelectedHotWordIds((ids) => ids.filter((id) => data.some((item) => item.id === id)));
|
||||
}, [data]);
|
||||
|
||||
const fetchData = async (override?: {
|
||||
current?: number;
|
||||
size?: number;
|
||||
word?: string | null;
|
||||
category?: string | null;
|
||||
groupFilter?: HotWordGroupFilter;
|
||||
searchGroupId?: number | null;
|
||||
}) => {
|
||||
const nextCurrent = override?.current ?? current;
|
||||
const nextSize = override?.size ?? size;
|
||||
const nextWord = override?.word !== undefined ? override.word ?? "" : searchWord;
|
||||
const nextCategory = override?.category !== undefined ? override.category ?? undefined : searchCategory;
|
||||
const nextGroupFilter = override?.groupFilter ?? hotWordGroupFilter;
|
||||
const nextSearchGroupId = override?.searchGroupId !== undefined ? override.searchGroupId ?? undefined : searchGroupId;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getHotWordPage({
|
||||
current,
|
||||
size,
|
||||
word: searchWord,
|
||||
category: searchCategory,
|
||||
hotWordGroupId: searchGroupId,
|
||||
current: nextCurrent,
|
||||
size: nextSize,
|
||||
word: nextWord,
|
||||
category: nextCategory,
|
||||
hotWordGroupId: typeof nextGroupFilter === "number" ? nextGroupFilter : nextSearchGroupId,
|
||||
ungrouped: nextGroupFilter === "ungrouped",
|
||||
tenantId: isPlatformAdmin ? activeTenantId : undefined,
|
||||
});
|
||||
if (res.data?.data) {
|
||||
|
|
@ -293,9 +339,79 @@ const HotWords: React.FC = () => {
|
|||
await Promise.all([reloadGroupList(), fetchData()]);
|
||||
};
|
||||
|
||||
const openBulkGroupEditor = () => {
|
||||
if (selectedHotWords.length === 0) {
|
||||
message.warning("请先选择热词");
|
||||
return;
|
||||
}
|
||||
|
||||
const openEditor = () => {
|
||||
bulkGroupForm.resetFields();
|
||||
const firstGroupId = selectedHotWords[0]?.hotWordGroupId ?? 0;
|
||||
const sameGroup = selectedHotWords.every((item) => (item.hotWordGroupId ?? 0) === firstGroupId);
|
||||
if (sameGroup) {
|
||||
bulkGroupForm.setFieldsValue({ hotWordGroupId: firstGroupId });
|
||||
}
|
||||
setBulkGroupEditorVisible(true);
|
||||
};
|
||||
|
||||
const assignedHotWordCount = countAssignedHotWords(selectedHotWords);
|
||||
if (assignedHotWordCount > 0) {
|
||||
Modal.confirm({
|
||||
title: "确认修改热词组?",
|
||||
content: `当前选择的热词中,有 ${assignedHotWordCount} 个已分配热词组。继续后,这些热词的原分组会被你后续选择的目标热词组覆盖。`,
|
||||
okText: "继续修改",
|
||||
cancelText: "取消",
|
||||
onOk: openEditor,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
openEditor();
|
||||
};
|
||||
|
||||
const handleBulkGroupSubmit = async () => {
|
||||
const values = await bulkGroupForm.validateFields();
|
||||
const records = [...selectedHotWords];
|
||||
const targetGroupId = values.hotWordGroupId === 0 ? undefined : values.hotWordGroupId;
|
||||
setBulkSubmitLoading(true);
|
||||
try {
|
||||
await updateHotWordGroupBatch(
|
||||
{
|
||||
ids: records.map((item) => item.id),
|
||||
hotWordGroupId: targetGroupId,
|
||||
tenantId: isPlatformAdmin ? activeTenantId : undefined,
|
||||
},
|
||||
{ suppressErrorToast: true }
|
||||
);
|
||||
message.success(`已修改 ${records.length} 个热词`);
|
||||
setBulkGroupEditorVisible(false);
|
||||
setSelectedHotWordIds([]);
|
||||
await Promise.all([fetchData(), loadGroupOptions(), loadGroupPage()]);
|
||||
} catch (error) {
|
||||
message.error(getBatchGroupErrorMessage(error));
|
||||
} finally {
|
||||
setBulkSubmitLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetFilters = () => {
|
||||
setSelectedHotWordIds([]);
|
||||
setSearchWord("");
|
||||
setSearchCategory(undefined);
|
||||
setSearchGroupId(undefined);
|
||||
setSelectedGroupName(undefined);
|
||||
setHotWordGroupFilter("all");
|
||||
bulkGroupForm.resetFields();
|
||||
setBulkGroupEditorVisible(false);
|
||||
setCurrent(1);
|
||||
void fetchData({ current: 1, word: "", category: null, groupFilter: "all", searchGroupId: null });
|
||||
};
|
||||
|
||||
const handleSelectGroup = (item: GroupListItem) => {
|
||||
setSearchGroupId(item.id);
|
||||
setSelectedGroupName(item.id ? item.groupName : undefined);
|
||||
setHotWordGroupFilter(item.id ?? "all");
|
||||
setCurrent(1);
|
||||
};
|
||||
|
||||
|
|
@ -303,6 +419,15 @@ const HotWords: React.FC = () => {
|
|||
? selectedGroupName || groupData.find((item) => item.id === searchGroupId)?.groupName || groupNameMap[searchGroupId] || "热词列表"
|
||||
: "全部热词";
|
||||
|
||||
const groupFilterOptions = useMemo(
|
||||
() => [
|
||||
{ label: "全部词组", value: "all" as const },
|
||||
{ label: "未分配", value: "ungrouped" as const },
|
||||
...groupOptions.map((item) => ({ label: item.groupName, value: item.id })),
|
||||
],
|
||||
[groupOptions]
|
||||
);
|
||||
|
||||
const groupListData: GroupListItem[] = [{ id: undefined, groupName: "全部热词" }, ...groupData];
|
||||
|
||||
const columns = [
|
||||
|
|
@ -511,7 +636,23 @@ const HotWords: React.FC = () => {
|
|||
|
||||
<DataListPanel
|
||||
className="hotwords-list-panel"
|
||||
leftActions={<Text strong>{hotWordGroupTitle}</Text>}
|
||||
leftActions={
|
||||
<Space wrap size="small">
|
||||
<Text strong>{hotWordGroupTitle}</Text>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
|
||||
新增热词
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
disabled={selectedHotWords.length === 0}
|
||||
onClick={openBulkGroupEditor}
|
||||
>
|
||||
修改分组{selectedHotWords.length > 0 ? ` (${selectedHotWords.length})` : ""}
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { setCurrent(1); void fetchData(); void loadGroupPage(); }} title="刷新" aria-label="刷新" />
|
||||
</Space>
|
||||
}
|
||||
rightActions={
|
||||
<Space wrap size="small">
|
||||
<Input
|
||||
|
|
@ -520,9 +661,23 @@ const HotWords: React.FC = () => {
|
|||
allowClear
|
||||
value={searchWord}
|
||||
onChange={(e) => setSearchWord(e.target.value)}
|
||||
onPressEnter={() => { setCurrent(1); void fetchData(); }}
|
||||
onPressEnter={() => { setCurrent(1); void fetchData({ current: 1, word: searchWord }); }}
|
||||
className="hotwords-search__word"
|
||||
/>
|
||||
<Select
|
||||
placeholder="筛选热词组"
|
||||
allowClear
|
||||
value={hotWordGroupFilter}
|
||||
options={groupFilterOptions}
|
||||
onChange={(value) => {
|
||||
const nextFilter = (value ?? "all") as HotWordGroupFilter;
|
||||
setHotWordGroupFilter(nextFilter);
|
||||
setSearchGroupId(undefined);
|
||||
setSelectedGroupName(undefined);
|
||||
setCurrent(1);
|
||||
}}
|
||||
className="hotwords-search__group"
|
||||
/>
|
||||
<Select
|
||||
placeholder="筛选分类"
|
||||
allowClear
|
||||
|
|
@ -530,15 +685,13 @@ const HotWords: React.FC = () => {
|
|||
onChange={(value) => {
|
||||
setSearchCategory(value as string);
|
||||
setCurrent(1);
|
||||
void fetchData();
|
||||
}}
|
||||
className="hotwords-search__category"
|
||||
options={categories.map((c) => ({ label: c.itemLabel, value: c.itemValue }))}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
|
||||
新增热词
|
||||
<Button onClick={handleResetFilters} disabled={selectedHotWordIds.length === 0 && searchWord === "" && !searchCategory && !searchGroupId && hotWordGroupFilter === "all"}>
|
||||
重置
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { setCurrent(1); void fetchData(); void loadGroupPage(); }} title="刷新" aria-label="刷新" />
|
||||
</Space>
|
||||
}
|
||||
footer={
|
||||
|
|
@ -546,7 +699,7 @@ const HotWords: React.FC = () => {
|
|||
current={current}
|
||||
pageSize={size}
|
||||
total={total}
|
||||
onChange={(page, pageSize) => {
|
||||
onChange={(page, pageSize) => {
|
||||
setCurrent(page);
|
||||
setSize(pageSize);
|
||||
}}
|
||||
|
|
@ -560,6 +713,10 @@ const HotWords: React.FC = () => {
|
|||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: "max(100%, 1000px)", y: "100%" }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedHotWordIds,
|
||||
onChange: (keys) => setSelectedHotWordIds(keys as number[]),
|
||||
}}
|
||||
pagination={false}
|
||||
/>
|
||||
</DataListPanel>
|
||||
|
|
@ -661,6 +818,32 @@ const HotWords: React.FC = () => {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="批量修改热词组"
|
||||
open={bulkGroupEditorVisible}
|
||||
onCancel={handleResetFilters}
|
||||
onOk={() => void handleBulkGroupSubmit()}
|
||||
confirmLoading={bulkSubmitLoading}
|
||||
okText="确定"
|
||||
cancelText={t("common.cancel")}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form form={bulkGroupForm} layout="vertical" className="hotwords-modal-form">
|
||||
<Form.Item>
|
||||
<Text type="secondary">将 {selectedHotWords.length} 个热词修改为目标分组。</Text>
|
||||
</Form.Item>
|
||||
<Form.Item name="hotWordGroupId" label="目标热词组" rules={[{ required: true, message: "请选择目标热词组" }]}>
|
||||
<Select
|
||||
placeholder="请选择热词组"
|
||||
options={[
|
||||
{ label: "未分组", value: 0 },
|
||||
...groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id })),
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue