Commit b54c4d35 authored by 刘斌's avatar 刘斌

feat: 增加AI管理的充值记录、消费记录

parent c8b316c7
package com.anplus.hr.ai.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.alibaba.cola.dto.PageResponse;
import com.alibaba.cola.dto.Response;
import com.alibaba.cola.dto.SingleResponse;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
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.AiConsumeRecordVo;
import com.anplus.hr.ai.service.AiConsumeRecordServ;
import top.binfast.common.core.constant.BusinessType;
import top.binfast.common.core.util.ResponseUtils;
import top.binfast.common.excel.annotion.ExcelExport;
import top.binfast.common.log.annotation.PinSysLog;
import java.util.List;
/**
* AI工具消费记录
*
* @author LiuBin
* @date 2026-09-04
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/consume")
public class AiConsumeRecordCtrl {
private final AiConsumeRecordServ aiConsumeRecordServ;
/**
* 分页查询AI工具消费记录列表。
*
* @param param 查询条件
* @return AI工具消费记录分页结果
*/
@SaCheckPermission("ai:consume:list")
@GetMapping("/list")
public PageResponse<AiConsumeRecordVo> pageList(AiConsumeRecordListParam param) {
return aiConsumeRecordServ.queryPageList(param);
}
/**
* 导出AI工具消费记录列表。
*
* @param param 查询条件
*/
@ExcelExport
@SaCheckPermission("ai:consume:export")
@PinSysLog(value = "AI工具消费记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public List<AiConsumeRecordVo> export(AiConsumeRecordListParam param) {
return aiConsumeRecordServ.queryList(param);
}
/**
* 获取AI工具消费记录详细信息
*
* @param id 主键
*/
@SaCheckPermission("ai:consume:query")
@GetMapping("/{id}")
public SingleResponse<AiConsumeRecordVo> getDetail(@PathVariable @Min(1)
Long id) {
return SingleResponse.of(aiConsumeRecordServ.queryById(id));
}
/**
* 新增AI工具消费记录。
*
* @param param AI工具消费记录参数
* @return 操作结果
*/
@SaCheckPermission("ai:consume:add")
@PinSysLog(value = "AI工具消费记录", businessType = BusinessType.INSERT)
@PostMapping()
public Response add(@Validated @RequestBody AiConsumeRecordParam param) {
return ResponseUtils.ofResult(aiConsumeRecordServ.insertByParam(param));
}
/**
* 修改AI工具消费记录。
*
* @param param AI工具消费记录参数
* @return 操作结果
*/
@SaCheckPermission("ai:consume:edit")
@PinSysLog(value = "AI工具消费记录", businessType = BusinessType.UPDATE)
@PutMapping()
public Response edit(@Validated @RequestBody AiConsumeRecordParam param) {
return ResponseUtils.ofResult(aiConsumeRecordServ.updateByParam(param));
}
/**
* 删除AI工具消费记录
*
* @param ids 主键串
*/
@SaCheckPermission("ai:consume:remove")
@PinSysLog(value = "AI工具消费记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public Response remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return ResponseUtils.ofResult(aiConsumeRecordServ.delByIds(List.of(ids)));
}
}
package com.anplus.hr.ai.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.alibaba.cola.dto.PageResponse;
import com.alibaba.cola.dto.Response;
import com.alibaba.cola.dto.SingleResponse;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
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.AiRechargeRecordVo;
import com.anplus.hr.ai.service.AiRechargeRecordServ;
import top.binfast.common.core.constant.BusinessType;
import top.binfast.common.core.util.ResponseUtils;
import top.binfast.common.excel.annotion.ExcelExport;
import top.binfast.common.log.annotation.PinSysLog;
import java.util.List;
/**
* AI工具充值记录
*
* @author LiuBin
* @date 2026-09-04
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/recharge")
public class AiRechargeRecordCtrl {
private final AiRechargeRecordServ aiRechargeRecordServ;
/**
* 分页查询AI工具充值记录列表。
*
* @param param 查询条件
* @return AI工具充值记录分页结果
*/
@SaCheckPermission("ai:recharge:list")
@GetMapping("/list")
public PageResponse<AiRechargeRecordVo> pageList(AiRechargeRecordListParam param) {
return aiRechargeRecordServ.queryPageList(param);
}
/**
* 导出AI工具充值记录列表。
*
* @param param 查询条件
*/
@ExcelExport
@SaCheckPermission("ai:recharge:export")
@PinSysLog(value = "AI工具充值记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public List<AiRechargeRecordVo> export(AiRechargeRecordListParam param) {
return aiRechargeRecordServ.queryList(param);
}
/**
* 获取AI工具充值记录详细信息
*
* @param id 主键
*/
@SaCheckPermission("ai:recharge:query")
@GetMapping("/{id}")
public SingleResponse<AiRechargeRecordVo> getDetail(@PathVariable @Min(1)
Long id) {
return SingleResponse.of(aiRechargeRecordServ.queryById(id));
}
/**
* 新增AI工具充值记录。
*
* @param param AI工具充值记录参数
* @return 操作结果
*/
@SaCheckPermission("ai:recharge:add")
@PinSysLog(value = "AI工具充值记录", businessType = BusinessType.INSERT)
@PostMapping()
public Response add(@Validated @RequestBody AiRechargeRecordParam param) {
return ResponseUtils.ofResult(aiRechargeRecordServ.insertByParam(param));
}
/**
* 修改AI工具充值记录。
*
* @param param AI工具充值记录参数
* @return 操作结果
*/
@SaCheckPermission("ai:recharge:edit")
@PinSysLog(value = "AI工具充值记录", businessType = BusinessType.UPDATE)
@PutMapping()
public Response edit(@Validated @RequestBody AiRechargeRecordParam param) {
return ResponseUtils.ofResult(aiRechargeRecordServ.updateByParam(param));
}
/**
* 删除AI工具充值记录
*
* @param ids 主键串
*/
@SaCheckPermission("ai:recharge:remove")
@PinSysLog(value = "AI工具充值记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public Response remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return ResponseUtils.ofResult(aiRechargeRecordServ.delByIds(List.of(ids)));
}
}
package com.anplus.hr.ai.domain.model;
import top.binfast.common.mybatis.bean.model.TenantModel;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* AI工具消费记录对象 ai_consume_record
*
* @author LiuBin
* @date 2026-09-04
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@TableName("ai_consume_record")
public class AiConsumeRecord extends TenantModel {
@Serial
private static final long serialVersionUID = 1L;
/**
* 项目名称(字典ai_project_name的dict_value)
*/
private String projectName;
/**
* 使用人姓名(支持模糊查询)
*/
private String userName;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
private String toolName;
/**
* 国内/国外(中文原文,固定二值)
*/
private String region;
/**
* 消耗积分
*/
private BigDecimal consumePoints;
/**
* 金额(人民币)
*/
private BigDecimal amountCny;
/**
* 消费发生时间
*/
private LocalDateTime consumeTime;
/**
* 备注
*/
private String remark;
}
package com.anplus.hr.ai.domain.model;
import top.binfast.common.mybatis.bean.model.TenantModel;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* AI工具充值记录对象 ai_recharge_record
*
* @author LiuBin
* @date 2026-09-04
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@TableName("ai_recharge_record")
public class AiRechargeRecord extends TenantModel {
@Serial
private static final long serialVersionUID = 1L;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
private String toolName;
/**
* 国内/国外(中文原文,固定二值)
*/
private String region;
/**
* 充值分类(字典ai_recharge_category的dict_value编码)
*/
private String rechargeCategory;
/**
* 充值完成时间
*/
private LocalDateTime rechargeTime;
/**
* 到期时间(纯字段,无联动)
*/
private LocalDate expireTime;
/**
* 充值积分
*/
private BigDecimal rechargePoints;
/**
* 充值金额(美元)
*/
private BigDecimal amountUsd;
/**
* 充值金额(人民币)
*/
private BigDecimal amountCny;
/**
* 开通账号数量
*/
private Integer accountCount;
/**
* 剩余积分(手工维护)
*/
private BigDecimal remainingPoints;
/**
* 备注
*/
private String remark;
}
package com.anplus.hr.ai.domain.params;
import lombok.Getter;
import lombok.Setter;
import top.binfast.common.core.bean.params.PageQueryParam;
import java.util.HashMap;
import java.util.Map;
/**
* AI工具消费记录分页对象 ai_consume_record
*
* @author LiuBin
* @date 2026-09-04
*/
@Getter
@Setter
public class AiConsumeRecordListParam extends PageQueryParam {
/**
* 项目名称(字典ai_project_name的dict_value)
*/
private String projectName;
/**
* 使用人姓名(支持模糊查询)
*/
private String userName;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
private String toolName;
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.constraints.NotBlank;
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;
/**
* AI工具消费记录业务对象 ai_consume_record
*
* @author LiuBin
* @date 2026-09-04
*/
@Data
@AutoMapper(target = AiConsumeRecord.class, reverseConvertGenerate = false)
public class AiConsumeRecordParam {
/**
* 主键ID(新增为空,修改必填)
*/
private Long id;
/**
* 项目名称(字典ai_project_name的dict_value)
*/
@NotBlank(message = "项目名称不能为空")
private String projectName;
/**
* 使用人姓名(支持模糊查询)
*/
@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;
/**
* 备注
*/
@Size(max = 200, message = "备注最长200字符")
private String remark;
}
package com.anplus.hr.ai.domain.params;
import lombok.Getter;
import lombok.Setter;
import top.binfast.common.core.bean.params.PageQueryParam;
import java.util.HashMap;
import java.util.Map;
/**
* AI工具充值记录分页对象 ai_recharge_record
*
* @author LiuBin
* @date 2026-09-04
*/
@Getter
@Setter
public class AiRechargeRecordListParam extends PageQueryParam {
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
private String toolName;
/**
* 国内/国外(中文原文,固定二值)
*/
private String region;
/**
* 充值分类(字典ai_recharge_category的dict_value编码)
*/
private String rechargeCategory;
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.constraints.Min;
import jakarta.validation.constraints.NotBlank;
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.AiRechargeRecord;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* AI工具充值记录业务对象 ai_recharge_record
*
* @author LiuBin
* @date 2026-09-04
*/
@Data
@AutoMapper(target = AiRechargeRecord.class, reverseConvertGenerate = false)
public class AiRechargeRecordParam {
/**
* 主键ID(新增为空,修改必填)
*/
private Long id;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
@NotBlank(message = "AI工具/平台名称不能为空")
private String toolName;
/**
* 国内/国外(中文原文,固定二值)
*/
@NotBlank(message = "国内/国外不能为空")
@Pattern(regexp = "国内|国外", message = "国内/国外仅接受「国内」或「国外」")
private String region;
/**
* 充值分类(字典ai_recharge_category的dict_value编码)
*/
@NotBlank(message = "充值分类不能为空")
private String rechargeCategory;
/**
* 充值完成时间
*/
@NotNull(message = "充值完成时间不能为空")
private LocalDateTime rechargeTime;
/**
* 到期时间(纯字段,无联动)
*/
private LocalDate expireTime;
/**
* 充值积分
*/
@Digits(integer = 16, fraction = 2, message = "充值积分最多2位小数")
private BigDecimal rechargePoints;
/**
* 充值金额(美元)
*/
@Digits(integer = 16, fraction = 2, message = "充值金额(美元)最多2位小数")
private BigDecimal amountUsd;
/**
* 充值金额(人民币)
*/
@NotNull(message = "充值金额(人民币)不能为空")
@Digits(integer = 16, fraction = 2, message = "充值金额(人民币)最多2位小数")
private BigDecimal amountCny;
/**
* 开通账号数量
*/
@Min(value = 0, message = "开通账号数量不能为负数")
private Integer accountCount;
/**
* 剩余积分(手工维护)
*/
@Digits(integer = 16, fraction = 2, message = "剩余积分最多2位小数")
private BigDecimal remainingPoints;
/**
* 备注
*/
@Size(max = 200, message = "备注最长200字符")
private String remark;
}
package com.anplus.hr.ai.domain.vo;
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 java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* AI工具消费记录视图对象 ai_consume_record
*
* @author LiuBin
* @date 2026-09-04
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = AiConsumeRecord.class)
public class AiConsumeRecordVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long id;
/**
* 项目名称(字典ai_project_name的dict_value)
*/
@ExcelProperty(value = "项目名称", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "ai_project_name")
private String projectName;
/**
* 使用人姓名
*/
@ExcelProperty(value = "姓名")
private String userName;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
@ExcelProperty(value = "AI工具/平台名称", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "ai_tool_name")
private String toolName;
/**
* 国内/国外(中文原文,固定二值)
*/
@ExcelProperty(value = "国内/国外")
private String region;
/**
* 消耗积分
*/
@ExcelProperty(value = "消耗积分")
private BigDecimal consumePoints;
/**
* 金额(人民币)
*/
@ExcelProperty(value = "金额(人民币)")
private BigDecimal amountCny;
/**
* 消费发生时间
*/
@ExcelProperty(value = "消费时间")
private LocalDateTime consumeTime;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
private LocalDateTime createTime;
}
package com.anplus.hr.ai.domain.vo;
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.AiRechargeRecord;
import top.binfast.common.excel.annotion.ExcelDictFormat;
import top.binfast.common.excel.converters.ExcelDictConvert;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* AI工具充值记录视图对象 ai_recharge_record
*
* @author LiuBin
* @date 2026-09-04
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = AiRechargeRecord.class)
public class AiRechargeRecordVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long id;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
@ExcelProperty(value = "AI工具/平台名称", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "ai_tool_name")
private String toolName;
/**
* 国内/国外(中文原文,固定二值)
*/
@ExcelProperty(value = "国内/国外")
private String region;
/**
* 充值分类(字典ai_recharge_category的dict_value编码)
*/
@ExcelProperty(value = "充值分类", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "ai_recharge_category")
private String rechargeCategory;
/**
* 充值完成时间
*/
@ExcelProperty(value = "充值完成时间")
private LocalDateTime rechargeTime;
/**
* 到期时间(纯字段,无联动)
*/
@ExcelProperty(value = "到期时间")
private LocalDate expireTime;
/**
* 充值积分
*/
@ExcelProperty(value = "充值积分")
private BigDecimal rechargePoints;
/**
* 充值金额(美元)
*/
@ExcelProperty(value = "充值金额(美元)")
private BigDecimal amountUsd;
/**
* 充值金额(人民币)
*/
@ExcelProperty(value = "充值金额(人民币)")
private BigDecimal amountCny;
/**
* 开通账号数量
*/
@ExcelProperty(value = "开通账号数量")
private Integer accountCount;
/**
* 剩余积分(手工维护)
*/
@ExcelProperty(value = "剩余积分")
private BigDecimal remainingPoints;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
private LocalDateTime createTime;
}
package com.anplus.hr.ai.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
import com.anplus.hr.ai.domain.model.AiConsumeRecord;
import top.binfast.common.mybatis.mapper.BinBaseMapper;
/**
* AI工具消费记录Mapper接口
*
* @author LiuBin
* @date 2026-09-04
*/
@Mapper
public interface AiConsumeRecordMapper extends BinBaseMapper<AiConsumeRecord>, MPJBaseMapper<AiConsumeRecord> {
}
package com.anplus.hr.ai.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Mapper;
import com.anplus.hr.ai.domain.model.AiRechargeRecord;
import top.binfast.common.mybatis.mapper.BinBaseMapper;
/**
* AI工具充值记录Mapper接口
*
* @author LiuBin
* @date 2026-09-04
*/
@Mapper
public interface AiRechargeRecordMapper extends BinBaseMapper<AiRechargeRecord>, MPJBaseMapper<AiRechargeRecord> {
}
package com.anplus.hr.ai.service;
import com.alibaba.cola.dto.PageResponse;
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.AiConsumeRecordVo;
import java.util.List;
/**
* AI工具消费记录Service接口
*
* @author LiuBin
* @date 2026-09-04
*/
public interface AiConsumeRecordServ extends IService<AiConsumeRecord> {
/**
* 分页查询AI工具消费记录列表
*
* @param param 查询条件
* @return AI工具消费记录分页列表
*/
PageResponse<AiConsumeRecordVo> queryPageList(AiConsumeRecordListParam param);
/**
* 查询符合条件的AI工具消费记录列表
*
* @param param 查询条件
* @return AI工具消费记录列表
*/
List<AiConsumeRecordVo> queryList(AiConsumeRecordListParam param);
/**
* 查询AI工具消费记录
*
* @param id 主键
* @return AI工具消费记录
*/
AiConsumeRecordVo queryById(Long id);
/**
* 新增AI工具消费记录
*
* @param param AI工具消费记录
* @return 是否新增成功
*/
Boolean insertByParam(AiConsumeRecordParam param);
/**
* 修改AI工具消费记录
*
* @param param AI工具消费记录
* @return 是否修改成功
*/
Boolean updateByParam(AiConsumeRecordParam param);
/**
* 校验并批量删除AI工具消费记录信息
*
* @param ids 待删除的主键集合
* @return 是否删除成功
*/
Boolean delByIds(List<Long> ids);
}
package com.anplus.hr.ai.service;
import com.alibaba.cola.dto.PageResponse;
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.AiRechargeRecordVo;
import java.util.List;
/**
* AI工具充值记录Service接口
*
* @author LiuBin
* @date 2026-09-04
*/
public interface AiRechargeRecordServ extends IService<AiRechargeRecord> {
/**
* 分页查询AI工具充值记录列表
*
* @param param 查询条件
* @return AI工具充值记录分页列表
*/
PageResponse<AiRechargeRecordVo> queryPageList(AiRechargeRecordListParam param);
/**
* 查询符合条件的AI工具充值记录列表
*
* @param param 查询条件
* @return AI工具充值记录列表
*/
List<AiRechargeRecordVo> queryList(AiRechargeRecordListParam param);
/**
* 查询AI工具充值记录
*
* @param id 主键
* @return AI工具充值记录
*/
AiRechargeRecordVo queryById(Long id);
/**
* 新增AI工具充值记录
*
* @param param AI工具充值记录
* @return 是否新增成功
*/
Boolean insertByParam(AiRechargeRecordParam param);
/**
* 修改AI工具充值记录
*
* @param param AI工具充值记录
* @return 是否修改成功
*/
Boolean updateByParam(AiRechargeRecordParam param);
/**
* 校验并批量删除AI工具充值记录信息
*
* @param ids 待删除的主键集合
* @return 是否删除成功
*/
Boolean delByIds(List<Long> ids);
}
package com.anplus.hr.ai.service.impl;
import com.alibaba.cola.dto.PageResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
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.AiConsumeRecordVo;
import com.anplus.hr.ai.mapper.AiConsumeRecordMapper;
import com.anplus.hr.ai.service.AiConsumeRecordServ;
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.util.List;
import java.util.Map;
/**
* AI工具消费记录Service业务层处理
*
* @author LiuBin
* @date 2026-09-04
*/
@RequiredArgsConstructor
@Service
public class AiConsumeRecordServImpl extends ServiceImpl<AiConsumeRecordMapper, AiConsumeRecord> implements AiConsumeRecordServ {
private final AiConsumeRecordMapper aiConsumeRecordMapper;
/**
* 分页查询AI工具消费记录列表
*
* @param param 查询条件
* @return AI工具消费记录分页列表
*/
@Override
public PageResponse<AiConsumeRecordVo> queryPageList(AiConsumeRecordListParam param) {
Page<AiConsumeRecord> page = QueryUtil.getPage(param);
LambdaQueryWrapper<AiConsumeRecord> lambdaQuery = this.buildQueryWrapper(param);
aiConsumeRecordMapper.selectPage(page, lambdaQuery);
return QueryUtil.getPageResponse(page, MapstructUtils.convert(page.getRecords(), AiConsumeRecordVo.class));
}
/**
* 查询符合条件的AI工具消费记录列表
*
* @param param 查询条件
* @return AI工具消费记录列表
*/
@Override
public List<AiConsumeRecordVo> queryList(AiConsumeRecordListParam param) {
LambdaQueryWrapper<AiConsumeRecord> lambdaQuery = this.buildQueryWrapper(param);
return MapstructUtils.convert(aiConsumeRecordMapper.selectList(lambdaQuery), AiConsumeRecordVo.class);
}
private LambdaQueryWrapper<AiConsumeRecord> buildQueryWrapper(AiConsumeRecordListParam param) {
Map<String, Object> params = param.getParams();
LambdaQueryBuilder<AiConsumeRecord> builder = QueryBuilder.lambda(AiConsumeRecord.class)
.eqIfText(AiConsumeRecord::getProjectName, param.getProjectName())
.eqIfText(AiConsumeRecord::getToolName, param.getToolName())
.likeIfText(AiConsumeRecord::getUserName, param.getUserName())
.betweenParams(AiConsumeRecord::getConsumeTime, params, "beginTime", "endTime")
.orderByDesc(AiConsumeRecord::getConsumeTime);
return builder.build();
}
/**
* 查询AI工具消费记录
*
* @param id 主键
* @return AI工具消费记录
*/
@Override
public AiConsumeRecordVo queryById(Long id) {
AiConsumeRecord aiConsumeRecord = aiConsumeRecordMapper.selectById(id);
return MapstructUtils.convert(aiConsumeRecord, AiConsumeRecordVo.class);
}
/**
* 新增AI工具消费记录
*
* @param param AI工具消费记录
* @return 是否新增成功
*/
@Override
public Boolean insertByParam(AiConsumeRecordParam param) {
AiConsumeRecord aiConsumeRecord = MapstructUtils.convert(param, AiConsumeRecord.class);
return this.save(aiConsumeRecord);
}
/**
* 修改AI工具消费记录
*
* @param param AI工具消费记录
* @return 是否修改成功
*/
@Override
public Boolean updateByParam(AiConsumeRecordParam param) {
AiConsumeRecord aiConsumeRecord = MapstructUtils.convert(param, AiConsumeRecord.class);
return this.updateById(aiConsumeRecord);
}
/**
* 校验并批量删除AI工具消费记录信息
*
* @param ids 待删除的主键集合
* @return 是否删除成功
*/
@Override
public Boolean delByIds(List<Long> ids) {
//无关联业务校验,直接逻辑删除(@TableLogic)
return this.removeByIds(ids);
}
}
package com.anplus.hr.ai.service.impl;
import com.alibaba.cola.dto.PageResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
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.AiRechargeRecordVo;
import com.anplus.hr.ai.mapper.AiRechargeRecordMapper;
import com.anplus.hr.ai.service.AiRechargeRecordServ;
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.util.List;
import java.util.Map;
/**
* AI工具充值记录Service业务层处理
*
* @author LiuBin
* @date 2026-09-04
*/
@RequiredArgsConstructor
@Service
public class AiRechargeRecordServImpl extends ServiceImpl<AiRechargeRecordMapper, AiRechargeRecord> implements AiRechargeRecordServ {
private final AiRechargeRecordMapper aiRechargeRecordMapper;
/**
* 分页查询AI工具充值记录列表
*
* @param param 查询条件
* @return AI工具充值记录分页列表
*/
@Override
public PageResponse<AiRechargeRecordVo> queryPageList(AiRechargeRecordListParam param) {
Page<AiRechargeRecord> page = QueryUtil.getPage(param);
LambdaQueryWrapper<AiRechargeRecord> lambdaQuery = this.buildQueryWrapper(param);
aiRechargeRecordMapper.selectPage(page, lambdaQuery);
return QueryUtil.getPageResponse(page, MapstructUtils.convert(page.getRecords(), AiRechargeRecordVo.class));
}
/**
* 查询符合条件的AI工具充值记录列表
*
* @param param 查询条件
* @return AI工具充值记录列表
*/
@Override
public List<AiRechargeRecordVo> queryList(AiRechargeRecordListParam param) {
LambdaQueryWrapper<AiRechargeRecord> lambdaQuery = this.buildQueryWrapper(param);
return MapstructUtils.convert(aiRechargeRecordMapper.selectList(lambdaQuery), AiRechargeRecordVo.class);
}
private LambdaQueryWrapper<AiRechargeRecord> buildQueryWrapper(AiRechargeRecordListParam param) {
Map<String, Object> params = param.getParams();
LambdaQueryBuilder<AiRechargeRecord> builder = 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();
}
/**
* 查询AI工具充值记录
*
* @param id 主键
* @return AI工具充值记录
*/
@Override
public AiRechargeRecordVo queryById(Long id) {
AiRechargeRecord aiRechargeRecord = aiRechargeRecordMapper.selectById(id);
return MapstructUtils.convert(aiRechargeRecord, AiRechargeRecordVo.class);
}
/**
* 新增AI工具充值记录
*
* @param param AI工具充值记录
* @return 是否新增成功
*/
@Override
public Boolean insertByParam(AiRechargeRecordParam param) {
AiRechargeRecord aiRechargeRecord = MapstructUtils.convert(param, AiRechargeRecord.class);
return this.save(aiRechargeRecord);
}
/**
* 修改AI工具充值记录
*
* @param param AI工具充值记录
* @return 是否修改成功
*/
@Override
public Boolean updateByParam(AiRechargeRecordParam param) {
AiRechargeRecord aiRechargeRecord = MapstructUtils.convert(param, AiRechargeRecord.class);
return this.updateById(aiRechargeRecord);
}
/**
* 校验并批量删除AI工具充值记录信息
*
* @param ids 待删除的主键集合
* @return 是否删除成功
*/
@Override
public Boolean delByIds(List<Long> ids) {
//无关联业务校验,直接逻辑删除(@TableLogic)
return this.removeByIds(ids);
}
}
server: server:
port: 9000 port: 8090
servlet: servlet:
context-path: /api context-path: /api
# 开启优雅停机,默认值:immediate 为立即关闭 # 开启优雅停机,默认值:immediate 为立即关闭
......
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.anplus.hr.ai.mapper.AiConsumeRecordMapper">
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.anplus.hr.ai.mapper.AiRechargeRecordMapper">
</mapper>
# 应用标题 # 应用标题
VITE_APP_TITLE=Plus Admin VITE_APP_TITLE=AI管理系统
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离
VITE_APP_NAMESPACE=dap-plus-vben VITE_APP_NAMESPACE=hr-ai
# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 # 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密
VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key
...@@ -10,17 +10,17 @@ VITE_ROUTER_HISTORY=history ...@@ -10,17 +10,17 @@ VITE_ROUTER_HISTORY=history
VITE_INJECT_APP_LOADING=true VITE_INJECT_APP_LOADING=true
# 打包后是否生成dist.zip # 打包后是否生成dist.zip
VITE_ARCHIVER=true VITE_ARCHIVER=false
# 后端接口地址 # 后端接口地址
VITE_GLOB_API_URL=/prod-api VITE_GLOB_API_URL=/api
# 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应) # 全局加密开关(即开启了加解密功能才会生效 不是全部接口加密 需要和后端对应)
VITE_GLOB_ENABLE_ENCRYPT=true VITE_GLOB_ENABLE_ENCRYPT=true
# RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对 # RSA公钥 请求加密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PUBLIC_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB VITE_GLOB_RSA_PUBLIC_KEY=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC44LZ19Io2qIUxZvOfDxpUXy2Kmvcb72cPL0NE3takaNO53To30evTpXvXdY3KdZsubQoBnuxVnCha+jnDN+FZ35gJ50oyvoRG03r24zaZMTe0HlSHsDL/p0e9zn4emRc+2X9wY6XG0ZLNcE/lpy2FiWDCQbXubVi9Bv6c+axUywIDAQAB
# RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对 # RSA私钥 响应解密使用 注意这两个是两对RSA公私钥 请求加密-后端解密是一对 响应解密-后端加密是一对
VITE_GLOB_RSA_PRIVATE_KEY=MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A== VITE_GLOB_RSA_PRIVATE_KEY=MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALjgtnX0ijaohTFm858PGlRfLYqa9xvvZw8vQ0Te1qRo07ndOjfR69Ole9d1jcp1my5tCgGe7FWcKFr6OcM34VnfmAnnSjK+hEbTevbjNpkxN7QeVIewMv+nR73Ofh6ZFz7Zf3BjpcbRks1wT+WnLYWJYMJBte5tWL0G/pz5rFTLAgMBAAECgYAw4s6skxynBlmt7gS2hwZV0BMZRw/Xla7hP4SgpfII6GUwDRUTZJFg1i3Qk9eJ9R4zIQcX1Z+1pYSwReFXQyP1rkYx/pU8fBfsXNmYI4rC4J4fTPeVYaVcjiiw9s0wJHoRl2Hc00+7okWQxA7s9UDFLj96oh/qETDpQ2jaB+aDXQJBAN/PO0XAN2BzvML4VboiVeLnzg83nEkr6Tulg+FaU7LPraPV9j8lVWwibjdGZKJDsBG47BKdRU6319LKOdCHhr0CQQDTd/9kExlnBoYyi/dGVIZDmDvAwufS+WAvxZrGYcV7pCNY5wnW6IfhQlNWJLyhhy8g+cJhQXHU/swhWIlZ7eYnAkBGGLOpvvAZJ2ahcgH3kDVyIcPqBDLQwA4Gfmp1tEKuqFjsDWFYmarNeWMnsMZoA+crRc26bsN8ShwwiuHcq/XJAkEAw8fOD9XK5+JXbga9iUSKBZy0fXM1c9LjJXdaIx6z2hONkipcprYjzv47734Vkap5jJsPRkFz5RF85cZ1a2fgcwJBANvtElF3T6NkHETqVpdCcDbCiQDLaHX3eifUe1z8efVGKOhlTYN/msfCLEgMknxNCOuF+V8Y9ukw9ygN0OUv3L8=
# 客户端id # 客户端id
VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e VITE_GLOB_APP_CLIENT_ID=e5cd7e4891bf95d1d19206ce24a7b32e
......
import type { ID, IDS, PageQuery, PageResult } from '@/api/common';
import type { Consume } from './model';
import { commonExport } from '@/api/helper';
import { alovaInstance } from '@/utils/http';
enum Api {
consumeExport = '/ai/consume/export',
consumeList = '/ai/consume/list',
root = '/ai/consume',
}
/**
* 查询消费记录列表
* @param params 请求参数
* @returns 列表
*/
export function consumeList(params?: PageQuery) {
return alovaInstance.get<PageResult<Consume>>(Api.consumeList, { params });
}
/**
* 导出消费记录excel
* @param data 请求参数
*/
export function consumeExport(data: Partial<Consume>) {
return commonExport(Api.consumeExport, data);
}
/**
* 消费记录详情
* @param id id
* @returns 详情
*/
export function consumeInfo(id: ID) {
return alovaInstance.get<Consume>(`${Api.root}/${id}`);
}
/**
* 消费记录新增
* @param data 参数
*/
export function consumeAdd(data: Partial<Consume>) {
return alovaInstance.postWithMsg<void>(Api.root, data);
}
/**
* 消费记录修改
* @param data 参数
*/
export function consumeUpdate(data: Partial<Consume>) {
return alovaInstance.putWithMsg<void>(Api.root, data);
}
/**
* 消费记录删除
* @param ids id集合
*/
export function consumeRemove(ids: IDS) {
return alovaInstance.deleteWithMsg<void>(`${Api.root}/${ids}`);
}
/**
* @description: Consume interface
*/
export interface Consume {
id: number;
/**
* 项目名称(字典ai_project_name的dict_value)
*/
projectName: string;
/**
* 使用人姓名
*/
userName: string;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
toolName: string;
/**
* 国内/国外(中文原文,固定二值)
*/
region: string;
/**
* 消耗积分
*/
consumePoints: number;
/**
* 金额(人民币)
*/
amountCny: number;
/**
* 消费发生时间
*/
consumeTime: string;
/**
* 备注
*/
remark: string;
createTime: string;
}
import type { ID, IDS, PageQuery, PageResult } from '@/api/common';
import type { Recharge } from './model';
import { commonExport } from '@/api/helper';
import { alovaInstance } from '@/utils/http';
enum Api {
rechargeExport = '/ai/recharge/export',
rechargeList = '/ai/recharge/list',
root = '/ai/recharge',
}
/**
* 查询充值记录列表
* @param params 请求参数
* @returns 列表
*/
export function rechargeList(params?: PageQuery) {
return alovaInstance.get<PageResult<Recharge>>(Api.rechargeList, { params });
}
/**
* 导出充值记录excel
* @param data 请求参数
*/
export function rechargeExport(data: Partial<Recharge>) {
return commonExport(Api.rechargeExport, data);
}
/**
* 充值记录详情
* @param id id
* @returns 详情
*/
export function rechargeInfo(id: ID) {
return alovaInstance.get<Recharge>(`${Api.root}/${id}`);
}
/**
* 充值记录新增
* @param data 参数
*/
export function rechargeAdd(data: Partial<Recharge>) {
return alovaInstance.postWithMsg<void>(Api.root, data);
}
/**
* 充值记录修改
* @param data 参数
*/
export function rechargeUpdate(data: Partial<Recharge>) {
return alovaInstance.putWithMsg<void>(Api.root, data);
}
/**
* 充值记录删除
* @param ids id集合
*/
export function rechargeRemove(ids: IDS) {
return alovaInstance.deleteWithMsg<void>(`${Api.root}/${ids}`);
}
/**
* @description: Recharge interface
*/
export interface Recharge {
id: number;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
toolName: string;
/**
* 国内/国外(中文原文,固定二值)
*/
region: string;
/**
* 充值分类(字典ai_recharge_category的dict_value编码)
*/
rechargeCategory: string;
/**
* 充值完成时间
*/
rechargeTime: string;
/**
* 到期时间
*/
expireTime: string;
/**
* 充值积分
*/
rechargePoints: number;
/**
* 充值金额(美元)
*/
amountUsd: number;
/**
* 充值金额(人民币)
*/
amountCny: number;
/**
* 开通账号数量
*/
accountCount: number;
/**
* 剩余积分
*/
remainingPoints: number;
/**
* 备注
*/
remark: string;
createTime: string;
}
...@@ -23,8 +23,9 @@ export const ContentTypeEnum = { ...@@ -23,8 +23,9 @@ export const ContentTypeEnum = {
export function commonExport(url: string, data: Record<string, any>) { export function commonExport(url: string, data: Record<string, any>) {
return alovaInstance.post<Blob>(url, data, { return alovaInstance.post<Blob>(url, data, {
headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED }, headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
// transformResponse: false, isTransformResponse: false,
responseType: 'blob', responseType: 'blob',
timeout: 600_000,
}); });
} }
......
export const DictEnum = { 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_COMMON_STATUS: 'sys_common_status',
SYS_DEVICE_TYPE: 'sys_device_type', // 设备类型 SYS_DEVICE_TYPE: 'sys_device_type', // 设备类型
SYS_GRANT_TYPE: 'sys_grant_type', // 授权类型 SYS_GRANT_TYPE: 'sys_grant_type', // 授权类型
......
{ {
"welcomeBack": "欢迎回来", "welcomeBack": "欢迎回来",
"pageTitle": "开箱即用的大型中后台管理系统", "pageTitle": "AI管理系统",
"pageDesc": "工程化、高性能、跨组件库的前端模版", "pageDesc": "让每笔钱都花的明明白白",
"loginSuccess": "登录成功", "loginSuccess": "登录成功",
"loginSuccessDesc": "欢迎回来", "loginSuccessDesc": "欢迎回来",
"loginSubtitle": "请输入您的账户信息以开始管理您的项目", "loginSubtitle": "请输入您的账户信息以开始管理您的项目",
......
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
// AI工具平台
{
path: '/ai',
name: 'Ai',
meta: {
title: 'AI工具平台',
keepAlive: true,
icon: 'lucide:bot',
permission: ['dashboard'],
},
children: [
{
path: 'recharge',
name: 'AiRechargeList',
component: () => import('@/views/ai/recharge/index.vue'),
meta: {
title: '充值记录',
icon: 'lucide:wallet',
keepAlive: true,
permission: ['dashboard'],
componentPath: '@/views/ai/recharge/index.vue',
},
},
{
path: 'consume',
name: 'AiConsumeList',
component: () => import('@/views/ai/consume/index.vue'),
meta: {
title: '消费记录',
icon: 'lucide:receipt',
keepAlive: true,
permission: ['dashboard'],
componentPath: '@/views/ai/consume/index.vue',
},
},
],
},
];
export default routes;
<script setup lang="ts">
import type { Consume } from '@/api/ai/consume/model';
import type { AntdFormRules } from '@/types/form';
import type { FormInstance } from 'antdv-next';
import { computed, ref } from 'vue';
import { consumeAdd, consumeInfo, consumeUpdate } from '@/api/ai/consume';
import { useVbenDrawer } from '@/components';
import {
FormInput as Input,
FormInputNumber as InputNumber,
FormSelect as Select,
FormTextArea as TextArea,
} from '@/components/global/form';
import { DictEnum } from '@/constants';
import { $t } from '@/locales';
import { cloneDeep, getPopupContainer } from '@/utils';
import { getDictOptions } from '@/utils/dict';
import { useBeforeCloseDiff } from '@/utils/popup';
import { DatePicker, Form, FormItem } from 'antdv-next';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false);
const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
type FormData = Partial<Consume>;
// 国内/国外为固定二值(非字典,Clarify C2')
const regionOptions = [
{ label: '国内', value: '国内' },
{ label: '国外', value: '国外' },
];
function getDefaultValues(): FormData {
return {
id: undefined,
projectName: undefined,
userName: undefined,
toolName: undefined,
region: undefined,
consumePoints: undefined,
amountCny: undefined,
consumeTime: undefined,
remark: '',
};
}
const formData = ref<FormData>(getDefaultValues());
const formInstance = ref<FormInstance>();
const formRules = ref<AntdFormRules<FormData>>({
projectName: [{ required: true, message: $t('ui.formRules.selectRequired') }],
userName: [{ required: true, message: $t('ui.formRules.required') }],
toolName: [{ required: true, message: $t('ui.formRules.selectRequired') }],
region: [{ required: true, message: $t('ui.formRules.selectRequired') }],
consumeTime: [{ required: true, message: $t('ui.formRules.required') }],
});
function customFormValueGetter() {
return JSON.stringify(formData.value);
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: customFormValueGetter,
currentGetter: customFormValueGetter,
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
async onOpenChange(isOpen) {
if (!isOpen) {
return null;
}
drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id;
// 更新 && 赋值
if (isUpdate.value && id) {
const record = await consumeInfo(id);
formData.value = {
...getDefaultValues(),
...record,
remark: record.remark ?? '',
};
}
await markInitialized();
drawerApi.drawerLoading(false);
},
});
async function handleConfirm() {
try {
drawerApi.lock(true);
await formInstance.value?.validate();
const data = cloneDeep(formData.value);
await (isUpdate.value ? consumeUpdate(data) : consumeAdd(data));
resetInitialized();
emit('reload');
drawerApi.close();
} catch (error) {
console.error(error);
} finally {
drawerApi.lock(false);
}
}
async function handleClosed() {
formData.value = getDefaultValues();
formInstance.value?.resetFields();
resetInitialized();
}
</script>
<template>
<BasicDrawer :title="title" :size="700">
<Form
ref="formInstance"
:model="formData"
:label-col="{ style: { width: '140px' } }"
>
<FormItem label="项目名称" name="projectName" :rules="formRules.projectName">
<Select
class="w-full"
:allow-clear="false"
option-filter-prop="label"
show-search
:get-popup-container="getPopupContainer"
:options="getDictOptions(DictEnum.AI_PROJECT_NAME)"
v-model:value="formData.projectName"
/>
</FormItem>
<FormItem label="使用人姓名" name="userName" :rules="formRules.userName">
<Input
class="w-full"
allow-clear
v-model:value="formData.userName"
/>
</FormItem>
<FormItem label="AI工具/平台名称" name="toolName" :rules="formRules.toolName">
<Select
class="w-full"
:allow-clear="false"
option-filter-prop="label"
show-search
:get-popup-container="getPopupContainer"
:options="getDictOptions(DictEnum.AI_TOOL_NAME)"
v-model:value="formData.toolName"
/>
</FormItem>
<FormItem label="国内/国外" name="region" :rules="formRules.region">
<Select
class="w-full"
:allow-clear="false"
:get-popup-container="getPopupContainer"
:options="regionOptions"
v-model:value="formData.region"
/>
</FormItem>
<FormItem label="消费时间" name="consumeTime" :rules="formRules.consumeTime">
<DatePicker
class="w-full"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
v-model:value="formData.consumeTime"
/>
</FormItem>
<FormItem label="消耗积分" name="consumePoints">
<InputNumber
class="w-full"
:style="{ width: '50%' }"
:min="0"
:precision="2"
v-model:value="formData.consumePoints"
/>
</FormItem>
<FormItem label="金额(人民币)" name="amountCny">
<InputNumber
class="w-full"
:style="{ width: '50%' }"
:min="0"
:precision="2"
v-model:value="formData.amountCny"
/>
</FormItem>
<FormItem label="备注" name="remark">
<TextArea
allow-clear
class="w-full"
:rows="4"
:maxlength="200"
show-count
v-model:value="formData.remark"
/>
</FormItem>
</Form>
</BasicDrawer>
</template>
<script setup lang="ts">
import type { FormInstance } from 'antdv-next';
import type { Dayjs } from 'dayjs';
import { ref } from 'vue';
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 { getDictOptions } from '@/utils/dict';
import { Card, DateRangePicker, Form, FormItem } from 'antdv-next';
const emit = defineEmits<{
reset: [];
submit: [record: Record<string, any>];
}>();
interface ConsumeSearchFormParams {
projectName?: string;
userName?: string;
toolName?: string;
consumeTime?: [Dayjs, Dayjs];
}
const model = ref<ConsumeSearchFormParams>({
projectName: undefined,
userName: undefined,
toolName: undefined,
consumeTime: undefined,
});
const formInstance = ref<FormInstance>();
const searchCollapsed = ref(false);
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]),
};
Reflect.deleteProperty(params, 'consumeTime');
}
return params;
}
async function getValues() {
return buildSearchParams(model.value);
}
async function handleSubmit() {
await formInstance.value?.validate();
emit('submit', await getValues());
}
async function handleReset() {
formInstance.value?.resetFields();
emit('reset');
}
defineExpose({
getValues,
resetFields: () => formInstance.value?.resetFields(),
});
</script>
<template>
<Card size="small">
<Form
ref="formInstance"
:model="model"
:label-col="{ style: { width: '120px' } }"
@keydown.enter.prevent="handleSubmit"
>
<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)"
/>
</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)"
/>
</FormItem>
<FormItem label="消费时间" name="consumeTime">
<DateRangePicker v-model:value="model.consumeTime" allow-clear />
</FormItem>
</template>
<!-- [grid-column-end:-1] 始终定位到最后一列,justify-self-end 靠右对齐 -->
<div class="[grid-column-end:-1] flex items-baseline justify-end gap-4">
<SearchButtonGroup
collapsible
v-model:collapsed="searchCollapsed"
@reset="handleReset"
@submit="handleSubmit"
/>
</div>
</div>
</Form>
</Card>
</template>
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: 'projectName',
slots: {
default: ({ row }) => {
return renderDict(row.projectName, DictEnum.AI_PROJECT_NAME);
},
},
},
{
title: '姓名',
field: 'userName',
},
{
title: 'AI工具/平台名称',
field: 'toolName',
slots: {
default: ({ row }) => {
return renderDict(row.toolName, DictEnum.AI_TOOL_NAME);
},
},
},
{
title: '国内/国外',
field: 'region',
},
{
title: '消耗积分',
field: 'consumePoints',
},
{
title: '金额(人民币)',
field: 'amountCny',
},
{
title: '消费时间',
field: 'consumeTime',
},
{
title: '备注',
field: 'remark',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
resizable: false,
width: 'auto',
},
];
<script setup lang="ts">
import type { Consume } from '@/api/ai/consume/model';
import type { VxeGridInstance, VxeGridListeners } from 'vxe-table';
import { ref, useTemplateRef } from 'vue';
import { consumeExport, consumeList, consumeRemove } from '@/api/ai/consume';
import { Page, useVbenDrawer } from '@/components';
import {
resolveQueryFormValues,
useTableQuery,
withDefaultVxeGridOptions,
} from '@/components/vxe-table';
import { useBlobExport } from '@/utils/file/export';
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';
const searchFormRef = ref<InstanceType<typeof ConsumeSearchForm>>();
const tableLoading = ref(false);
const gridOptions = withDefaultVxeGridOptions<Consume>({
checkboxConfig: {
// 高亮
highlight: true,
// 翻页时保留选中状态
reserve: true,
trigger: 'cell',
},
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
showLoading: false,
ajax: {
query: async ({ page }, formValues) => {
const values = await resolveQueryFormValues(searchFormRef, formValues);
tableLoading.value = true;
try {
return await consumeList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...values,
});
} finally {
tableLoading.value = false;
}
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
slots: {
buttons: 'toolbar-left',
tools: 'toolbar-right',
},
},
id: 'ai-consume-index',
});
const tableRef = useTemplateRef<VxeGridInstance<Consume>>('tableRef');
const { query, reload } = useTableQuery(
searchFormRef,
tableRef,
syncCheckedRows,
);
const checkedRows = ref<Consume[]>([]);
const gridEvents: VxeGridListeners = {
checkboxAll: syncCheckedRows,
checkboxChange: syncCheckedRows,
};
const [ConsumeDrawer, drawerApi] = useVbenDrawer({
connectedComponent: consumeDrawer,
});
function handleAdd() {
drawerApi.setData({});
drawerApi.open();
}
async function handleEdit(record: Consume) {
drawerApi.setData({ id: record.id });
drawerApi.open();
}
async function handleDelete(row: Consume) {
await consumeRemove([row.id]);
// 取消该行选中状态,避免 reserve 记录残留
tableRef.value?.setCheckboxRow(row, false);
await query();
}
function handleMultiDelete() {
const rows = getCheckedRows();
const ids = rows.map((row: Consume) => row.id);
window.modal.confirm({
title: '提示',
okType: 'danger',
content: `确认删除选中的${ids.length}条记录吗?`,
onOk: async () => {
await consumeRemove(ids);
// 清除所有选中状态,避免 reserve 记录残留
tableRef.value?.clearCheckboxRow();
tableRef.value?.clearCheckboxReserve();
await query();
},
});
}
const { exportBlob, exportLoading, buildExportFileName } =
useBlobExport(consumeExport);
async function handleExport() {
// 构建表单请求参数
const formValues = (await searchFormRef.value?.getValues()) ?? {};
// 文件名
const fileName = buildExportFileName('消费记录数据');
exportBlob({ data: formValues, fileName });
}
function handleSearchSubmit(data: Record<string, any>) {
reload(data);
}
function handleSearchReset() {
reload();
}
function getCheckedRows() {
const table = tableRef.value;
if (!table) {
return [];
}
return [
...table.getCheckboxRecords(),
...table.getCheckboxReserveRecords(),
] as Consume[];
}
function syncCheckedRows() {
checkedRows.value = getCheckedRows();
}
</script>
<template>
<Page :auto-content-height="true">
<Spin
:styles="{ root: { height: '100%' }, container: { height: '100%' } }"
:spinning="tableLoading"
size="large"
:delay="300"
>
<div class="flex h-full flex-col gap-2">
<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"
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
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>
</Spin>
<ConsumeDrawer @reload="() => query()" />
</Page>
</template>
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: 'AI工具/平台名称',
field: 'toolName',
slots: {
default: ({ row }) => {
return renderDict(row.toolName, DictEnum.AI_TOOL_NAME);
},
},
},
{
title: '国内/国外',
field: 'region',
},
{
title: '充值分类',
field: 'rechargeCategory',
slots: {
default: ({ row }) => {
return renderDict(row.rechargeCategory, DictEnum.AI_RECHARGE_CATEGORY);
},
},
},
{
title: '充值完成时间',
field: 'rechargeTime',
},
{
title: '到期时间',
field: 'expireTime',
},
{
title: '充值积分',
field: 'rechargePoints',
},
{
title: '充值金额(美元)',
field: 'amountUsd',
},
{
title: '充值金额(人民币)',
field: 'amountCny',
},
{
title: '开通账号数量',
field: 'accountCount',
},
{
title: '剩余积分',
field: 'remainingPoints',
},
{
title: '备注',
field: 'remark',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
resizable: false,
width: 'auto',
},
];
<script setup lang="ts">
import type { Recharge } from '@/api/ai/recharge/model';
import type { VxeGridInstance, VxeGridListeners } from 'vxe-table';
import { ref, useTemplateRef } from 'vue';
import { rechargeExport, rechargeList, rechargeRemove } from '@/api/ai/recharge';
import { Page, useVbenDrawer } from '@/components';
import {
resolveQueryFormValues,
useTableQuery,
withDefaultVxeGridOptions,
} from '@/components/vxe-table';
import { useBlobExport } from '@/utils/file/export';
import { Popconfirm, Space, Spin } from 'antdv-next';
import { VxeGrid } from 'vxe-table';
import { columns } from './data';
import rechargeDrawer from './recharge-drawer.vue';
import RechargeSearchForm from './recharge-search.vue';
const searchFormRef = ref<InstanceType<typeof RechargeSearchForm>>();
const tableLoading = ref(false);
const gridOptions = withDefaultVxeGridOptions<Recharge>({
checkboxConfig: {
// 高亮
highlight: true,
// 翻页时保留选中状态
reserve: true,
trigger: 'cell',
},
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
showLoading: false,
ajax: {
query: async ({ page }, formValues) => {
const values = await resolveQueryFormValues(searchFormRef, formValues);
tableLoading.value = true;
try {
return await rechargeList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...values,
});
} finally {
tableLoading.value = false;
}
},
},
},
rowConfig: {
keyField: 'id',
},
toolbarConfig: {
slots: {
buttons: 'toolbar-left',
tools: 'toolbar-right',
},
},
id: 'ai-recharge-index',
});
const tableRef = useTemplateRef<VxeGridInstance<Recharge>>('tableRef');
const { query, reload } = useTableQuery(
searchFormRef,
tableRef,
syncCheckedRows,
);
const checkedRows = ref<Recharge[]>([]);
const gridEvents: VxeGridListeners = {
checkboxAll: syncCheckedRows,
checkboxChange: syncCheckedRows,
};
const [RechargeDrawer, drawerApi] = useVbenDrawer({
connectedComponent: rechargeDrawer,
});
function handleAdd() {
drawerApi.setData({});
drawerApi.open();
}
async function handleEdit(record: Recharge) {
drawerApi.setData({ id: record.id });
drawerApi.open();
}
async function handleDelete(row: Recharge) {
await rechargeRemove([row.id]);
// 取消该行选中状态,避免 reserve 记录残留
tableRef.value?.setCheckboxRow(row, false);
await query();
}
function handleMultiDelete() {
const rows = getCheckedRows();
const ids = rows.map((row: Recharge) => row.id);
window.modal.confirm({
title: '提示',
okType: 'danger',
content: `确认删除选中的${ids.length}条记录吗?`,
onOk: async () => {
await rechargeRemove(ids);
// 清除所有选中状态,避免 reserve 记录残留
tableRef.value?.clearCheckboxRow();
tableRef.value?.clearCheckboxReserve();
await query();
},
});
}
const { exportBlob, exportLoading, buildExportFileName } =
useBlobExport(rechargeExport);
async function handleExport() {
// 构建表单请求参数
const formValues = (await searchFormRef.value?.getValues()) ?? {};
// 文件名
const fileName = buildExportFileName('充值记录数据');
exportBlob({ data: formValues, fileName });
}
function handleSearchSubmit(data: Record<string, any>) {
reload(data);
}
function handleSearchReset() {
reload();
}
function getCheckedRows() {
const table = tableRef.value;
if (!table) {
return [];
}
return [
...table.getCheckboxRecords(),
...table.getCheckboxReserveRecords(),
] as Recharge[];
}
function syncCheckedRows() {
checkedRows.value = getCheckedRows();
}
</script>
<template>
<Page :auto-content-height="true">
<Spin
:styles="{ root: { height: '100%' }, container: { height: '100%' } }"
:spinning="tableLoading"
size="large"
:delay="300"
>
<div class="flex h-full flex-col gap-2">
<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"
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
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>
</Spin>
<RechargeDrawer @reload="() => query()" />
</Page>
</template>
<script setup lang="ts">
import type { Recharge } from '@/api/ai/recharge/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 { useVbenDrawer } from '@/components';
import {
FormInputNumber as InputNumber,
FormSelect as Select,
FormTextArea as TextArea,
} from '@/components/global/form';
import { DictEnum } from '@/constants';
import { $t } from '@/locales';
import { cloneDeep, getPopupContainer } from '@/utils';
import { getDictOptions } from '@/utils/dict';
import { useBeforeCloseDiff } from '@/utils/popup';
import { DatePicker, Form, FormItem } from 'antdv-next';
const emit = defineEmits<{ reload: [] }>();
const isUpdate = ref(false);
const title = computed(() => {
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
});
type FormData = Partial<Recharge>;
// 国内/国外为固定二值(非字典,Clarify C2)
const regionOptions = [
{ label: '国内', value: '国内' },
{ label: '国外', value: '国外' },
];
function getDefaultValues(): FormData {
return {
id: undefined,
toolName: undefined,
region: undefined,
rechargeCategory: undefined,
rechargeTime: undefined,
expireTime: undefined,
rechargePoints: undefined,
amountUsd: undefined,
amountCny: undefined,
accountCount: undefined,
remainingPoints: undefined,
remark: '',
};
}
const formData = ref<FormData>(getDefaultValues());
const formInstance = ref<FormInstance>();
const formRules = ref<AntdFormRules<FormData>>({
toolName: [{ required: true, message: $t('ui.formRules.selectRequired') }],
region: [{ required: true, message: $t('ui.formRules.selectRequired') }],
rechargeCategory: [
{ required: true, message: $t('ui.formRules.selectRequired') },
],
rechargeTime: [{ required: true, message: $t('ui.formRules.required') }],
amountCny: [{ required: true, message: $t('ui.formRules.required') }],
});
function customFormValueGetter() {
return JSON.stringify(formData.value);
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: customFormValueGetter,
currentGetter: customFormValueGetter,
},
);
const [BasicDrawer, drawerApi] = useVbenDrawer({
onBeforeClose,
onClosed: handleClosed,
onConfirm: handleConfirm,
async onOpenChange(isOpen) {
if (!isOpen) {
return null;
}
drawerApi.drawerLoading(true);
const { id } = drawerApi.getData() as { id?: number | string };
isUpdate.value = !!id;
// 更新 && 赋值
if (isUpdate.value && id) {
const record = await rechargeInfo(id);
formData.value = {
...getDefaultValues(),
...record,
remark: record.remark ?? '',
};
}
await markInitialized();
drawerApi.drawerLoading(false);
},
});
async function handleConfirm() {
try {
drawerApi.lock(true);
await formInstance.value?.validate();
const data = cloneDeep(formData.value);
await (isUpdate.value ? rechargeUpdate(data) : rechargeAdd(data));
resetInitialized();
emit('reload');
drawerApi.close();
} catch (error) {
console.error(error);
} finally {
drawerApi.lock(false);
}
}
async function handleClosed() {
formData.value = getDefaultValues();
formInstance.value?.resetFields();
resetInitialized();
}
</script>
<template>
<BasicDrawer :title="title" :size="700">
<Form
ref="formInstance"
:model="formData"
:label-col="{ style: { width: '140px' } }"
>
<FormItem label="AI工具/平台名称" name="toolName" :rules="formRules.toolName">
<Select
class="w-full"
:allow-clear="false"
option-filter-prop="label"
show-search
:get-popup-container="getPopupContainer"
:options="getDictOptions(DictEnum.AI_TOOL_NAME)"
v-model:value="formData.toolName"
/>
</FormItem>
<FormItem label="国内/国外" name="region" :rules="formRules.region">
<Select
class="w-full"
:allow-clear="false"
:get-popup-container="getPopupContainer"
:options="regionOptions"
v-model:value="formData.region"
/>
</FormItem>
<FormItem
label="充值分类"
name="rechargeCategory"
:rules="formRules.rechargeCategory"
>
<Select
class="w-full"
:allow-clear="false"
:get-popup-container="getPopupContainer"
:options="getDictOptions(DictEnum.AI_RECHARGE_CATEGORY)"
v-model:value="formData.rechargeCategory"
/>
</FormItem>
<FormItem
label="充值完成时间"
name="rechargeTime"
:rules="formRules.rechargeTime"
>
<DatePicker
class="w-full"
show-time
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
v-model:value="formData.rechargeTime"
/>
</FormItem>
<FormItem label="到期时间" name="expireTime">
<DatePicker
class="w-full"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
v-model:value="formData.expireTime"
/>
</FormItem>
<FormItem label="充值积分" name="rechargePoints">
<InputNumber
class="w-full"
:style="{ width: '50%' }"
:min="0"
:precision="2"
v-model:value="formData.rechargePoints"
/>
</FormItem>
<FormItem label="充值金额(美元)" name="amountUsd">
<InputNumber
class="w-full"
:style="{ width: '50%' }"
:min="0"
:precision="2"
v-model:value="formData.amountUsd"
/>
</FormItem>
<FormItem label="充值金额(人民币)" name="amountCny" :rules="formRules.amountCny">
<InputNumber
class="w-full"
:style="{ width: '50%' }"
:min="0"
:precision="2"
v-model:value="formData.amountCny"
/>
</FormItem>
<FormItem label="开通账号数量" name="accountCount">
<InputNumber
class="w-full"
:style="{ width: '50%' }"
:min="0"
:precision="0"
v-model:value="formData.accountCount"
/>
</FormItem>
<FormItem label="剩余积分" name="remainingPoints">
<InputNumber
class="w-full"
:style="{ width: '50%' }"
:min="0"
:precision="2"
v-model:value="formData.remainingPoints"
/>
</FormItem>
<FormItem label="备注" name="remark">
<TextArea
allow-clear
class="w-full"
:rows="4"
:maxlength="200"
show-count
v-model:value="formData.remark"
/>
</FormItem>
</Form>
</BasicDrawer>
</template>
<script setup lang="ts">
import type { FormInstance } from 'antdv-next';
import type { Dayjs } from 'dayjs';
import { ref } from 'vue';
import { 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 { getDictOptions } from '@/utils/dict';
import { Card, DateRangePicker, Form, FormItem } from 'antdv-next';
const emit = defineEmits<{
reset: [];
submit: [record: Record<string, any>];
}>();
interface RechargeSearchFormParams {
rechargeTime?: [Dayjs, Dayjs];
toolName?: string;
region?: string;
rechargeCategory?: string;
}
// 国内/国外为固定二值(非字典,Clarify C2)
const regionOptions = [
{ label: '国内', value: '国内' },
{ label: '国外', value: '国外' },
];
const model = ref<RechargeSearchFormParams>({
rechargeTime: undefined,
toolName: undefined,
region: undefined,
rechargeCategory: undefined,
});
const formInstance = ref<FormInstance>();
const searchCollapsed = ref(false);
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]),
};
Reflect.deleteProperty(params, 'rechargeTime');
}
return params;
}
async function getValues() {
return buildSearchParams(model.value);
}
async function handleSubmit() {
await formInstance.value?.validate();
emit('submit', await getValues());
}
async function handleReset() {
formInstance.value?.resetFields();
emit('reset');
}
defineExpose({
getValues,
resetFields: () => formInstance.value?.resetFields(),
});
</script>
<template>
<Card size="small">
<Form
ref="formInstance"
:model="model"
:label-col="{ style: { width: '120px' } }"
@keydown.enter.prevent="handleSubmit"
>
<div :class="tableSeachClass">
<template v-if="!searchCollapsed">
<FormItem label="充值时间" name="rechargeTime">
<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)"
/>
</FormItem>
<FormItem label="国内/国外" name="region">
<FormSelect
allow-clear
v-model:value="model.region"
:options="regionOptions"
/>
</FormItem>
<FormItem label="充值分类" name="rechargeCategory">
<FormSelect
allow-clear
v-model:value="model.rechargeCategory"
:options="getDictOptions(DictEnum.AI_RECHARGE_CATEGORY)"
/>
</FormItem>
</template>
<!-- [grid-column-end:-1] 始终定位到最后一列,justify-self-end 靠右对齐 -->
<div class="[grid-column-end:-1] flex items-baseline justify-end gap-4">
<SearchButtonGroup
collapsible
v-model:collapsed="searchCollapsed"
@reset="handleReset"
@submit="handleSubmit"
/>
</div>
</div>
</Form>
</Card>
</template>
...@@ -37,7 +37,7 @@ export default defineConfig(async () => { ...@@ -37,7 +37,7 @@ export default defineConfig(async () => {
changeOrigin: true, changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ''), // rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址 // mock代理目标地址
target: 'http://127.0.0.1:9000', target: 'http://127.0.0.1:8090',
ws: true, ws: true,
}, },
}, },
......
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