feat: 规则批量删除与多选操作
后端新增 DELETE /rule/batch,按 userId 校验规则归属,先清理标签关联再批量删除并返回实际删除数;前端规则管理表格增加多选列与批量删除按钮,补充三语文案;同步更新 README 中 AI 渠道推荐与图片记账说明。
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
### 交易渠道
|
||||
- 内置渠道:支付宝、微信、美团、淘宝、京东、招商银行、交通银行、现金
|
||||
- 支持自定义渠道的新建、改名与删除(有账单使用时不可删除,改名不影响历史账单)
|
||||
- 新增/编辑记录时可由 AI 推荐已有渠道或生成新渠道
|
||||
|
||||
### 统计与图表
|
||||
- 月度收支统计、分类占比、收支趋势、分组统计与概览汇总
|
||||
@@ -35,7 +36,7 @@
|
||||
### AI 能力(DeepSeek)
|
||||
- 交易记录分类与标签的批量 / 单条智能建议,支持一键采纳
|
||||
- 自然语言查询解析与自然语言记账解析
|
||||
- 图片记账:上传账单 / 支付截图,由视觉模型识别并生成多条记录,逐条预览勾选后批量导入
|
||||
- 图片记账:一次上传最多 5 张账单 / 支付截图(支持粘贴),由视觉模型识别、SSE 实时推送进度,生成多条记录并标记疑似重复(按「类型+金额+时间」),提取原始字段,逐条预览勾选后批量导入
|
||||
- AI 生成分类、标签,以及 Emoji / 颜色推荐
|
||||
- AI 调用用量统计与明细查看
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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} 件のルールを削除しました' },
|
||||
},
|
||||
|
||||
// 账单导入
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user