Commit b9f1ac5d authored by 刘斌's avatar 刘斌

fix: 完善消费记录和充值记录

parent b54c4d35
......@@ -20,6 +20,12 @@
<artifactId>admin</artifactId>
<version>${binfast.version}</version>
</dependency>
<!-- 用户密码加密 如数据库连接等 -->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>top.binfast</groupId>
<artifactId>daemon-codegen</artifactId>
......@@ -36,6 +42,8 @@
</dependencies>
<build>
<!-- 打包产物名(不含扩展名),mvn package 后生成 admin-ai.${project.version}.jar -->
<finalName>admin-ai.${project.version}</finalName>
<plugins>
<!-- 跳过单元测试 -->
<plugin>
......
package com.anplus.hr.ai.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.lang.tree.Tree;
import com.alibaba.cola.dto.MultiResponse;
import com.alibaba.cola.dto.PageResponse;
import com.alibaba.cola.dto.Response;
import com.alibaba.cola.dto.SingleResponse;
......@@ -11,8 +13,11 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.anplus.hr.ai.domain.params.AiConsumeRecordListParam;
import com.anplus.hr.ai.domain.params.AiConsumeRecordParam;
import com.anplus.hr.ai.domain.vo.AiConsumeRecordSummaryVo;
import com.anplus.hr.ai.domain.vo.AiConsumeRecordVo;
import com.anplus.hr.ai.service.AiConsumeRecordServ;
import top.binfast.app.biz.sysapi.bean.params.sysDept.SysDeptParam;
import top.binfast.app.biz.sysbiz.service.SysDeptServ;
import top.binfast.common.core.constant.BusinessType;
import top.binfast.common.core.util.ResponseUtils;
import top.binfast.common.excel.annotion.ExcelExport;
......@@ -33,6 +38,7 @@ import java.util.List;
public class AiConsumeRecordCtrl {
private final AiConsumeRecordServ aiConsumeRecordServ;
private final SysDeptServ sysDeptServ;
/**
* 分页查询AI工具消费记录列表。
......@@ -46,6 +52,18 @@ public class AiConsumeRecordCtrl {
return aiConsumeRecordServ.queryPageList(param);
}
/**
* 查询当前筛选条件下的金额合计(跨所有分页的全量结果,口径与列表完全一致)。
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountCny 保留两位小数;无匹配数据时为 0.00)
*/
@SaCheckPermission("ai:consume:list")
@GetMapping("/summary")
public SingleResponse<AiConsumeRecordSummaryVo> summary(AiConsumeRecordListParam param) {
return SingleResponse.of(aiConsumeRecordServ.querySummary(param));
}
/**
* 导出AI工具消费记录列表。
*
......@@ -71,6 +89,18 @@ public class AiConsumeRecordCtrl {
return SingleResponse.of(aiConsumeRecordServ.queryById(id));
}
/**
* 按批次号获取整批消费记录详情(主从批量编辑回填用)。
*
* @param batchNo 批次号
* @return 整批详情(公共字段 + details 明细列表,line_sort 升序)
*/
@SaCheckPermission("ai:consume:query")
@GetMapping("/batch/{batchNo}")
public SingleResponse<AiConsumeRecordVo> getBatchDetail(@PathVariable String batchNo) {
return SingleResponse.of(aiConsumeRecordServ.queryByBatchNo(batchNo));
}
/**
* 新增AI工具消费记录。
*
......@@ -109,4 +139,16 @@ public class AiConsumeRecordCtrl {
@PathVariable Long[] ids) {
return ResponseUtils.ofResult(aiConsumeRecordServ.delByIds(List.of(ids)));
}
/**
* 获取消费记录筛选用的部门树。
*
* @param dept 部门查询条件
* @return 部门树列表
*/
@SaCheckPermission("ai:consume:list")
@GetMapping("/deptTree")
public MultiResponse<Tree<Long>> deptTree(SysDeptParam dept) {
return MultiResponse.of(sysDeptServ.selectDeptTreeList(dept));
}
}
package com.anplus.hr.ai.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.lang.tree.Tree;
import com.alibaba.cola.dto.MultiResponse;
import com.alibaba.cola.dto.PageResponse;
import com.alibaba.cola.dto.Response;
import com.alibaba.cola.dto.SingleResponse;
......@@ -11,8 +13,11 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.anplus.hr.ai.domain.params.AiRechargeRecordListParam;
import com.anplus.hr.ai.domain.params.AiRechargeRecordParam;
import com.anplus.hr.ai.domain.vo.AiRechargeRecordSummaryVo;
import com.anplus.hr.ai.domain.vo.AiRechargeRecordVo;
import com.anplus.hr.ai.service.AiRechargeRecordServ;
import top.binfast.app.biz.sysapi.bean.params.sysDept.SysDeptParam;
import top.binfast.app.biz.sysbiz.service.SysDeptServ;
import top.binfast.common.core.constant.BusinessType;
import top.binfast.common.core.util.ResponseUtils;
import top.binfast.common.excel.annotion.ExcelExport;
......@@ -33,6 +38,7 @@ import java.util.List;
public class AiRechargeRecordCtrl {
private final AiRechargeRecordServ aiRechargeRecordServ;
private final SysDeptServ sysDeptServ;
/**
* 分页查询AI工具充值记录列表。
......@@ -46,6 +52,18 @@ public class AiRechargeRecordCtrl {
return aiRechargeRecordServ.queryPageList(param);
}
/**
* 查询当前筛选条件下的双金额合计(跨所有分页的全量结果,口径与列表完全一致)。
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountUsd、amountCny 各保留两位小数;无匹配数据时为 0.00)
*/
@SaCheckPermission("ai:recharge:list")
@GetMapping("/summary")
public SingleResponse<AiRechargeRecordSummaryVo> summary(AiRechargeRecordListParam param) {
return SingleResponse.of(aiRechargeRecordServ.querySummary(param));
}
/**
* 导出AI工具充值记录列表。
*
......@@ -109,4 +127,16 @@ public class AiRechargeRecordCtrl {
@PathVariable Long[] ids) {
return ResponseUtils.ofResult(aiRechargeRecordServ.delByIds(List.of(ids)));
}
/**
* 获取充值记录筛选用的部门树。
*
* @param dept 部门查询条件
* @return 部门树列表
*/
@SaCheckPermission("ai:recharge:list")
@GetMapping("/deptTree")
public MultiResponse<Tree<Long>> deptTree(SysDeptParam dept) {
return MultiResponse.of(sysDeptServ.selectDeptTreeList(dept));
}
}
......@@ -8,7 +8,7 @@ import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalDate;
/**
* AI工具消费记录对象 ai_consume_record
......@@ -26,7 +26,12 @@ public class AiConsumeRecord extends TenantModel {
private static final long serialVersionUID = 1L;
/**
* 项目名称(字典ai_project_name的dict_value)
* 所属部门ID(关联sys_dept.id)
*/
private Long deptId;
/**
* 项目名称(用户直接输入的自由文本)
*/
private String projectName;
......@@ -36,7 +41,7 @@ public class AiConsumeRecord extends TenantModel {
private String userName;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value
* AI工具/平台名称(用户直接输入的自由文本
*/
private String toolName;
......@@ -58,11 +63,21 @@ public class AiConsumeRecord extends TenantModel {
/**
* 消费发生时间
*/
private LocalDateTime consumeTime;
private LocalDate consumeTime;
/**
* 备注
*/
private String remark;
/**
* 批次号(主从批量录入:同批多行共享同一 batch_no,全局唯一)
*/
private String batchNo;
/**
* 行序号(批次内从 1 连续自增)
*/
private Integer lineSort;
}
......@@ -9,7 +9,6 @@ import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* AI工具充值记录对象 ai_recharge_record
......@@ -27,7 +26,12 @@ public class AiRechargeRecord extends TenantModel {
private static final long serialVersionUID = 1L;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
* 所属部门ID(关联sys_dept.id)
*/
private Long deptId;
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
private String toolName;
......@@ -44,7 +48,7 @@ public class AiRechargeRecord extends TenantModel {
/**
* 充值完成时间
*/
private LocalDateTime rechargeTime;
private LocalDate rechargeTime;
/**
* 到期时间(纯字段,无联动)
......
package com.anplus.hr.ai.domain.params;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.math.BigDecimal;
/**
* AI工具消费记录明细行业务对象(主从批量录入的子表行)
*
* <p>对应 {@code ai_consume_record} 单行的工具维度字段。消耗积分与金额维持非必填
* (Clarify Q4:对齐既有 baseline 与现有代码口径,放宽需求文档「必填」措辞),
* 仅约束最多 2 位小数。校验注解均不带 groups,由父对象 {@code @Valid} 级联触发。</p>
*
* @author LiuBin
* @date 2026-09-08
*/
@Data
public class AiConsumeDetailParam {
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
@NotBlank(message = "AI工具/平台名称不能为空")
private String toolName;
/**
* 国内/国外(中文原文,固定二值)
*/
@NotBlank(message = "国内/国外不能为空")
@Pattern(regexp = "国内|国外", message = "国内/国外仅接受「国内」或「国外」")
private String region;
/**
* 消耗积分(非必填,最多2位小数)
*/
@Digits(integer = 16, fraction = 2, message = "消耗积分最多2位小数")
private BigDecimal consumePoints;
/**
* 金额(人民币)(非必填,最多2位小数)
*/
@Digits(integer = 16, fraction = 2, message = "金额(人民币)最多2位小数")
private BigDecimal amountCny;
}
......@@ -18,7 +18,7 @@ import java.util.Map;
public class AiConsumeRecordListParam extends PageQueryParam {
/**
* 项目名称(字典ai_project_name的dict_value
* 项目名称(用户直接输入的自由文本
*/
private String projectName;
......@@ -28,10 +28,15 @@ public class AiConsumeRecordListParam extends PageQueryParam {
private String userName;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value
* AI工具/平台名称(用户直接输入的自由文本
*/
private String toolName;
/**
* 归属部门id(部门树,含子部门)
*/
private Long belongDeptId;
private Map<String, Object> params = new HashMap<>();
}
package com.anplus.hr.ai.domain.params;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.Digits;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import com.anplus.hr.ai.domain.model.AiConsumeRecord;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.util.List;
/**
* AI工具消费记录业务对象 ai_consume_record
* AI工具消费记录业务对象(主从批量录入)ai_consume_record
*
* <p>上部公共字段对整批生效,下部 {@code details} 为明细行列表;新增时 {@code batchNo}
* 为空由服务端生成,编辑时携带 {@code batchNo} 定位原批(整批全删重插)。校验注解均不带
* groups,配合 Controller 的 {@code @Validated} 默认组生效(对齐既有 Param 风格)。</p>
*
* @author LiuBin
* @date 2026-09-04
*/
@Data
@AutoMapper(target = AiConsumeRecord.class, reverseConvertGenerate = false)
public class AiConsumeRecordParam {
/**
* 主键ID(新增为空,修改必填
* 批次号(编辑时定位原批;新增时为空、由服务端生成
*/
private Long id;
private String batchNo;
/**
* 项目名称(字典ai_project_name的dict_value)
* 所属部门ID(关联sys_dept.id)
*/
@NotNull(message = "所属部门不能为空")
private Long deptId;
/**
* 项目名称(用户直接输入的自由文本)
*/
@NotBlank(message = "项目名称不能为空")
private String projectName;
......@@ -39,36 +46,11 @@ public class AiConsumeRecordParam {
@NotBlank(message = "使用人姓名不能为空")
private String userName;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
@NotBlank(message = "AI工具/平台名称不能为空")
private String toolName;
/**
* 国内/国外(中文原文,固定二值)
*/
@NotBlank(message = "国内/国外不能为空")
@Pattern(regexp = "国内|国外", message = "国内/国外仅接受「国内」或「国外」")
private String region;
/**
* 消耗积分
*/
@Digits(integer = 16, fraction = 2, message = "消耗积分最多2位小数")
private BigDecimal consumePoints;
/**
* 金额(人民币)
*/
@Digits(integer = 16, fraction = 2, message = "金额(人民币)最多2位小数")
private BigDecimal amountCny;
/**
* 消费发生时间
*/
@NotNull(message = "消费发生时间不能为空")
private LocalDateTime consumeTime;
private LocalDate consumeTime;
/**
* 备注
......@@ -76,4 +58,11 @@ public class AiConsumeRecordParam {
@Size(max = 200, message = "备注最长200字符")
private String remark;
/**
* 消费明细行列表(至少一行,级联校验每行)
*/
@Valid
@NotEmpty(message = "消费明细不能为空")
private List<AiConsumeDetailParam> details;
}
......@@ -18,7 +18,7 @@ import java.util.Map;
public class AiRechargeRecordListParam extends PageQueryParam {
/**
* AI工具/平台名称(字典ai_tool_name的dict_value
* AI工具/平台名称(用户直接输入的自由文本
*/
private String toolName;
......@@ -32,6 +32,11 @@ public class AiRechargeRecordListParam extends PageQueryParam {
*/
private String rechargeCategory;
/**
* 归属部门id(部门树,含子部门)
*/
private Long belongDeptId;
private Map<String, Object> params = new HashMap<>();
}
......@@ -12,7 +12,6 @@ import com.anplus.hr.ai.domain.model.AiRechargeRecord;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* AI工具充值记录业务对象 ai_recharge_record
......@@ -30,7 +29,13 @@ public class AiRechargeRecordParam {
private Long id;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
* 所属部门ID(关联sys_dept.id)
*/
@NotNull(message = "所属部门不能为空")
private Long deptId;
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
@NotBlank(message = "AI工具/平台名称不能为空")
private String toolName;
......@@ -52,7 +57,7 @@ public class AiRechargeRecordParam {
* 充值完成时间
*/
@NotNull(message = "充值完成时间不能为空")
private LocalDateTime rechargeTime;
private LocalDate rechargeTime;
/**
* 到期时间(纯字段,无联动)
......
package com.anplus.hr.ai.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* AI工具消费记录合计视图对象(列表底部合计行 / GET /ai/consume/summary 载体)
*
* @author LiuBin
* @date 2026-09-08
*/
@Data
public class AiConsumeRecordSummaryVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 金额(人民币)合计:当前查询条件筛选出的全部结果(跨所有分页)amountCny 求和,保留两位小数
*/
private BigDecimal amountCny;
}
package com.anplus.hr.ai.domain.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.anplus.hr.ai.domain.model.AiConsumeRecord;
import top.binfast.common.excel.annotion.ExcelDictFormat;
import top.binfast.common.excel.converters.ExcelDictConvert;
import top.binfast.common.translation.annotation.Translation;
import top.binfast.common.translation.constant.TransConstant;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
......@@ -34,10 +37,31 @@ public class AiConsumeRecordVo implements Serializable {
private Long id;
/**
* 项目名称(字典ai_project_name的dict_value
* 批次号(主从批量录入元数据;非展示、非导出,前端编辑流按此定位整批
*/
@ExcelProperty(value = "项目名称", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "ai_project_name")
private String batchNo;
/**
* 行序号(批次内从 1 连续;非展示、非导出)
*/
private Integer lineSort;
/**
* 所属部门ID(关联sys_dept.id)
*/
private Long deptId;
/**
* 所属部门名称(@Translation 列表自动翻译;导出由 ServiceImpl 批量回填)
*/
@Translation(type = TransConstant.DEPT_ID_TO_NAME, mapper = "deptId")
@ExcelProperty(value = "所属部门")
private String deptName;
/**
* 项目名称(用户直接输入的自由文本)
*/
@ExcelProperty(value = "项目名称")
private String projectName;
/**
......@@ -47,10 +71,9 @@ public class AiConsumeRecordVo implements Serializable {
private String userName;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value
* AI工具/平台名称(用户直接输入的自由文本
*/
@ExcelProperty(value = "AI工具/平台名称", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "ai_tool_name")
@ExcelProperty(value = "AI工具/平台名称")
private String toolName;
/**
......@@ -75,7 +98,7 @@ public class AiConsumeRecordVo implements Serializable {
* 消费发生时间
*/
@ExcelProperty(value = "消费时间")
private LocalDateTime consumeTime;
private LocalDate consumeTime;
/**
* 备注
......@@ -85,4 +108,11 @@ public class AiConsumeRecordVo implements Serializable {
private LocalDateTime createTime;
/**
* 明细行列表(仅整批详情接口 GET /ai/consume/batch/{batchNo} 返回;
* 列表与单条详情为 null 且不参与 JSON 序列化;非展示、非导出)
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private List<AiConsumeRecordVo> details;
}
package com.anplus.hr.ai.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* AI工具充值记录合计视图对象(列表底部合计行 / GET /ai/recharge/summary 载体)
*
* @author LiuBin
* @date 2026-09-08
*/
@Data
public class AiRechargeRecordSummaryVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 充值金额(美元)合计:当前查询条件筛选出的全部结果(跨所有分页)amountUsd 求和,保留两位小数
*/
private BigDecimal amountUsd;
/**
* 充值金额(人民币)合计:当前查询条件筛选出的全部结果(跨所有分页)amountCny 求和,保留两位小数
*/
private BigDecimal amountCny;
}
......@@ -7,6 +7,8 @@ import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.anplus.hr.ai.domain.model.AiRechargeRecord;
import top.binfast.common.excel.annotion.ExcelDictFormat;
import top.binfast.common.excel.converters.ExcelDictConvert;
import top.binfast.common.translation.annotation.Translation;
import top.binfast.common.translation.constant.TransConstant;
import java.io.Serial;
import java.io.Serializable;
......@@ -35,10 +37,21 @@ public class AiRechargeRecordVo implements Serializable {
private Long id;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value
* 所属部门ID(关联sys_dept.id
*/
@ExcelProperty(value = "AI工具/平台名称", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "ai_tool_name")
private Long deptId;
/**
* 所属部门名称(@Translation 列表自动翻译;导出由 ServiceImpl 批量回填)
*/
@Translation(type = TransConstant.DEPT_ID_TO_NAME, mapper = "deptId")
@ExcelProperty(value = "所属部门")
private String deptName;
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
@ExcelProperty(value = "AI工具/平台名称")
private String toolName;
/**
......@@ -58,7 +71,7 @@ public class AiRechargeRecordVo implements Serializable {
* 充值完成时间
*/
@ExcelProperty(value = "充值完成时间")
private LocalDateTime rechargeTime;
private LocalDate rechargeTime;
/**
* 到期时间(纯字段,无联动)
......
package com.anplus.hr.ai.mapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.anplus.hr.ai.domain.model.AiConsumeRecord;
import top.binfast.common.mybatis.mapper.BinBaseMapper;
import java.util.List;
/**
* AI工具消费记录Mapper接口
*
......@@ -14,4 +18,29 @@ import top.binfast.common.mybatis.mapper.BinBaseMapper;
@Mapper
public interface AiConsumeRecordMapper extends BinBaseMapper<AiConsumeRecord>, MPJBaseMapper<AiConsumeRecord> {
/**
* 按批次号查询整批记录(line_sort 升序)。
*
* <p>租户过滤由全局 TenantLineInnerInterceptor 自动注入,逻辑删除行由 @TableLogic 自动排除。</p>
*
* @param batchNo 批次号
* @return 该批次下的全部有效记录,按行序号升序
*/
default List<AiConsumeRecord> selectByBatchNo(String batchNo) {
return this.selectList(new LambdaQueryWrapper<AiConsumeRecord>()
.eq(AiConsumeRecord::getBatchNo, batchNo)
.orderByAsc(AiConsumeRecord::getLineSort));
}
/**
* 按批次号物理删除整批记录(编辑「整批全删重插」用,绕过 @TableLogic 逻辑删除)。
*
* <p>SQL 显式带 tenant_id 条件(design.md D3 / R1),杜绝跨租户误删同 batch_no 行。</p>
*
* @param batchNo 批次号
* @param tenantId 当前租户ID
* @return 删除的行数
*/
int deleteByBatchNo(@Param("batchNo") String batchNo, @Param("tenantId") Long tenantId);
}
......@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.spring.service.IService;
import com.anplus.hr.ai.domain.model.AiConsumeRecord;
import com.anplus.hr.ai.domain.params.AiConsumeRecordListParam;
import com.anplus.hr.ai.domain.params.AiConsumeRecordParam;
import com.anplus.hr.ai.domain.vo.AiConsumeRecordSummaryVo;
import com.anplus.hr.ai.domain.vo.AiConsumeRecordVo;
import java.util.List;
......@@ -33,6 +34,14 @@ public interface AiConsumeRecordServ extends IService<AiConsumeRecord> {
*/
List<AiConsumeRecordVo> queryList(AiConsumeRecordListParam param);
/**
* 查询当前条件下的金额合计(跨所有分页的全量筛选结果,口径与列表完全一致)
*
* @param param 查询条件(与分页查询共用同一条件对象 AiConsumeRecordListParam)
* @return 合计载体(amountCny 保留两位小数;无匹配数据时为 0.00)
*/
AiConsumeRecordSummaryVo querySummary(AiConsumeRecordListParam param);
/**
* 查询AI工具消费记录
*
......@@ -41,6 +50,14 @@ public interface AiConsumeRecordServ extends IService<AiConsumeRecord> {
*/
AiConsumeRecordVo queryById(Long id);
/**
* 按批次号查询整批详情(公共字段 + 明细列表,line_sort 升序)
*
* @param batchNo 批次号
* @return 整批详情;批次不存在或跨租户返回 null
*/
AiConsumeRecordVo queryByBatchNo(String batchNo);
/**
* 新增AI工具消费记录
*
......
......@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.spring.service.IService;
import com.anplus.hr.ai.domain.model.AiRechargeRecord;
import com.anplus.hr.ai.domain.params.AiRechargeRecordListParam;
import com.anplus.hr.ai.domain.params.AiRechargeRecordParam;
import com.anplus.hr.ai.domain.vo.AiRechargeRecordSummaryVo;
import com.anplus.hr.ai.domain.vo.AiRechargeRecordVo;
import java.util.List;
......@@ -33,6 +34,14 @@ public interface AiRechargeRecordServ extends IService<AiRechargeRecord> {
*/
List<AiRechargeRecordVo> queryList(AiRechargeRecordListParam param);
/**
* 查询当前条件下的双金额合计(跨所有分页的全量筛选结果,口径与列表完全一致)
*
* @param param 查询条件(与分页查询共用同一条件对象 AiRechargeRecordListParam)
* @return 合计载体(amountUsd、amountCny 各保留两位小数;无匹配数据时为 0.00)
*/
AiRechargeRecordSummaryVo querySummary(AiRechargeRecordListParam param);
/**
* 查询AI工具充值记录
*
......
package com.anplus.hr.ai.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.cola.dto.PageResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
......@@ -9,16 +12,23 @@ import org.springframework.stereotype.Service;
import com.anplus.hr.ai.domain.model.AiRechargeRecord;
import com.anplus.hr.ai.domain.params.AiRechargeRecordListParam;
import com.anplus.hr.ai.domain.params.AiRechargeRecordParam;
import com.anplus.hr.ai.domain.vo.AiRechargeRecordSummaryVo;
import com.anplus.hr.ai.domain.vo.AiRechargeRecordVo;
import com.anplus.hr.ai.mapper.AiRechargeRecordMapper;
import com.anplus.hr.ai.service.AiRechargeRecordServ;
import top.binfast.app.biz.sysapi.dao.auth.SysDeptMapper;
import top.binfast.app.biz.sysbiz.service.SysDeptServ;
import top.binfast.common.core.util.MapstructUtils;
import top.binfast.common.mybatis.query.LambdaQueryBuilder;
import top.binfast.common.mybatis.query.QueryBuilder;
import top.binfast.common.mybatis.util.QueryUtil;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* AI工具充值记录Service业务层处理
......@@ -31,6 +41,8 @@ import java.util.Map;
public class AiRechargeRecordServImpl extends ServiceImpl<AiRechargeRecordMapper, AiRechargeRecord> implements AiRechargeRecordServ {
private final AiRechargeRecordMapper aiRechargeRecordMapper;
private final SysDeptMapper deptMapper;
private final SysDeptServ sysDeptServ;
/**
* 分页查询AI工具充值记录列表
......@@ -55,18 +67,81 @@ public class AiRechargeRecordServImpl extends ServiceImpl<AiRechargeRecordMapper
@Override
public List<AiRechargeRecordVo> queryList(AiRechargeRecordListParam param) {
LambdaQueryWrapper<AiRechargeRecord> lambdaQuery = this.buildQueryWrapper(param);
return MapstructUtils.convert(aiRechargeRecordMapper.selectList(lambdaQuery), AiRechargeRecordVo.class);
List<AiRechargeRecordVo> list = MapstructUtils.convert(aiRechargeRecordMapper.selectList(lambdaQuery), AiRechargeRecordVo.class);
//导出路径批量回填部门名:@Translation 仅在 JSON 响应增强管线生效,Excel 导出走 fesod 直接反射读字段,需手动回填
Set<Long> deptIds = list.stream()
.map(AiRechargeRecordVo::getDeptId)
.filter(ObjectUtil::isNotNull)
.collect(Collectors.toSet());
if (CollUtil.isNotEmpty(deptIds)) {
Map<Long, String> deptNames = sysDeptServ.selectDeptNamesByIds(deptIds);
list.forEach(vo -> vo.setDeptName(deptNames.get(vo.getDeptId())));
}
//导出末尾追加合计行:全量数据已在内存,两金额列各流式求和(filter nonNull 与 SQL SUM 忽略 NULL 语义一致),不发第二次 SQL(design D7)
BigDecimal amountUsdTotal = list.stream()
.map(AiRechargeRecordVo::getAmountUsd)
.filter(ObjectUtil::isNotNull)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal amountCnyTotal = list.stream()
.map(AiRechargeRecordVo::getAmountCny)
.filter(ObjectUtil::isNotNull)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.HALF_UP);
//合计行仅 deptName + 两金额有值,其余字段(含 rechargeCategory)保持 null:
//rechargeCategory=null 经 ExcelDictConvert null 分支落空单元格,不触发字典转换异常(design D7/Risks)
AiRechargeRecordVo totalRow = new AiRechargeRecordVo();
totalRow.setDeptName("合计");
totalRow.setAmountUsd(amountUsdTotal);
totalRow.setAmountCny(amountCnyTotal);
list.add(totalRow);
return list;
}
private LambdaQueryWrapper<AiRechargeRecord> buildQueryWrapper(AiRechargeRecordListParam param) {
/**
* 查询当前条件下的双金额合计(跨所有分页的全量筛选结果,口径与列表完全一致)。
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountUsd、amountCny 各保留两位小数;无匹配数据时为 0.00)
*/
@Override
public AiRechargeRecordSummaryVo querySummary(AiRechargeRecordListParam param) {
//amountUsd、amountCny 各构造一次聚合查询(均复用 buildCondition,不含 orderBy,规避 ONLY_FULL_GROUP_BY,design D3/D4)
List<Object> usdObjs = aiRechargeRecordMapper.selectObjs(
buildCondition(param).selectSum(AiRechargeRecord::getAmountUsd).build());
List<Object> cnyObjs = aiRechargeRecordMapper.selectObjs(
buildCondition(param).selectSum(AiRechargeRecord::getAmountCny).build());
//SUM 无 GROUP BY 恒返回单行;空结果集时该行为 null,落 ZERO(design D5)
BigDecimal amountUsd = CollUtil.isEmpty(usdObjs) ? BigDecimal.ZERO : Convert.toBigDecimal(usdObjs.get(0), BigDecimal.ZERO);
BigDecimal amountCny = CollUtil.isEmpty(cnyObjs) ? BigDecimal.ZERO : Convert.toBigDecimal(cnyObjs.get(0), BigDecimal.ZERO);
AiRechargeRecordSummaryVo summary = new AiRechargeRecordSummaryVo();
summary.setAmountUsd(amountUsd.setScale(2, RoundingMode.HALF_UP));
summary.setAmountCny(amountCny.setScale(2, RoundingMode.HALF_UP));
return summary;
}
/**
* 构造查询条件(不含排序),供列表查询与合计聚合共用,确保两者筛选口径完全一致(design D3)。
*
* @param param 查询条件
* @return 承载全部筛选条件(多租户 + 部门树子树 + 搜索条件 + 时间范围)的 builder,不含 orderBy
*/
private LambdaQueryBuilder<AiRechargeRecord> buildCondition(AiRechargeRecordListParam param) {
Map<String, Object> params = param.getParams();
LambdaQueryBuilder<AiRechargeRecord> builder = QueryBuilder.lambda(AiRechargeRecord.class)
return QueryBuilder.lambda(AiRechargeRecord.class)
.eqIfText(AiRechargeRecord::getToolName, param.getToolName())
.eqIfText(AiRechargeRecord::getRegion, param.getRegion())
.eqIfText(AiRechargeRecord::getRechargeCategory, param.getRechargeCategory())
.betweenParams(AiRechargeRecord::getRechargeTime, params, "beginTime", "endTime")
.orderByDesc(AiRechargeRecord::getRechargeTime);
return builder.build();
//部门树筛选(含子部门子树,对齐 SysUserServImpl.buildQueryWrapper)
.and(ObjectUtil.isNotNull(param.getBelongDeptId()), x -> {
List<Long> deptIds = deptMapper.selectDeptAndChildById(param.getBelongDeptId());
x.in(AiRechargeRecord::getDeptId, deptIds);
});
}
private LambdaQueryWrapper<AiRechargeRecord> buildQueryWrapper(AiRechargeRecordListParam param) {
return buildCondition(param).orderByDesc(AiRechargeRecord::getRechargeTime).build();
}
/**
......
......@@ -4,4 +4,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.anplus.hr.ai.mapper.AiConsumeRecordMapper">
<!--
按批次号物理删除整批(编辑「整批全删重插」用,绕过 @TableLogic 逻辑删除)。
显式带 tenant_id 条件(design.md D3 / R1),杜绝跨租户误删同 batch_no 行。
-->
<delete id="deleteByBatchNo">
delete from ai_consume_record
where batch_no = #{batchNo} and tenant_id = #{tenantId}
</delete>
</mapper>
import type { ID, IDS, PageQuery, PageResult } from '@/api/common';
import type { DeptTree } from '@/api/system/user/model';
import type { Consume } from './model';
import type { Consume, ConsumeForm, ConsumeSummary } from './model';
import { commonExport } from '@/api/helper';
import { alovaInstance } from '@/utils/http';
......@@ -8,6 +9,7 @@ import { alovaInstance } from '@/utils/http';
enum Api {
consumeExport = '/ai/consume/export',
consumeList = '/ai/consume/list',
consumeSummary = '/ai/consume/summary',
root = '/ai/consume',
}
......@@ -20,6 +22,15 @@ export function consumeList(params?: PageQuery) {
return alovaInstance.get<PageResult<Consume>>(Api.consumeList, { params });
}
/**
* 查询消费记录金额合计(跨所有分页的全量筛选结果,口径与列表一致)
* @param params 请求参数(与列表查询共用同一条件)
* @returns 合计载体(amountCny)
*/
export function consumeSummary(params?: PageQuery) {
return alovaInstance.get<ConsumeSummary>(Api.consumeSummary, { params });
}
/**
* 导出消费记录excel
* @param data 请求参数
......@@ -29,7 +40,7 @@ export function consumeExport(data: Partial<Consume>) {
}
/**
* 消费记录详情
* 消费记录详情(单条)
* @param id id
* @returns 详情
*/
......@@ -38,18 +49,27 @@ export function consumeInfo(id: ID) {
}
/**
* 消费记录新增
* @param data 参数
* 消费记录整批详情(主从批量录入编辑回填用)
* @param batchNo 批次号
* @returns 整批详情(公共字段 + details 明细列表)
*/
export function consumeBatchDetail(batchNo: string) {
return alovaInstance.get<Consume>(`${Api.root}/batch/${batchNo}`);
}
/**
* 消费记录新增(主从批量:公共字段 + details 明细数组)
* @param data 批量入参
*/
export function consumeAdd(data: Partial<Consume>) {
export function consumeAdd(data: ConsumeForm) {
return alovaInstance.postWithMsg<void>(Api.root, data);
}
/**
* 消费记录修改
* @param data 参数
* 消费记录修改(主从批量:整批全删重插)
* @param data 批量入参
*/
export function consumeUpdate(data: Partial<Consume>) {
export function consumeUpdate(data: ConsumeForm) {
return alovaInstance.putWithMsg<void>(Api.root, data);
}
......@@ -60,3 +80,11 @@ export function consumeUpdate(data: Partial<Consume>) {
export function consumeRemove(ids: IDS) {
return alovaInstance.deleteWithMsg<void>(`${Api.root}/${ids}`);
}
/**
* 消费记录专用 - 获取部门树
* @returns 部门树
*/
export function consumeDeptTreeSelect() {
return alovaInstance.get<DeptTree[]>('/ai/consume/deptTree');
}
......@@ -4,7 +4,27 @@
export interface Consume {
id: number;
/**
* 项目名称(字典ai_project_name的dict_value)
* 批次号(主从批量录入元数据;非展示、非导出,编辑流按此定位整批)
*/
batchNo?: string;
/**
* 行序号(批次内从 1 连续;非展示、非导出)
*/
lineSort?: number;
/**
* 所属部门ID
*/
deptId?: number;
/**
* 所属部门名称(后端 @Translation 翻译 / 导出回填)
*/
deptName?: string;
/**
* 归属部门id(部门树查询用,含子部门)
*/
belongDeptId?: number | string;
/**
* 项目名称(用户直接输入的自由文本)
*/
projectName: string;
/**
......@@ -12,7 +32,7 @@ export interface Consume {
*/
userName: string;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value
* AI工具/平台名称(用户直接输入的自由文本
*/
toolName: string;
/**
......@@ -36,4 +56,74 @@ export interface Consume {
*/
remark: string;
createTime: string;
/**
* 明细行列表(仅整批详情接口 GET /batch/{batchNo} 返回;列表与单条详情为 undefined)
*/
details?: Consume[];
}
/**
* @description: 消费明细行入参(对应后端 AiConsumeDetailParam)
*/
export interface ConsumeDetailForm {
/**
* AI工具/平台名称
*/
toolName?: string;
/**
* 国内/国外(固定二值:国内 | 国外)
*/
region?: string;
/**
* 消耗积分(非必填,最多 2 位小数)
*/
consumePoints?: number;
/**
* 金额(人民币,非必填,最多 2 位小数)
*/
amountCny?: number;
}
/**
* @description: 消费记录批量入参(主从结构,对应后端 AiConsumeRecordParam)
*/
export interface ConsumeForm {
/**
* 批次号(编辑时定位原批;新增时为空、由服务端生成)
*/
batchNo?: string;
/**
* 所属部门ID
*/
deptId?: number;
/**
* 项目名称(用户直接输入的自由文本)
*/
projectName?: string;
/**
* 使用人姓名
*/
userName?: string;
/**
* 消费发生时间(日期类型,落库 date 当天日期)
*/
consumeTime?: string;
/**
* 备注
*/
remark?: string;
/**
* 消费明细行列表(至少一行)
*/
details: ConsumeDetailForm[];
}
/**
* @description: 消费记录金额合计载体(对应后端 AiConsumeRecordSummaryVo)
*/
export interface ConsumeSummary {
/**
* 金额(人民币)全量合计(两位小数)
*/
amountCny: number;
}
import type { ID, IDS, PageQuery, PageResult } from '@/api/common';
import type { DeptTree } from '@/api/system/user/model';
import type { Recharge } from './model';
import type { Recharge, RechargeSummary } from './model';
import { commonExport } from '@/api/helper';
import { alovaInstance } from '@/utils/http';
......@@ -8,6 +9,7 @@ import { alovaInstance } from '@/utils/http';
enum Api {
rechargeExport = '/ai/recharge/export',
rechargeList = '/ai/recharge/list',
rechargeSummary = '/ai/recharge/summary',
root = '/ai/recharge',
}
......@@ -20,6 +22,15 @@ export function rechargeList(params?: PageQuery) {
return alovaInstance.get<PageResult<Recharge>>(Api.rechargeList, { params });
}
/**
* 查询充值记录双金额合计(跨所有分页的全量筛选结果,口径与列表一致)
* @param params 请求参数(与列表查询共用同一条件)
* @returns 合计载体(amountUsd、amountCny)
*/
export function rechargeSummary(params?: PageQuery) {
return alovaInstance.get<RechargeSummary>(Api.rechargeSummary, { params });
}
/**
* 导出充值记录excel
* @param data 请求参数
......@@ -60,3 +71,11 @@ export function rechargeUpdate(data: Partial<Recharge>) {
export function rechargeRemove(ids: IDS) {
return alovaInstance.deleteWithMsg<void>(`${Api.root}/${ids}`);
}
/**
* 充值记录专用 - 获取部门树
* @returns 部门树
*/
export function rechargeDeptTreeSelect() {
return alovaInstance.get<DeptTree[]>('/ai/recharge/deptTree');
}
......@@ -4,7 +4,19 @@
export interface Recharge {
id: number;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
* 所属部门ID
*/
deptId?: number;
/**
* 所属部门名称(后端 @Translation 翻译 / 导出回填)
*/
deptName?: string;
/**
* 归属部门id(部门树查询用,含子部门)
*/
belongDeptId?: number | string;
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
toolName: string;
/**
......@@ -49,3 +61,17 @@ export interface Recharge {
remark: string;
createTime: string;
}
/**
* @description: 充值记录双金额合计载体(对应后端 AiRechargeRecordSummaryVo)
*/
export interface RechargeSummary {
/**
* 充值金额(美元)全量合计(两位小数)
*/
amountUsd: number;
/**
* 充值金额(人民币)全量合计(两位小数)
*/
amountCny: number;
}
import type { GenerateMenuAndRoutesOptions, RouteRecordRaw } from '@/types';
import type { GenerateMenuAndRoutesOptions } from '@/types';
import type { Component, DefineComponent } from 'vue';
......@@ -8,7 +8,6 @@ import {
cloneDeep,
generateMenus,
generateRoutesByBackend,
generateRoutesByFrontend,
isFunction,
isString,
mapTree,
......@@ -80,16 +79,11 @@ async function generateAccessible(options: GenerateMenuAndRoutesOptions) {
* @param options
*/
async function generateRoutes(options: GenerateMenuAndRoutesOptions) {
const { forbiddenComponent, roles, routes } = options;
const [frontendResultRoutes, backendResultRoutes] = await Promise.all([
generateRoutesByFrontend(routes, roles || [], forbiddenComponent),
generateRoutesByBackend(options),
]);
let resultRoutes = mergeRoutesByName(
backendResultRoutes,
frontendResultRoutes,
);
// 本项目采用「后端权限模式」:后端 /auth/menu/getRouters 返回当前用户授权的路由
// name 集合,fetchMenuListAsync 已基于前端路由表完成过滤与组件解析,故直接以后端
// 结果为唯一真相源,不再并入前端模式。(roles 为空时 generateRoutesByFrontend 会
// 返回全量路由表,经合并会把后端已过滤掉的无权限路由又加回来,导致越权显示全部菜单。)
let resultRoutes = await generateRoutesByBackend(options);
/**
* 调整路由树,做以下处理:
......@@ -146,63 +140,4 @@ async function generateRoutes(options: GenerateMenuAndRoutesOptions) {
return resultRoutes;
}
/**
* 根据 name 合并前后端路由
* @param baseRoutes 后端路由
* @param extraRoutes 前端路由
*/
function mergeRoutesByName(
baseRoutes: RouteRecordRaw[],
extraRoutes: RouteRecordRaw[],
): RouteRecordRaw[] {
const result: RouteRecordRaw[] = [];
const routeMap = new Map<string, RouteRecordRaw>();
for (const route of baseRoutes) {
const clone = { ...route } as RouteRecordRaw;
result.push(clone);
if (clone.name && isString(clone.name)) {
routeMap.set(clone.name as string, clone);
}
}
for (const route of extraRoutes) {
if (
route.name &&
isString(route.name) &&
routeMap.has(route.name as string)
) {
const existing = routeMap.get(route.name as string);
if (!existing) {
continue;
}
const existingChildren = existing.children ?? [];
const routeChildren = route.children ?? [];
const merged = {
...route,
...existing, // keep backend as base
meta: {
...route.meta,
...existing.meta, // backend meta wins on conflicts
},
} as RouteRecordRaw;
if (existingChildren.length > 0 || routeChildren.length > 0) {
merged.children = mergeRoutesByName(existingChildren, routeChildren);
}
Object.assign(existing, merged);
} else {
const clone = { ...route } as RouteRecordRaw;
result.push(clone);
if (clone.name && isString(clone.name)) {
routeMap.set(clone.name as string, clone);
}
}
}
return result;
}
export { generateAccessible };
export const DictEnum = {
AI_PROJECT_NAME: 'ai_project_name', // 项目名称
AI_RECHARGE_CATEGORY: 'ai_recharge_category', // 充值分类
AI_TOOL_NAME: 'ai_tool_name', // AI工具/平台名称
SYS_COMMON_STATUS: 'sys_common_status',
SYS_DEVICE_TYPE: 'sys_device_type', // 设备类型
SYS_GRANT_TYPE: 'sys_grant_type', // 授权类型
......
......@@ -219,23 +219,25 @@ function backMenuToVbenMenuV2(
let componentPath = '';
// 如果是动态组件或异步加载函数,可以提取路径字符串
if (typeof component === 'function') {
try {
// 获取 () => import('xxx') 中的路径字符串
// componentPath =
// component.toString().match(/import\("(.*)"\)/)?.[1] || '';
const match = component.toString().match(/import\("(.*)"\)/);
if (match && match[1]) {
// 替换 '/src' 为 '../views'
// componentPath = match[1].replace('/src', '..');
componentPath = meta?.componentPath
? (meta.componentPath as string).replace('@/', '../')
: match[1].replace('/src', '..');
if (componentPath.includes('?t=')) {
componentPath = componentPath.split('?')[0] || '';
// 优先使用 meta.componentPath:路由静态声明,dev/生产一致可靠,
// 转换后(@/ → ../)正好匹配 import.meta.glob('../views/**/*.vue') 的 key。
if (meta?.componentPath) {
componentPath = (meta.componentPath as string).replace('@/', '../');
} else {
// 回退:解析 () => import('xxx') 的源码字符串。
// 生产环境经 Rollup 转译后 component.toString() 不再是源码形式,
// 该回退仅在 dev 可靠,故动态路由务必显式配置 meta.componentPath。
try {
const match = component.toString().match(/import\("(.*)"\)/);
if (match && match[1]) {
componentPath = match[1].replace('/src', '..');
}
} catch {
console.warn(`无法解析组件路径:${String(route.name)}`);
}
} catch {
console.warn(`无法解析组件路径:${String(route.name)}`);
}
if (componentPath.includes('?t=')) {
componentPath = componentPath.split('?')[0] || '';
}
}
......
......@@ -25,6 +25,7 @@ const routes: RouteRecordRaw[] = [
meta: {
affixTab: true,
title: $t('page.dashboard.analytics'),
componentPath: '@/views/dashboard/analytics/index.vue',
},
},
{
......@@ -48,6 +49,7 @@ const routes: RouteRecordRaw[] = [
title: '更新日志',
badge: `当前: ${version}`,
badgeVariants: 'bg-primary',
componentPath: '@/views/_core/changelog/index.vue',
},
},
],
......@@ -58,6 +60,7 @@ const routes: RouteRecordRaw[] = [
icon: 'lucide:copyright',
order: 9999,
title: $t('demos.vben.about'),
componentPath: '@/views/_core/about/index.vue',
},
name: 'About',
path: '/vben-admin/about',
......
......@@ -9,6 +9,7 @@ const routes: RouteRecordRaw[] = [
title: $t('ui.widgets.profile'),
hideInMenu: true,
requireHomeRedirect: true,
componentPath: '@/views/_core/profile/index.vue',
},
name: 'Profile',
path: '/profile',
......
......@@ -43,8 +43,8 @@ const rememberMe = ref(!!localUsername);
const formState = reactive({
tenantId: DEFAULT_TENANT_ID,
username: localUsername || 'admin',
password: '111111',
username: localUsername || '',
password: '',
code: '',
});
......
......@@ -4,12 +4,10 @@ import type { Dayjs } from 'dayjs';
import { ref } from 'vue';
import { FormInput, FormSelect } from '@/components/global/form';
import { FormInput } from '@/components/global/form';
import { SearchButtonGroup } from '@/components/table';
import { tableSeachClass } from '@/components/vxe-table';
import { DictEnum } from '@/constants';
import { formatDateTime } from '@/utils';
import { getDictOptions } from '@/utils/dict';
import { formatDate } from '@/utils';
import { Card, DateRangePicker, Form, FormItem } from 'antdv-next';
const emit = defineEmits<{
......@@ -38,8 +36,9 @@ function buildSearchParams(values: ConsumeSearchFormParams) {
const params: Record<string, any> = { ...values };
if (params.consumeTime) {
params.params = {
beginTime: formatDateTime(params.consumeTime[0]),
endTime: formatDateTime(params.consumeTime[1]),
// date 列 BETWEEN 两端均为纯日期,天然含结束日当天(无需再补 00:00:00/23:59:59)
beginTime: formatDate(params.consumeTime[0], 'YYYY-MM-DD'),
endTime: formatDate(params.consumeTime[1], 'YYYY-MM-DD'),
};
Reflect.deleteProperty(params, 'consumeTime');
}
......@@ -77,25 +76,13 @@ defineExpose({
<div :class="tableSeachClass">
<template v-if="!searchCollapsed">
<FormItem label="项目名称" name="projectName">
<FormSelect
allow-clear
option-filter-prop="label"
show-search
v-model:value="model.projectName"
:options="getDictOptions(DictEnum.AI_PROJECT_NAME)"
/>
<FormInput v-model:value="model.projectName" allow-clear />
</FormItem>
<FormItem label="姓名" name="userName">
<FormInput v-model:value="model.userName" allow-clear />
</FormItem>
<FormItem label="AI工具/平台" name="toolName">
<FormSelect
allow-clear
option-filter-prop="label"
show-search
v-model:value="model.toolName"
:options="getDictOptions(DictEnum.AI_TOOL_NAME)"
/>
<FormInput v-model:value="model.toolName" allow-clear />
</FormItem>
<FormItem label="消费时间" name="consumeTime">
<DateRangePicker v-model:value="model.consumeTime" allow-clear />
......
import type { VxeGridProps } from 'vxe-table';
import { DictEnum } from '@/constants';
import { renderDict } from '@/utils/render';
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
title: '所属部门',
field: 'deptName',
},
{
title: '项目名称',
field: 'projectName',
slots: {
default: ({ row }) => {
return renderDict(row.projectName, DictEnum.AI_PROJECT_NAME);
},
},
},
{
title: '姓名',
......@@ -21,11 +17,6 @@ export const columns: VxeGridProps['columns'] = [
{
title: 'AI工具/平台名称',
field: 'toolName',
slots: {
default: ({ row }) => {
return renderDict(row.toolName, DictEnum.AI_TOOL_NAME);
},
},
},
{
title: '国内/国外',
......
......@@ -4,7 +4,13 @@ import type { VxeGridInstance, VxeGridListeners } from 'vxe-table';
import { ref, useTemplateRef } from 'vue';
import { consumeExport, consumeList, consumeRemove } from '@/api/ai/consume';
import {
consumeDeptTreeSelect,
consumeExport,
consumeList,
consumeRemove,
consumeSummary,
} from '@/api/ai/consume';
import { Page, useVbenDrawer } from '@/components';
import {
resolveQueryFormValues,
......@@ -12,17 +18,26 @@ import {
withDefaultVxeGridOptions,
} from '@/components/vxe-table';
import { useBlobExport } from '@/utils/file/export';
import DeptTree from '@/views/system/user/dept-tree.vue';
import { Popconfirm, Space, Spin } from 'antdv-next';
import { VxeGrid } from 'vxe-table';
import { columns } from './data';
import consumeDrawer from './consume-drawer.vue';
import ConsumeSearchForm from './consume-search.vue';
import { columns } from './data';
// 左边部门用
const selectDeptId = ref<string[]>([]);
const searchFormRef = ref<InstanceType<typeof ConsumeSearchForm>>();
// 缓存最近一次搜索参数,部门树切换时重新查询用
const currentSearchParams = ref<Record<string, any>>({});
const tableLoading = ref(false);
// 列表底部合计行数据(独立 ref,不写回 gridOptions,规避 TS 自引用循环类型推断,design D6)
const footerData = ref<Record<string, any>[]>([]);
const gridOptions = withDefaultVxeGridOptions<Consume>({
checkboxConfig: {
// 高亮
......@@ -42,11 +57,27 @@ const gridOptions = withDefaultVxeGridOptions<Consume>({
const values = await resolveQueryFormValues(searchFormRef, formValues);
tableLoading.value = true;
try {
return await consumeList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...values,
});
// 部门树选择处理
if (selectDeptId.value.length === 1) {
values.belongDeptId = selectDeptId.value[0];
} else {
Reflect.deleteProperty(values, 'belongDeptId');
}
// 列表与合计并行拉取:合计走独立接口、跨页全量,口径与列表完全一致(design D1/D6)
const [listRes, sum] = await Promise.all([
consumeList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...values,
}),
consumeSummary(values),
]);
// 合计写入独立 footerData;键名按 data.ts 列 field 对齐;toFixed(2) 规避 JSON 数值退化(design D6)
footerData.value = [
{ deptName: '合计', amountCny: Number(sum.amountCny).toFixed(2) },
];
return listRes;
} finally {
tableLoading.value = false;
}
......@@ -56,6 +87,8 @@ const gridOptions = withDefaultVxeGridOptions<Consume>({
rowConfig: {
keyField: 'id',
},
// footer 页面局部开启(不改全局 vxe 配置,design D2)
showFooter: true,
toolbarConfig: {
slots: {
buttons: 'toolbar-left',
......@@ -88,7 +121,7 @@ function handleAdd() {
}
async function handleEdit(record: Consume) {
drawerApi.setData({ id: record.id });
drawerApi.setData({ batchNo: record.batchNo, id: record.id });
drawerApi.open();
}
......@@ -121,19 +154,33 @@ const { exportBlob, exportLoading, buildExportFileName } =
async function handleExport() {
// 构建表单请求参数
const formValues = (await searchFormRef.value?.getValues()) ?? {};
// 部门树选择处理(与 query 口径一致,导出携带 belongDeptId)
if (selectDeptId.value.length === 1) {
formValues.belongDeptId = selectDeptId.value[0];
} else {
Reflect.deleteProperty(formValues, 'belongDeptId');
}
// 文件名
const fileName = buildExportFileName('消费记录数据');
exportBlob({ data: formValues, fileName });
}
function handleSearchSubmit(data: Record<string, any>) {
currentSearchParams.value = data;
reload(data);
}
function handleSearchReset() {
currentSearchParams.value = {};
selectDeptId.value = [];
reload();
}
function handleDeptSelect(keys: string[]) {
selectDeptId.value = keys;
reload(currentSearchParams.value);
}
function getCheckedRows() {
const table = tableRef.value;
if (!table) {
......@@ -158,77 +205,87 @@ function syncCheckedRows() {
size="large"
:delay="300"
>
<div class="flex h-full flex-col gap-2">
<ConsumeSearchForm
ref="searchFormRef"
@submit="handleSearchSubmit"
@reset="handleSearchReset"
<div class="flex h-full gap-[8px]">
<DeptTree
:api="consumeDeptTreeSelect"
v-model:select-dept-id="selectDeptId"
class="w-[260px]"
@reload="() => reload()"
@select="handleDeptSelect"
/>
<div class="bg-card flex-1 overflow-hidden rounded-lg">
<VxeGrid
ref="tableRef"
class="p-2 pt-0"
v-bind="gridOptions"
v-on="gridEvents"
>
<template #toolbar-left>
<div class="text-[16px] font-medium">消费记录</div>
</template>
<template #toolbar-right>
<Space>
<a-button
v-access:code="['ai:consume:export']"
:loading="exportLoading"
:disabled="exportLoading"
@click="handleExport"
>
{{ $t('pages.common.export') }}
</a-button>
<a-button
:disabled="checkedRows.length === 0"
danger
type="primary"
v-access:code="['ai:consume:remove']"
@click="handleMultiDelete"
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button
type="primary"
v-access:code="['ai:consume:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #action="{ row }">
<Space>
<action-button
v-access:code="['ai:consume:edit']"
@click="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</action-button>
<Popconfirm
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<action-button
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
<ConsumeSearchForm
ref="searchFormRef"
@submit="handleSearchSubmit"
@reset="handleSearchReset"
/>
<div class="bg-card flex-1 overflow-hidden rounded-lg">
<VxeGrid
ref="tableRef"
class="p-2 pt-0"
v-bind="gridOptions"
:footer-data="footerData"
v-on="gridEvents"
>
<template #toolbar-left>
<div class="text-[16px] font-medium">消费记录</div>
</template>
<template #toolbar-right>
<Space>
<a-button
v-access:code="['ai:consume:export']"
:loading="exportLoading"
:disabled="exportLoading"
@click="handleExport"
>
{{ $t('pages.common.export') }}
</a-button>
<a-button
:disabled="checkedRows.length === 0"
danger
type="primary"
v-access:code="['ai:consume:remove']"
@click.stop=""
@click="handleMultiDelete"
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button
type="primary"
v-access:code="['ai:consume:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #action="{ row }">
<Space>
<action-button
v-access:code="['ai:consume:edit']"
@click="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</action-button>
</Popconfirm>
</Space>
</template>
<template #loading>
<Spin :spinning="true" size="large" />
</template>
</VxeGrid>
<Popconfirm
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<action-button
danger
v-access:code="['ai:consume:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</action-button>
</Popconfirm>
</Space>
</template>
<template #loading>
<Spin :spinning="true" size="large" />
</template>
</VxeGrid>
</div>
</div>
</div>
</Spin>
......
......@@ -5,14 +5,13 @@ import { renderDict } from '@/utils/render';
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
title: '所属部门',
field: 'deptName',
},
{
title: 'AI工具/平台名称',
field: 'toolName',
slots: {
default: ({ row }) => {
return renderDict(row.toolName, DictEnum.AI_TOOL_NAME);
},
},
},
{
title: '国内/国外',
......
......@@ -4,7 +4,13 @@ import type { VxeGridInstance, VxeGridListeners } from 'vxe-table';
import { ref, useTemplateRef } from 'vue';
import { rechargeExport, rechargeList, rechargeRemove } from '@/api/ai/recharge';
import {
rechargeDeptTreeSelect,
rechargeExport,
rechargeList,
rechargeRemove,
rechargeSummary,
} from '@/api/ai/recharge';
import { Page, useVbenDrawer } from '@/components';
import {
resolveQueryFormValues,
......@@ -12,6 +18,7 @@ import {
withDefaultVxeGridOptions,
} from '@/components/vxe-table';
import { useBlobExport } from '@/utils/file/export';
import DeptTree from '@/views/system/user/dept-tree.vue';
import { Popconfirm, Space, Spin } from 'antdv-next';
import { VxeGrid } from 'vxe-table';
......@@ -19,10 +26,18 @@ import { columns } from './data';
import rechargeDrawer from './recharge-drawer.vue';
import RechargeSearchForm from './recharge-search.vue';
// 左边部门用
const selectDeptId = ref<string[]>([]);
const searchFormRef = ref<InstanceType<typeof RechargeSearchForm>>();
// 缓存最近一次搜索参数,部门树切换时重新查询用
const currentSearchParams = ref<Record<string, any>>({});
const tableLoading = ref(false);
// 列表底部合计行数据(独立 ref,不写回 gridOptions,规避 TS 自引用循环类型推断,design D6)
const footerData = ref<Record<string, any>[]>([]);
const gridOptions = withDefaultVxeGridOptions<Recharge>({
checkboxConfig: {
// 高亮
......@@ -42,11 +57,31 @@ const gridOptions = withDefaultVxeGridOptions<Recharge>({
const values = await resolveQueryFormValues(searchFormRef, formValues);
tableLoading.value = true;
try {
return await rechargeList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...values,
});
// 部门树选择处理
if (selectDeptId.value.length === 1) {
values.belongDeptId = selectDeptId.value[0];
} else {
Reflect.deleteProperty(values, 'belongDeptId');
}
// 列表与合计并行拉取:合计走独立接口、跨页全量,口径与列表完全一致(design D1/D6)
const [listRes, sum] = await Promise.all([
rechargeList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...values,
}),
rechargeSummary(values),
]);
// 合计写入独立 footerData;两金额键按 data.ts 列 field 对齐;toFixed(2) 规避 JSON 数值退化(design D6)
footerData.value = [
{
deptName: '合计',
amountUsd: Number(sum.amountUsd).toFixed(2),
amountCny: Number(sum.amountCny).toFixed(2),
},
];
return listRes;
} finally {
tableLoading.value = false;
}
......@@ -56,6 +91,8 @@ const gridOptions = withDefaultVxeGridOptions<Recharge>({
rowConfig: {
keyField: 'id',
},
// footer 页面局部开启(不改全局 vxe 配置,design D2)
showFooter: true,
toolbarConfig: {
slots: {
buttons: 'toolbar-left',
......@@ -121,19 +158,33 @@ const { exportBlob, exportLoading, buildExportFileName } =
async function handleExport() {
// 构建表单请求参数
const formValues = (await searchFormRef.value?.getValues()) ?? {};
// 部门树选择处理(与 query 口径一致,导出携带 belongDeptId)
if (selectDeptId.value.length === 1) {
formValues.belongDeptId = selectDeptId.value[0];
} else {
Reflect.deleteProperty(formValues, 'belongDeptId');
}
// 文件名
const fileName = buildExportFileName('充值记录数据');
exportBlob({ data: formValues, fileName });
}
function handleSearchSubmit(data: Record<string, any>) {
currentSearchParams.value = data;
reload(data);
}
function handleSearchReset() {
currentSearchParams.value = {};
selectDeptId.value = [];
reload();
}
function handleDeptSelect(keys: string[]) {
selectDeptId.value = keys;
reload(currentSearchParams.value);
}
function getCheckedRows() {
const table = tableRef.value;
if (!table) {
......@@ -158,77 +209,87 @@ function syncCheckedRows() {
size="large"
:delay="300"
>
<div class="flex h-full flex-col gap-2">
<RechargeSearchForm
ref="searchFormRef"
@submit="handleSearchSubmit"
@reset="handleSearchReset"
<div class="flex h-full gap-[8px]">
<DeptTree
:api="rechargeDeptTreeSelect"
v-model:select-dept-id="selectDeptId"
class="w-[260px]"
@reload="() => reload()"
@select="handleDeptSelect"
/>
<div class="bg-card flex-1 overflow-hidden rounded-lg">
<VxeGrid
ref="tableRef"
class="p-2 pt-0"
v-bind="gridOptions"
v-on="gridEvents"
>
<template #toolbar-left>
<div class="text-[16px] font-medium">充值记录</div>
</template>
<template #toolbar-right>
<Space>
<a-button
v-access:code="['ai:recharge:export']"
:loading="exportLoading"
:disabled="exportLoading"
@click="handleExport"
>
{{ $t('pages.common.export') }}
</a-button>
<a-button
:disabled="checkedRows.length === 0"
danger
type="primary"
v-access:code="['ai:recharge:remove']"
@click="handleMultiDelete"
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button
type="primary"
v-access:code="['ai:recharge:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #action="{ row }">
<Space>
<action-button
v-access:code="['ai:recharge:edit']"
@click="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</action-button>
<Popconfirm
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<action-button
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
<RechargeSearchForm
ref="searchFormRef"
@submit="handleSearchSubmit"
@reset="handleSearchReset"
/>
<div class="bg-card flex-1 overflow-hidden rounded-lg">
<VxeGrid
ref="tableRef"
class="p-2 pt-0"
v-bind="gridOptions"
:footer-data="footerData"
v-on="gridEvents"
>
<template #toolbar-left>
<div class="text-[16px] font-medium">充值记录</div>
</template>
<template #toolbar-right>
<Space>
<a-button
v-access:code="['ai:recharge:export']"
:loading="exportLoading"
:disabled="exportLoading"
@click="handleExport"
>
{{ $t('pages.common.export') }}
</a-button>
<a-button
:disabled="checkedRows.length === 0"
danger
type="primary"
v-access:code="['ai:recharge:remove']"
@click.stop=""
@click="handleMultiDelete"
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button
type="primary"
v-access:code="['ai:recharge:add']"
@click="handleAdd"
>
{{ $t('pages.common.add') }}
</a-button>
</Space>
</template>
<template #action="{ row }">
<Space>
<action-button
v-access:code="['ai:recharge:edit']"
@click="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</action-button>
</Popconfirm>
</Space>
</template>
<template #loading>
<Spin :spinning="true" size="large" />
</template>
</VxeGrid>
<Popconfirm
placement="left"
title="确认删除?"
@confirm="handleDelete(row)"
>
<action-button
danger
v-access:code="['ai:recharge:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</action-button>
</Popconfirm>
</Space>
</template>
<template #loading>
<Spin :spinning="true" size="large" />
</template>
</VxeGrid>
</div>
</div>
</div>
</Spin>
......
<script setup lang="ts">
import type { Recharge } from '@/api/ai/recharge/model';
import type { DeptTree } from '@/api/system/user/model';
import type { AntdFormRules } from '@/types/form';
import type { FormInstance } from 'antdv-next';
import { computed, ref } from 'vue';
import { rechargeAdd, rechargeInfo, rechargeUpdate } from '@/api/ai/recharge';
import { getDeptTree } from '@/api/system/user';
import { useVbenDrawer } from '@/components';
import {
FormInput as Input,
FormInputNumber as InputNumber,
FormSelect as Select,
FormTextArea as TextArea,
FormTreeSelect as TreeSelect,
} from '@/components/global/form';
import { DictEnum } from '@/constants';
import { $t } from '@/locales';
import { cloneDeep, getPopupContainer } from '@/utils';
import { addFullName, cloneDeep, getPopupContainer } from '@/utils';
import { getDictOptions } from '@/utils/dict';
import { useBeforeCloseDiff } from '@/utils/popup';
import { DatePicker, Form, FormItem } from 'antdv-next';
......@@ -26,7 +30,9 @@ const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
type FormData = Partial<Recharge>;
type FormData = Partial<Recharge> & {
deptId?: number | string;
};
// 国内/国外为固定二值(非字典,Clarify C2)
const regionOptions = [
......@@ -37,6 +43,7 @@ const regionOptions = [
function getDefaultValues(): FormData {
return {
id: undefined,
deptId: undefined,
toolName: undefined,
region: undefined,
rechargeCategory: undefined,
......@@ -53,9 +60,11 @@ function getDefaultValues(): FormData {
const formData = ref<FormData>(getDefaultValues());
const formInstance = ref<FormInstance>();
const deptTreeData = ref<DeptTree[]>([]);
const formRules = ref<AntdFormRules<FormData>>({
toolName: [{ required: true, message: $t('ui.formRules.selectRequired') }],
deptId: [{ required: true, message: $t('ui.formRules.selectRequired') }],
toolName: [{ required: true, message: $t('ui.formRules.required') }],
region: [{ required: true, message: $t('ui.formRules.selectRequired') }],
rechargeCategory: [
{ required: true, message: $t('ui.formRules.selectRequired') },
......@@ -64,6 +73,13 @@ const formRules = ref<AntdFormRules<FormData>>({
amountCny: [{ required: true, message: $t('ui.formRules.required') }],
});
async function setupDeptSelect() {
const deptTree = await getDeptTree();
// 选中后显示在输入框的值 即父节点 / 子节点
addFullName(deptTree, 'label', ' / ');
deptTreeData.value = deptTree;
}
function customFormValueGetter() {
return JSON.stringify(formData.value);
}
......@@ -86,6 +102,8 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id;
// 初始化部门树
await setupDeptSelect();
// 更新 && 赋值
if (isUpdate.value && id) {
const record = await rechargeInfo(id);
......@@ -119,6 +137,7 @@ async function handleConfirm() {
async function handleClosed() {
formData.value = getDefaultValues();
formInstance.value?.resetFields();
deptTreeData.value = [];
resetInitialized();
}
</script>
......@@ -130,14 +149,25 @@ async function handleClosed() {
:model="formData"
:label-col="{ style: { width: '140px' } }"
>
<FormItem label="AI工具/平台名称" name="toolName" :rules="formRules.toolName">
<Select
<FormItem label="所属部门" name="deptId" :rules="formRules.deptId">
<TreeSelect
allow-clear
class="w-full"
:allow-clear="false"
option-filter-prop="label"
show-search
:field-names="{ label: 'label', value: 'id', children: 'children' }"
:get-popup-container="getPopupContainer"
:options="getDictOptions(DictEnum.AI_TOOL_NAME)"
show-search
:tree-data="deptTreeData"
tree-default-expand-all
:tree-line="{ showLeafIcon: false }"
tree-node-filter-prop="label"
tree-node-label-prop="fullName"
v-model:value="formData.deptId"
/>
</FormItem>
<FormItem label="AI工具/平台名称" name="toolName" :rules="formRules.toolName">
<Input
class="w-full"
allow-clear
v-model:value="formData.toolName"
/>
</FormItem>
......@@ -170,9 +200,8 @@ async function handleClosed() {
>
<DatePicker
class="w-full"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
v-model:value="formData.rechargeTime"
/>
</FormItem>
......
......@@ -4,11 +4,11 @@ import type { Dayjs } from 'dayjs';
import { ref } from 'vue';
import { FormSelect } from '@/components/global/form';
import { FormInput, FormSelect } from '@/components/global/form';
import { SearchButtonGroup } from '@/components/table';
import { tableSeachClass } from '@/components/vxe-table';
import { DictEnum } from '@/constants';
import { formatDateTime } from '@/utils';
import { formatDate } from '@/utils';
import { getDictOptions } from '@/utils/dict';
import { Card, DateRangePicker, Form, FormItem } from 'antdv-next';
......@@ -44,8 +44,9 @@ function buildSearchParams(values: RechargeSearchFormParams) {
const params: Record<string, any> = { ...values };
if (params.rechargeTime) {
params.params = {
beginTime: formatDateTime(params.rechargeTime[0]),
endTime: formatDateTime(params.rechargeTime[1]),
// date 列 BETWEEN 两端均为纯日期,天然含结束日当天(无需再补 00:00:00/23:59:59)
beginTime: formatDate(params.rechargeTime[0], 'YYYY-MM-DD'),
endTime: formatDate(params.rechargeTime[1], 'YYYY-MM-DD'),
};
Reflect.deleteProperty(params, 'rechargeTime');
}
......@@ -86,13 +87,7 @@ defineExpose({
<DateRangePicker v-model:value="model.rechargeTime" allow-clear />
</FormItem>
<FormItem label="AI工具/平台" name="toolName">
<FormSelect
allow-clear
option-filter-prop="label"
show-search
v-model:value="model.toolName"
:options="getDictOptions(DictEnum.AI_TOOL_NAME)"
/>
<FormInput v-model:value="model.toolName" allow-clear />
</FormItem>
<FormItem label="国内/国外" name="region">
<FormSelect
......
......@@ -410,7 +410,7 @@ async function handleCopyConfig() {
/>
</FormItem>
<FormItem
v-if="showMenu"
v-if="showNotButton"
label="页面名称"
name="pageName"
:rules="formRules.pageName"
......
......@@ -25,7 +25,7 @@ const tabOptions = [
];
interface RolePermissionData {
roleId?: number | string;
id?: number | string;
roleKey?: string;
roleName?: string;
// 菜单权限
......@@ -91,7 +91,7 @@ const [BasicModal, modalApi] = useVbenModal({
]);
formData.value = {
roleId: record.id,
id: record.id,
roleName: record.roleName,
roleKey: record.roleKey,
menuCheckStrictly: record.menuCheckStrictly,
......
......@@ -19,7 +19,7 @@
<mapstruct-plus.version>1.5.1</mapstruct-plus.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
<binfast.version>2.0.2</binfast.version>
<binfast.version>2.0.3</binfast.version>
<lombok.version>1.18.46</lombok.version>
</properties>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment