feat: 规则批量删除与多选操作

后端新增 DELETE /rule/batch,按 userId 校验规则归属,先清理标签关联再批量删除并返回实际删除数;前端规则管理表格增加多选列与批量删除按钮,补充三语文案;同步更新 README 中 AI 渠道推荐与图片记账说明。
This commit is contained in:
2026-09-21 20:30:45 +08:00
parent 2ade60da42
commit 09ca5b43fb
7 changed files with 100 additions and 2 deletions
+2 -1
View File
@@ -17,6 +17,7 @@
### 交易渠道
- 内置渠道:支付宝、微信、美团、淘宝、京东、招商银行、交通银行、现金
- 支持自定义渠道的新建、改名与删除(有账单使用时不可删除,改名不影响历史账单)
- 新增/编辑记录时可由 AI 推荐已有渠道或生成新渠道
### 统计与图表
- 月度收支统计、分类占比、收支趋势、分组统计与概览汇总
@@ -35,7 +36,7 @@
### AI 能力(DeepSeek)
- 交易记录分类与标签的批量 / 单条智能建议,支持一键采纳
- 自然语言查询解析与自然语言记账解析
- 图片记账:上传账单 / 支付截图,由视觉模型识别并生成多条记录,逐条预览勾选后批量导入
- 图片记账:一次上传最多 5 张账单 / 支付截图(支持粘贴),由视觉模型识别、SSE 实时推送进度,生成多条记录并标记疑似重复(按「类型+金额+时间」),提取原始字段,逐条预览勾选后批量导入
- AI 生成分类、标签,以及 Emoji / 颜色推荐
- AI 调用用量统计与明细查看
+5
View File
@@ -73,6 +73,11 @@ export function deleteRule(id: number) {
return request.delete(`/rule/${id}`)
}
/** 批量删除规则,返回实际删除数量 */
export function batchDeleteRules(ids: number[]) {
return request.delete('/rule/batch', { data: { ids } })
}
/** 启用/停用指定规则 */
export function toggleRuleEnabled(id: number, enabled: boolean) {
return request.put(`/rule/${id}/enabled`, { enabled })
+4
View File
@@ -546,6 +546,10 @@ export default {
status: { zh: '启用状态', en: 'Status', ja: '有効状態' },
statusOn: { zh: '已启用', en: 'Enabled', ja: '有効' },
statusOff: { zh: '已停用', en: 'Disabled', ja: '無効' },
batchDelete: { zh: '批量删除', en: 'Batch Delete', ja: '一括削除' },
batchDeleteNone: { zh: '请先选择要删除的规则', en: 'Please select rules to delete', ja: '削除するルールを選択してください' },
batchDeleteConfirm: { zh: '确定删除选中的 {count} 条规则吗?', en: 'Delete the {count} selected rule(s)?', ja: '選択した {count} 件のルールを削除しますか?' },
batchDeleted: { zh: '已删除 {count} 条规则', en: 'Deleted {count} rule(s)', ja: '{count} 件のルールを削除しました' },
},
// 账单导入
+34 -1
View File
@@ -7,7 +7,7 @@ import {
NInput, NInputNumber, NSelect, NSwitch, NTag, NText, NCard,
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { getRulePage, createRule, updateRule, deleteRule, toggleRuleEnabled } from '../api/rule'
import { getRulePage, createRule, updateRule, deleteRule, batchDeleteRules, toggleRuleEnabled } from '../api/rule'
import type { RuleItem } from '../api/rule'
import { getCategoryList } from '../api/category'
import { getTagList } from '../api/tag'
@@ -52,6 +52,7 @@ const total = ref(0)
const searchKeyword = ref('')
const filterEnabled = ref<string | null>(null) // 'ON' | 'OFF' | null
const filterType = ref<string | null>(null)
const checkedRuleIds = ref<number[]>([]) // 表格多选选中的规则ID(用于批量删除)
/** AI 规则建议行(带本地 key 供表格选择使用) */
type AiRuleRow = AiRuleSuggestion & { key: number }
@@ -162,6 +163,8 @@ function conditionTags(row: RuleItem) {
/** 创建表格列配置 */
const columns = computed<DataTableColumns<RuleItem>>(() => [
// 多选列:用于批量操作(如批量删除)
{ type: 'selection' },
{
title: t('common.name'),
key: 'name',
@@ -713,6 +716,31 @@ function handleDelete(id: number) {
})
}
/** 批量删除选中的规则:确认后调用接口并刷新列表 */
function handleBatchDelete() {
const count = checkedRuleIds.value.length
if (count === 0) {
message.warning(t('rule.batchDeleteNone'))
return
}
dialog.warning({
title: t('common.confirmDelete'),
content: t('rule.batchDeleteConfirm', { count }),
positiveText: t('common.delete'),
negativeText: t('common.cancel'),
onPositiveClick: async () => {
try {
const res: any = await batchDeleteRules(checkedRuleIds.value)
message.success(t('rule.batchDeleted', { count: res.data }))
checkedRuleIds.value = []
await fetchData()
} catch (e: any) {
message.error(e.message || t('common.deleteFailed'))
}
},
})
}
// 收支类型变化时,若已选分类类型不匹配则清空,避免保存非法组合
watch(() => form.value.type, (type) => {
if (form.value.categoryId && type) {
@@ -739,6 +767,9 @@ onMounted(() => {
<h2 style="margin: 0;">{{ t('rule.title') }}</h2>
<NSpace>
<NButton v-if="aiEnabled" @click="handleAiRuleOpen">{{ t('ai.rule.entry') }}</NButton>
<NButton type="error" :disabled="checkedRuleIds.length === 0" @click="handleBatchDelete">
{{ t('rule.batchDelete') }}
</NButton>
<NButton type="primary" @click="handleAdd">{{ t('rule.add') }}</NButton>
</NSpace>
</NSpace>
@@ -773,11 +804,13 @@ onMounted(() => {
</NCard>
<NDataTable
v-model:checked-row-keys="checkedRuleIds"
remote
:columns="columns"
:data="rules"
:loading="loading"
:bordered="false"
:row-key="(row: RuleItem) => row.id"
:pagination="pagination"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RestController;
import cn.rosercode.finance.common.PageResult;
import cn.rosercode.finance.common.Result;
import cn.rosercode.finance.dto.BatchDeleteRequest;
import cn.rosercode.finance.dto.RuleQuery;
import cn.rosercode.finance.dto.RuleRequest;
import cn.rosercode.finance.service.RuleService;
@@ -111,6 +112,19 @@ public class RuleController {
return Result.success();
}
/**
* 批量删除规则
*
* @param body 批量删除请求(规则ID列表)
* @return 实际删除的规则数
*/
@DeleteMapping("/batch")
@Operation(summary = "批量删除规则", description = "根据规则ID列表批量删除当前用户的规则及其标签关联")
public Result<Integer> batchDelete(@RequestBody @Valid BatchDeleteRequest body) {
Long userId = UserContext.getUserId();
return Result.success(ruleService.batchDelete(userId, body.getIds()));
}
/**
* 启用或停用规则
*
@@ -56,6 +56,17 @@ public interface RuleService {
*/
void delete(Long userId, Long id);
/**
* 批量删除规则及其标签关联。
*
* <p>仅删除归属当前用户的规则,先清理标签关联再批量删除,返回实际删除数。</p>
*
* @param userId 当前用户ID
* @param ids 待删除的规则ID列表
* @return 实际删除的规则数
*/
int batchDelete(Long userId, List<Long> ids);
/**
* 启用或停用规则。
*
@@ -195,6 +195,36 @@ public class RuleServiceImpl implements RuleService {
log.info("删除规则: userId={}, id={}", userId, id);
}
/**
* {@inheritDoc}
*
* 仅删除归属当前用户的规则,先清理标签关联再批量删除,返回实际删除数。
*/
@Override
@Transactional
public int batchDelete(Long userId, List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return 0;
}
// 仅保留属于当前用户的规则ID
List<Long> ownedIds = ruleMapper.selectList(new LambdaQueryWrapper<Rule>()
.select(Rule::getId)
.eq(Rule::getUserId, userId)
.in(Rule::getId, ids))
.stream()
.map(Rule::getId)
.collect(Collectors.toList());
if (ownedIds.isEmpty()) {
return 0;
}
// 先清理标签关联,再批量删除规则
ruleTagMapper.delete(new LambdaQueryWrapper<RuleTag>()
.in(RuleTag::getRuleId, ownedIds));
int deleted = ruleMapper.deleteByIds(ownedIds);
log.info("批量删除规则: userId={}, 请求={}, 实际={}", userId, ids.size(), deleted);
return deleted;
}
/**
* {@inheritDoc}
*/