feat: 规则批量删除与多选操作
后端新增 DELETE /rule/batch,按 userId 校验规则归属,先清理标签关联再批量删除并返回实际删除数;前端规则管理表格增加多选列与批量删除按钮,补充三语文案;同步更新 README 中 AI 渠道推荐与图片记账说明。
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
### 交易渠道
|
### 交易渠道
|
||||||
- 内置渠道:支付宝、微信、美团、淘宝、京东、招商银行、交通银行、现金
|
- 内置渠道:支付宝、微信、美团、淘宝、京东、招商银行、交通银行、现金
|
||||||
- 支持自定义渠道的新建、改名与删除(有账单使用时不可删除,改名不影响历史账单)
|
- 支持自定义渠道的新建、改名与删除(有账单使用时不可删除,改名不影响历史账单)
|
||||||
|
- 新增/编辑记录时可由 AI 推荐已有渠道或生成新渠道
|
||||||
|
|
||||||
### 统计与图表
|
### 统计与图表
|
||||||
- 月度收支统计、分类占比、收支趋势、分组统计与概览汇总
|
- 月度收支统计、分类占比、收支趋势、分组统计与概览汇总
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
### AI 能力(DeepSeek)
|
### AI 能力(DeepSeek)
|
||||||
- 交易记录分类与标签的批量 / 单条智能建议,支持一键采纳
|
- 交易记录分类与标签的批量 / 单条智能建议,支持一键采纳
|
||||||
- 自然语言查询解析与自然语言记账解析
|
- 自然语言查询解析与自然语言记账解析
|
||||||
- 图片记账:上传账单 / 支付截图,由视觉模型识别并生成多条记录,逐条预览勾选后批量导入
|
- 图片记账:一次上传最多 5 张账单 / 支付截图(支持粘贴),由视觉模型识别、SSE 实时推送进度,生成多条记录并标记疑似重复(按「类型+金额+时间」),提取原始字段,逐条预览勾选后批量导入
|
||||||
- AI 生成分类、标签,以及 Emoji / 颜色推荐
|
- AI 生成分类、标签,以及 Emoji / 颜色推荐
|
||||||
- AI 调用用量统计与明细查看
|
- AI 调用用量统计与明细查看
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ export function deleteRule(id: number) {
|
|||||||
return request.delete(`/rule/${id}`)
|
return request.delete(`/rule/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 批量删除规则,返回实际删除数量 */
|
||||||
|
export function batchDeleteRules(ids: number[]) {
|
||||||
|
return request.delete('/rule/batch', { data: { ids } })
|
||||||
|
}
|
||||||
|
|
||||||
/** 启用/停用指定规则 */
|
/** 启用/停用指定规则 */
|
||||||
export function toggleRuleEnabled(id: number, enabled: boolean) {
|
export function toggleRuleEnabled(id: number, enabled: boolean) {
|
||||||
return request.put(`/rule/${id}/enabled`, { enabled })
|
return request.put(`/rule/${id}/enabled`, { enabled })
|
||||||
|
|||||||
@@ -546,6 +546,10 @@ export default {
|
|||||||
status: { zh: '启用状态', en: 'Status', ja: '有効状態' },
|
status: { zh: '启用状态', en: 'Status', ja: '有効状態' },
|
||||||
statusOn: { zh: '已启用', en: 'Enabled', ja: '有効' },
|
statusOn: { zh: '已启用', en: 'Enabled', ja: '有効' },
|
||||||
statusOff: { zh: '已停用', en: 'Disabled', 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,
|
NInput, NInputNumber, NSelect, NSwitch, NTag, NText, NCard,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import type { DataTableColumns } 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 type { RuleItem } from '../api/rule'
|
||||||
import { getCategoryList } from '../api/category'
|
import { getCategoryList } from '../api/category'
|
||||||
import { getTagList } from '../api/tag'
|
import { getTagList } from '../api/tag'
|
||||||
@@ -52,6 +52,7 @@ const total = ref(0)
|
|||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const filterEnabled = ref<string | null>(null) // 'ON' | 'OFF' | null
|
const filterEnabled = ref<string | null>(null) // 'ON' | 'OFF' | null
|
||||||
const filterType = ref<string | null>(null)
|
const filterType = ref<string | null>(null)
|
||||||
|
const checkedRuleIds = ref<number[]>([]) // 表格多选选中的规则ID(用于批量删除)
|
||||||
|
|
||||||
/** AI 规则建议行(带本地 key 供表格选择使用) */
|
/** AI 规则建议行(带本地 key 供表格选择使用) */
|
||||||
type AiRuleRow = AiRuleSuggestion & { key: number }
|
type AiRuleRow = AiRuleSuggestion & { key: number }
|
||||||
@@ -162,6 +163,8 @@ function conditionTags(row: RuleItem) {
|
|||||||
|
|
||||||
/** 创建表格列配置 */
|
/** 创建表格列配置 */
|
||||||
const columns = computed<DataTableColumns<RuleItem>>(() => [
|
const columns = computed<DataTableColumns<RuleItem>>(() => [
|
||||||
|
// 多选列:用于批量操作(如批量删除)
|
||||||
|
{ type: 'selection' },
|
||||||
{
|
{
|
||||||
title: t('common.name'),
|
title: t('common.name'),
|
||||||
key: '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) => {
|
watch(() => form.value.type, (type) => {
|
||||||
if (form.value.categoryId && type) {
|
if (form.value.categoryId && type) {
|
||||||
@@ -739,6 +767,9 @@ onMounted(() => {
|
|||||||
<h2 style="margin: 0;">{{ t('rule.title') }}</h2>
|
<h2 style="margin: 0;">{{ t('rule.title') }}</h2>
|
||||||
<NSpace>
|
<NSpace>
|
||||||
<NButton v-if="aiEnabled" @click="handleAiRuleOpen">{{ t('ai.rule.entry') }}</NButton>
|
<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>
|
<NButton type="primary" @click="handleAdd">{{ t('rule.add') }}</NButton>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
</NSpace>
|
</NSpace>
|
||||||
@@ -773,11 +804,13 @@ onMounted(() => {
|
|||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
<NDataTable
|
<NDataTable
|
||||||
|
v-model:checked-row-keys="checkedRuleIds"
|
||||||
remote
|
remote
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="rules"
|
:data="rules"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
|
:row-key="(row: RuleItem) => row.id"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
@update:page="handlePageChange"
|
@update:page="handlePageChange"
|
||||||
@update:page-size="handlePageSizeChange"
|
@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.PageResult;
|
||||||
import cn.rosercode.finance.common.Result;
|
import cn.rosercode.finance.common.Result;
|
||||||
|
import cn.rosercode.finance.dto.BatchDeleteRequest;
|
||||||
import cn.rosercode.finance.dto.RuleQuery;
|
import cn.rosercode.finance.dto.RuleQuery;
|
||||||
import cn.rosercode.finance.dto.RuleRequest;
|
import cn.rosercode.finance.dto.RuleRequest;
|
||||||
import cn.rosercode.finance.service.RuleService;
|
import cn.rosercode.finance.service.RuleService;
|
||||||
@@ -111,6 +112,19 @@ public class RuleController {
|
|||||||
return Result.success();
|
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);
|
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);
|
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}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user