新增预警页面
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package com.jeesite.modules.erp.dao;
|
||||
|
||||
import com.jeesite.common.dao.CrudDao;
|
||||
import com.jeesite.common.mybatis.annotation.MyBatisDao;
|
||||
import com.jeesite.modules.erp.entity.ErpCategoryFlow;
|
||||
|
||||
/**
|
||||
* erp_category_flowDAO接口
|
||||
* @author gaoxq
|
||||
* @version 2025-12-13
|
||||
*/
|
||||
@MyBatisDao(dataSourceName="work")
|
||||
public interface ErpCategoryFlowDao extends CrudDao<ErpCategoryFlow> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.jeesite.modules.erp.dao;
|
||||
|
||||
import com.jeesite.common.dao.CrudDao;
|
||||
import com.jeesite.common.mybatis.annotation.MyBatisDao;
|
||||
import com.jeesite.modules.erp.entity.ErpIncExpRatio;
|
||||
|
||||
/**
|
||||
* 支出占比DAO接口
|
||||
* @author gaoxq
|
||||
* @version 2025-12-13
|
||||
*/
|
||||
@MyBatisDao(dataSourceName="work")
|
||||
public interface ErpIncExpRatioDao extends CrudDao<ErpIncExpRatio> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.jeesite.modules.erp.entity;
|
||||
|
||||
|
||||
import com.jeesite.common.entity.DataEntity;
|
||||
import com.jeesite.common.mybatis.annotation.Column;
|
||||
import com.jeesite.common.mybatis.annotation.Table;
|
||||
import com.jeesite.common.mybatis.mapper.query.QueryType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* erp_category_flowEntity
|
||||
*
|
||||
* @author gaoxq
|
||||
* @version 2025-12-13
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Table(name = "erp_category_flow_view", alias = "a", label = "用途信息", columns = {
|
||||
@Column(name = "cycle_type", attrName = "cycleType", label = "cycle_type"),
|
||||
@Column(name = "category_name", attrName = "categoryName", label = "分类名称", queryType = QueryType.LIKE),
|
||||
@Column(name = "stat_date", attrName = "statDate", label = "stat_date"),
|
||||
@Column(name = "this_income", attrName = "thisIncome", label = "this_income"),
|
||||
@Column(name = "prev_income", attrName = "prevIncome", label = "prev_income"),
|
||||
@Column(name = "income_mom", attrName = "incomeMom", label = "income_mom", isUpdateForce = true),
|
||||
@Column(name = "this_expense", attrName = "thisExpense", label = "this_expense"),
|
||||
@Column(name = "prev_expense", attrName = "prevExpense", label = "prev_expense"),
|
||||
@Column(name = "expense_mom", attrName = "expenseMom", label = "expense_mom", isUpdateForce = true),
|
||||
}, orderBy = "a.stat_date"
|
||||
)
|
||||
@Data
|
||||
public class ErpCategoryFlow extends DataEntity<ErpCategoryFlow> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String cycleType; // cycle_type
|
||||
private String categoryName; // 分类名称
|
||||
private String statDate; // 交易日期
|
||||
private Double thisIncome; // 本期收入
|
||||
private Double prevIncome; // 上期收入
|
||||
private Double incomeMom; // 收入环比
|
||||
private Double thisExpense; // 本期支出
|
||||
private Double prevExpense; // 上期支出
|
||||
private Double expenseMom; // 支出环比
|
||||
|
||||
public ErpCategoryFlow() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ErpCategoryFlow(String id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
|
||||
public String getStatDate_gte() {
|
||||
return sqlMap.getWhere().getValue("stat_date", QueryType.GTE);
|
||||
}
|
||||
|
||||
public void setStatDate_gte(String statDate) {
|
||||
sqlMap.getWhere().and("stat_date", QueryType.GTE, statDate);
|
||||
}
|
||||
|
||||
public String getStatDate_lte() {
|
||||
return sqlMap.getWhere().getValue("stat_date", QueryType.LTE);
|
||||
}
|
||||
|
||||
public void setStatDate_lte(String statDate) {
|
||||
sqlMap.getWhere().and("stat_date", QueryType.LTE, statDate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.jeesite.modules.erp.entity;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import com.jeesite.common.entity.DataEntity;
|
||||
import com.jeesite.common.mybatis.annotation.Column;
|
||||
import com.jeesite.common.mybatis.annotation.Table;
|
||||
import com.jeesite.common.mybatis.mapper.query.QueryType;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 支出占比Entity
|
||||
*
|
||||
* @author gaoxq
|
||||
* @version 2025-12-13
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Table(name = "erp_inc_exp_ratio_view", alias = "a", label = "支出占比信息", columns = {
|
||||
@Column(name = "cycle_type", attrName = "cycleType", label = "cycle_type"),
|
||||
@Column(name = "stat_date", attrName = "statDate", label = "stat_date"),
|
||||
@Column(name = "income_amount", attrName = "incomeAmount", label = "income_amount", isUpdateForce = true),
|
||||
@Column(name = "expense_amount", attrName = "expenseAmount", label = "expense_amount", isUpdateForce = true),
|
||||
@Column(name = "expense_ratio", attrName = "expenseRatio", label = "expense_ratio"),
|
||||
}, orderBy = "a.stat_date"
|
||||
)
|
||||
@Data
|
||||
public class ErpIncExpRatio extends DataEntity<ErpIncExpRatio> implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String cycleType; // cycle_type
|
||||
private String statDate; // 日期
|
||||
private Double incomeAmount; // 收入
|
||||
private Double expenseAmount; // 支出
|
||||
private Double expenseRatio; // 占比
|
||||
|
||||
public ErpIncExpRatio() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ErpIncExpRatio(String id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public String getStatDate_gte() {
|
||||
return sqlMap.getWhere().getValue("stat_date", QueryType.GTE);
|
||||
}
|
||||
|
||||
public void setStatDate_gte(String statDate) {
|
||||
sqlMap.getWhere().and("stat_date", QueryType.GTE, statDate);
|
||||
}
|
||||
|
||||
public String getStatDate_lte() {
|
||||
return sqlMap.getWhere().getValue("stat_date", QueryType.LTE);
|
||||
}
|
||||
|
||||
public void setStatDate_lte(String statDate) {
|
||||
sqlMap.getWhere().and("stat_date", QueryType.LTE, statDate);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import java.io.Serial;
|
||||
@Column(name = "this_value", attrName = "thisValue", label = "当期金额", isQuery = false, isUpdateForce = true),
|
||||
@Column(name = "prev_value", attrName = "prevValue", label = "上期金额", isQuery = false, isUpdateForce = true),
|
||||
@Column(name = "mom_rate", attrName = "momRate", label = "环比", isQuery = false, isUpdateForce = true),
|
||||
@Column(name = "f_cycle", attrName = "fcycle", label = "周期类型"),
|
||||
@Column(name = "cycle_type", attrName = "cycleType", label = "周期类型"),
|
||||
}, orderBy = "a.c_date"
|
||||
)
|
||||
@Data
|
||||
@@ -42,7 +42,7 @@ public class ErpSummaryAll extends DataEntity<ErpSummaryAll> implements Serializ
|
||||
private Double thisValue; // 当期金额
|
||||
private Double prevValue; // 上期金额
|
||||
private Double momRate; // 环比
|
||||
private String fcycle; // 周期类型
|
||||
private String cycleType; // 周期类型
|
||||
|
||||
@ExcelFields({
|
||||
@ExcelField(title = "记录时间", attrName = "createTime", align = Align.CENTER, sort = 10, dataFormat = "yyyy-MM-dd hh:mm"),
|
||||
@@ -52,7 +52,7 @@ public class ErpSummaryAll extends DataEntity<ErpSummaryAll> implements Serializ
|
||||
@ExcelField(title = "当期金额", attrName = "thisValue", align = Align.CENTER, sort = 50),
|
||||
@ExcelField(title = "上期金额", attrName = "prevValue", align = Align.CENTER, sort = 60),
|
||||
@ExcelField(title = "环比", attrName = "momRate", align = Align.CENTER, sort = 70),
|
||||
@ExcelField(title = "周期类型", attrName = "fcycle", dictType = "report_cycle", align = Align.CENTER, sort = 80),
|
||||
@ExcelField(title = "周期类型", attrName = "cycleType", dictType = "report_cycle", align = Align.CENTER, sort = 80),
|
||||
})
|
||||
public ErpSummaryAll() {
|
||||
this(null);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.jeesite.modules.erp.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.jeesite.common.entity.Page;
|
||||
import com.jeesite.common.service.CrudService;
|
||||
import com.jeesite.modules.erp.entity.ErpCategoryFlow;
|
||||
import com.jeesite.modules.erp.dao.ErpCategoryFlowDao;
|
||||
|
||||
/**
|
||||
* erp_category_flowService
|
||||
* @author gaoxq
|
||||
* @version 2025-12-13
|
||||
*/
|
||||
@Service
|
||||
public class ErpCategoryFlowService extends CrudService<ErpCategoryFlowDao, ErpCategoryFlow> {
|
||||
|
||||
/**
|
||||
* 获取单条数据
|
||||
* @param erpCategoryFlow 主键
|
||||
*/
|
||||
@Override
|
||||
public ErpCategoryFlow get(ErpCategoryFlow erpCategoryFlow) {
|
||||
return super.get(erpCategoryFlow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询分页数据
|
||||
* @param erpCategoryFlow 查询条件
|
||||
* @param erpCategoryFlow page 分页对象
|
||||
*/
|
||||
@Override
|
||||
public Page<ErpCategoryFlow> findPage(ErpCategoryFlow erpCategoryFlow) {
|
||||
return super.findPage(erpCategoryFlow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表数据
|
||||
* @param erpCategoryFlow 查询条件
|
||||
*/
|
||||
@Override
|
||||
public List<ErpCategoryFlow> findList(ErpCategoryFlow erpCategoryFlow) {
|
||||
return super.findList(erpCategoryFlow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据(插入或更新)
|
||||
* @param erpCategoryFlow 数据对象
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void save(ErpCategoryFlow erpCategoryFlow) {
|
||||
super.save(erpCategoryFlow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
* @param erpCategoryFlow 数据对象
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateStatus(ErpCategoryFlow erpCategoryFlow) {
|
||||
super.updateStatus(erpCategoryFlow);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param erpCategoryFlow 数据对象
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(ErpCategoryFlow erpCategoryFlow) {
|
||||
erpCategoryFlow.sqlMap().markIdDelete(); // 逻辑删除时标记ID值
|
||||
super.delete(erpCategoryFlow);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.jeesite.modules.erp.service;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.jeesite.common.entity.Page;
|
||||
import com.jeesite.common.service.CrudService;
|
||||
import com.jeesite.modules.erp.entity.ErpIncExpRatio;
|
||||
import com.jeesite.modules.erp.dao.ErpIncExpRatioDao;
|
||||
|
||||
/**
|
||||
* 支出占比Service
|
||||
* @author gaoxq
|
||||
* @version 2025-12-13
|
||||
*/
|
||||
@Service
|
||||
public class ErpIncExpRatioService extends CrudService<ErpIncExpRatioDao, ErpIncExpRatio> {
|
||||
|
||||
/**
|
||||
* 获取单条数据
|
||||
* @param erpIncExpRatio 主键
|
||||
*/
|
||||
@Override
|
||||
public ErpIncExpRatio get(ErpIncExpRatio erpIncExpRatio) {
|
||||
return super.get(erpIncExpRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询分页数据
|
||||
* @param erpIncExpRatio 查询条件
|
||||
* @param erpIncExpRatio page 分页对象
|
||||
*/
|
||||
@Override
|
||||
public Page<ErpIncExpRatio> findPage(ErpIncExpRatio erpIncExpRatio) {
|
||||
return super.findPage(erpIncExpRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表数据
|
||||
* @param erpIncExpRatio 查询条件
|
||||
*/
|
||||
@Override
|
||||
public List<ErpIncExpRatio> findList(ErpIncExpRatio erpIncExpRatio) {
|
||||
return super.findList(erpIncExpRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据(插入或更新)
|
||||
* @param erpIncExpRatio 数据对象
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void save(ErpIncExpRatio erpIncExpRatio) {
|
||||
super.save(erpIncExpRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
* @param erpIncExpRatio 数据对象
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void updateStatus(ErpIncExpRatio erpIncExpRatio) {
|
||||
super.updateStatus(erpIncExpRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param erpIncExpRatio 数据对象
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(ErpIncExpRatio erpIncExpRatio) {
|
||||
erpIncExpRatio.sqlMap().markIdDelete(); // 逻辑删除时标记ID值
|
||||
super.delete(erpIncExpRatio);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.jeesite.modules.erp.web;
|
||||
|
||||
import com.jeesite.modules.app.utils.vDate;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.jeesite.common.config.Global;
|
||||
import com.jeesite.common.entity.Page;
|
||||
import com.jeesite.common.web.BaseController;
|
||||
import com.jeesite.modules.erp.entity.ErpCategoryFlow;
|
||||
import com.jeesite.modules.erp.service.ErpCategoryFlowService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* erp_category_flowController
|
||||
*
|
||||
* @author gaoxq
|
||||
* @version 2025-12-13
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping(value = "${adminPath}/erp/categoryFlow")
|
||||
public class ErpCategoryFlowController extends BaseController {
|
||||
|
||||
private final ErpCategoryFlowService erpCategoryFlowService;
|
||||
|
||||
public ErpCategoryFlowController(ErpCategoryFlowService erpCategoryFlowService) {
|
||||
this.erpCategoryFlowService = erpCategoryFlowService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
@ModelAttribute
|
||||
public ErpCategoryFlow get(Long id, boolean isNewRecord) {
|
||||
return erpCategoryFlowService.get(id, isNewRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*/
|
||||
@RequiresPermissions("erp:categoryFlow:view")
|
||||
@RequestMapping(value = {"list", ""})
|
||||
public String list(ErpCategoryFlow erpCategoryFlow, Model model) {
|
||||
model.addAttribute("erpCategoryFlow", erpCategoryFlow);
|
||||
return "modules/erp/erpCategoryFlowList";
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表数据
|
||||
*/
|
||||
@RequiresPermissions("erp:categoryFlow:view")
|
||||
@RequestMapping(value = "listData")
|
||||
@ResponseBody
|
||||
public Page<ErpCategoryFlow> listData(ErpCategoryFlow erpCategoryFlow, HttpServletRequest request, HttpServletResponse response) {
|
||||
erpCategoryFlow.setPage(new Page<>(request, response));
|
||||
Page<ErpCategoryFlow> page = erpCategoryFlowService.findPage(erpCategoryFlow);
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看编辑表单
|
||||
*/
|
||||
@RequiresPermissions("erp:categoryFlow:view")
|
||||
@RequestMapping(value = "form")
|
||||
public String form(ErpCategoryFlow erpCategoryFlow, Model model) {
|
||||
model.addAttribute("erpCategoryFlow", erpCategoryFlow);
|
||||
return "modules/erp/erpCategoryFlowForm";
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据
|
||||
*/
|
||||
@RequiresPermissions("erp:categoryFlow:edit")
|
||||
@PostMapping(value = "save")
|
||||
@ResponseBody
|
||||
public String save(@Validated ErpCategoryFlow erpCategoryFlow) {
|
||||
erpCategoryFlowService.save(erpCategoryFlow);
|
||||
return renderResult(Global.TRUE, text("保存用途信息成功!"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
*/
|
||||
@RequiresPermissions("erp:categoryFlow:edit")
|
||||
@RequestMapping(value = "delete")
|
||||
@ResponseBody
|
||||
public String delete(ErpCategoryFlow erpCategoryFlow) {
|
||||
erpCategoryFlowService.delete(erpCategoryFlow);
|
||||
return renderResult(Global.TRUE, text("删除用途信息成功!"));
|
||||
}
|
||||
|
||||
|
||||
@RequestMapping(value = "listAll")
|
||||
@ResponseBody
|
||||
public List<ErpCategoryFlow> listAll(ErpCategoryFlow erpCategoryFlow) {
|
||||
erpCategoryFlow.setStatDate_gte(vDate.calculateStartCycleCode(erpCategoryFlow.getCycleType()));
|
||||
return erpCategoryFlowService.findList(erpCategoryFlow);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.jeesite.modules.erp.web;
|
||||
|
||||
import com.jeesite.modules.app.utils.vDate;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.jeesite.common.config.Global;
|
||||
import com.jeesite.common.entity.Page;
|
||||
import com.jeesite.common.web.BaseController;
|
||||
import com.jeesite.modules.erp.entity.ErpIncExpRatio;
|
||||
import com.jeesite.modules.erp.service.ErpIncExpRatioService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 支出占比Controller
|
||||
*
|
||||
* @author gaoxq
|
||||
* @version 2025-12-13
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping(value = "${adminPath}/erp/incExpRatio")
|
||||
public class ErpIncExpRatioController extends BaseController {
|
||||
|
||||
private final ErpIncExpRatioService erpIncExpRatioService;
|
||||
|
||||
public ErpIncExpRatioController(ErpIncExpRatioService erpIncExpRatioService) {
|
||||
this.erpIncExpRatioService = erpIncExpRatioService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据
|
||||
*/
|
||||
@ModelAttribute
|
||||
public ErpIncExpRatio get(Long id, boolean isNewRecord) {
|
||||
return erpIncExpRatioService.get(id, isNewRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*/
|
||||
@RequiresPermissions("erp:incExpRatio:view")
|
||||
@RequestMapping(value = {"list", ""})
|
||||
public String list(ErpIncExpRatio erpIncExpRatio, Model model) {
|
||||
model.addAttribute("erpIncExpRatio", erpIncExpRatio);
|
||||
return "modules/erp/erpIncExpRatioList";
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表数据
|
||||
*/
|
||||
@RequiresPermissions("erp:incExpRatio:view")
|
||||
@RequestMapping(value = "listData")
|
||||
@ResponseBody
|
||||
public Page<ErpIncExpRatio> listData(ErpIncExpRatio erpIncExpRatio, HttpServletRequest request, HttpServletResponse response) {
|
||||
erpIncExpRatio.setPage(new Page<>(request, response));
|
||||
Page<ErpIncExpRatio> page = erpIncExpRatioService.findPage(erpIncExpRatio);
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看编辑表单
|
||||
*/
|
||||
@RequiresPermissions("erp:incExpRatio:view")
|
||||
@RequestMapping(value = "form")
|
||||
public String form(ErpIncExpRatio erpIncExpRatio, Model model) {
|
||||
model.addAttribute("erpIncExpRatio", erpIncExpRatio);
|
||||
return "modules/erp/erpIncExpRatioForm";
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据
|
||||
*/
|
||||
@RequiresPermissions("erp:incExpRatio:edit")
|
||||
@PostMapping(value = "save")
|
||||
@ResponseBody
|
||||
public String save(@Validated ErpIncExpRatio erpIncExpRatio) {
|
||||
erpIncExpRatioService.save(erpIncExpRatio);
|
||||
return renderResult(Global.TRUE, text("保存支出占比成功!"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
*/
|
||||
@RequiresPermissions("erp:incExpRatio:edit")
|
||||
@RequestMapping(value = "delete")
|
||||
@ResponseBody
|
||||
public String delete(ErpIncExpRatio erpIncExpRatio) {
|
||||
erpIncExpRatioService.delete(erpIncExpRatio);
|
||||
return renderResult(Global.TRUE, text("删除支出占比成功!"));
|
||||
}
|
||||
|
||||
@RequestMapping(value = "listAll")
|
||||
@ResponseBody
|
||||
public List<ErpIncExpRatio> listAll(ErpIncExpRatio erpIncExpRatio) {
|
||||
erpIncExpRatio.setStatDate_gte(vDate.calculateStartCycleCode(erpIncExpRatio.getCycleType()));
|
||||
return erpIncExpRatioService.findList(erpIncExpRatio);
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ public class ErpSummaryAllController extends BaseController {
|
||||
@RequestMapping(value = "listAll")
|
||||
@ResponseBody
|
||||
public List<ErpSummaryAll> listAll(ErpSummaryAll erpSummaryAll) {
|
||||
erpSummaryAll.setCdate_gte(vDate.calculateStartCycleCode(erpSummaryAll.getFcycle()));
|
||||
erpSummaryAll.setCdate_gte(vDate.calculateStartCycleCode(erpSummaryAll.getCycleType()));
|
||||
return erpSummaryAllService.findList(erpSummaryAll);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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.jeesite.modules.erp.dao.ErpCategoryFlowDao">
|
||||
|
||||
<!-- 查询数据
|
||||
<select id="findList" resultType="ErpCategoryFlow">
|
||||
SELECT ${sqlMap.column.toSql()}
|
||||
FROM ${sqlMap.table.toSql()}
|
||||
<where>
|
||||
${sqlMap.where.toSql()}
|
||||
</where>
|
||||
ORDER BY ${sqlMap.order.toSql()}
|
||||
</select> -->
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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.jeesite.modules.erp.dao.ErpIncExpRatioDao">
|
||||
|
||||
<!-- 查询数据
|
||||
<select id="findList" resultType="ErpIncExpRatio">
|
||||
SELECT ${sqlMap.column.toSql()}
|
||||
FROM ${sqlMap.table.toSql()}
|
||||
<where>
|
||||
${sqlMap.where.toSql()}
|
||||
</where>
|
||||
ORDER BY ${sqlMap.order.toSql()}
|
||||
</select> -->
|
||||
|
||||
</mapper>
|
||||
@@ -38,6 +38,7 @@
|
||||
"@jeesite/types": "workspace:*",
|
||||
"@jeesite/vite": "workspace:*",
|
||||
"@jeesite/web": "workspace:*",
|
||||
"@jiaminghi/data-view": "^2.10.0",
|
||||
"ant-design-vue": "4.2.6",
|
||||
"axios": "1.12.2",
|
||||
"dayjs": "1.11.18",
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface BizDbConfig extends BasicModel<BizDbConfig> {
|
||||
dbUsername: string; // 数据库账号
|
||||
dbPassword: string; // 数据库密码
|
||||
description?: string; // 数据库描述
|
||||
isEnabled: number; // 是否启用
|
||||
isEnabled: string; // 是否启用
|
||||
dbSchemaName?: string; // schema名称
|
||||
updateTime?: string; // 更新时间
|
||||
ftenantId?: string; // 租户id
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
<template>
|
||||
<Card title="账户汇总图" style="width: 100%; height: 400px; margin: 4px 0;">
|
||||
<template #extra>
|
||||
<BasicForm
|
||||
:labelWidth="100"
|
||||
:schemas="schemas"
|
||||
:initialValues="defaultFormValues"
|
||||
@submit="handleFormSubmit"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</template>
|
||||
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -26,29 +17,10 @@ import type {
|
||||
BarSeriesOption
|
||||
} from 'echarts';
|
||||
|
||||
// 表单默认值
|
||||
const defaultFormValues = ref({
|
||||
cycleType: 'M'
|
||||
});
|
||||
const props = defineProps<{
|
||||
formParams: Record<string, any>; // 接收周期等参数
|
||||
}>();
|
||||
|
||||
// 表单配置
|
||||
const schemas: FormSchema[] = [
|
||||
{
|
||||
label: '周期',
|
||||
field: 'cycleType',
|
||||
defaultValue: defaultFormValues.value.cycleType,
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
dictType: 'report_cycle',
|
||||
allowClear: true,
|
||||
onChange: (value: string) => {
|
||||
defaultFormValues.value.cycleType = value;
|
||||
fetchList({ cycleType: value });
|
||||
}
|
||||
},
|
||||
colProps: { md: 24, lg: 24 },
|
||||
},
|
||||
];
|
||||
|
||||
// 图表DOM引用
|
||||
const chartDom = ref<HTMLDivElement | null>(null);
|
||||
@@ -104,13 +76,10 @@ const sortDates = (dates: string[]): string[] => {
|
||||
/**
|
||||
* 获取数据列表 - 增加类型约束和加载状态
|
||||
*/
|
||||
const fetchList = async (params: { cycleType?: string }) => {
|
||||
const fetchList = async (params: Record<string, any>) => {
|
||||
if (loading.value) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const cycleType = params.cycleType || defaultFormValues.value.cycleType;
|
||||
const result = await erpExpIncListAll({ cycleType });
|
||||
const result = await erpExpIncListAll(params);
|
||||
tableData.value = result || [];
|
||||
initChart();
|
||||
} catch (error) {
|
||||
@@ -450,15 +419,6 @@ const initChart = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理表单提交
|
||||
*/
|
||||
const handleFormSubmit = (values: { cycleType?: string }) => {
|
||||
if (values.cycleType) {
|
||||
fetchList({ cycleType: values.cycleType });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 窗口缩放自适应
|
||||
*/
|
||||
@@ -480,7 +440,7 @@ watch(tableData, () => {
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchList({});
|
||||
fetchList(props.formParams);
|
||||
window.addEventListener('resize', resizeChart);
|
||||
});
|
||||
|
||||
@@ -492,6 +452,14 @@ onUnmounted(() => {
|
||||
}
|
||||
clearTimeout((window as any).chartResizeTimer);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.formParams,
|
||||
async (newParams) => {
|
||||
await fetchList(newParams); // 参数变化时重新调用fetchList
|
||||
},
|
||||
{ deep: true, immediate: false } // deep: true 监听对象内部变化
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
<template>
|
||||
<Card title="周期汇总图" style="width: 100%; height: 400px; margin: 4px 0;">
|
||||
<template #extra>
|
||||
<BasicForm
|
||||
:labelWidth="100"
|
||||
:schemas="schemas"
|
||||
:initialValues="defaultFormValues"
|
||||
@submit="handleFormSubmit"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</template>
|
||||
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
|
||||
</Card>
|
||||
</template>
|
||||
@@ -26,29 +17,9 @@ import type {
|
||||
BarSeriesOption
|
||||
} from 'echarts';
|
||||
|
||||
// 表单默认值
|
||||
const defaultFormValues = ref({
|
||||
cycleType: 'M'
|
||||
});
|
||||
|
||||
// 表单配置
|
||||
const schemas: FormSchema[] = [
|
||||
{
|
||||
label: '周期',
|
||||
field: 'cycleType',
|
||||
defaultValue: defaultFormValues.value.cycleType,
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
dictType: 'report_cycle',
|
||||
allowClear: true,
|
||||
onChange: (value: string) => {
|
||||
defaultFormValues.value.cycleType = value;
|
||||
fetchList({ cycleType: value });
|
||||
}
|
||||
},
|
||||
colProps: { md: 24, lg: 24 },
|
||||
},
|
||||
];
|
||||
const props = defineProps<{
|
||||
formParams: Record<string, any>; // 接收周期等参数
|
||||
}>();
|
||||
|
||||
// 图表DOM引用
|
||||
const chartDom = ref<HTMLDivElement | null>(null);
|
||||
@@ -78,13 +49,12 @@ const convertToYuan = (num: number): number => {
|
||||
/**
|
||||
* 获取数据列表 - 增加类型约束和加载状态
|
||||
*/
|
||||
const fetchList = async (params: { cycleType?: string }) => {
|
||||
const fetchList = async (params: Record<string, any>) => {
|
||||
if (loading.value) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const cycleType = params.cycleType || defaultFormValues.value.cycleType;
|
||||
const result = await erpExpIncListAll({ cycleType });
|
||||
const result = await erpExpIncListAll(params);
|
||||
tableData.value = result || [];
|
||||
initChart();
|
||||
} catch (error) {
|
||||
@@ -410,15 +380,6 @@ const initChart = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理表单提交
|
||||
*/
|
||||
const handleFormSubmit = (values: { cycleType?: string }) => {
|
||||
if (values.cycleType) {
|
||||
fetchList({ cycleType: values.cycleType });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 窗口缩放自适应
|
||||
*/
|
||||
@@ -440,7 +401,7 @@ watch(tableData, () => {
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchList({});
|
||||
fetchList(props.formParams);
|
||||
window.addEventListener('resize', resizeChart);
|
||||
});
|
||||
|
||||
@@ -452,6 +413,14 @@ onUnmounted(() => {
|
||||
}
|
||||
clearTimeout((window as any).chartResizeTimer);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.formParams,
|
||||
async (newParams) => {
|
||||
await fetchList(newParams); // 参数变化时重新调用fetchList
|
||||
},
|
||||
{ deep: true, immediate: false } // deep: true 监听对象内部变化
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,66 +1,18 @@
|
||||
<template>
|
||||
<Card title="交易柱线图" style="width: 100%; height: 400px; margin: 4px 0;">
|
||||
<template #extra>
|
||||
<BasicForm
|
||||
:labelWidth="100"
|
||||
:schemas="schemas"
|
||||
:initialValues="defaultFormValues"
|
||||
@submit="handleFormSubmit"
|
||||
style="width: 400px;"
|
||||
/>
|
||||
</template>
|
||||
<Card title="支出柱线图" style="width: 100%; height: 400px; margin: 4px 0;">
|
||||
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
|
||||
</Card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Card } from 'ant-design-vue';
|
||||
import { BasicForm, FormSchema } from '@jeesite/core/components/Form';
|
||||
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
|
||||
import { ErpSummaryAll, erpSummaryAllListAll } from '@jeesite/erp/api/erp/summaryAll';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
// 表单默认值
|
||||
const defaultFormValues = {
|
||||
ctype: '1',
|
||||
fcycle: 'M'
|
||||
};
|
||||
|
||||
const currentFormValues = ref<Record<string, any>>({ ...defaultFormValues });
|
||||
|
||||
// 表单配置
|
||||
const schemas: FormSchema[] = [
|
||||
{
|
||||
label: '类型',
|
||||
field: 'ctype',
|
||||
component: 'Select',
|
||||
defaultValue: defaultFormValues.ctype,
|
||||
componentProps: {
|
||||
dictType: 'transaction_type',
|
||||
allowClear: true,
|
||||
onChange: (value: string) => {
|
||||
currentFormValues.value.ctype = value;
|
||||
fetchList(currentFormValues.value);
|
||||
}
|
||||
},
|
||||
colProps: { md: 12, lg: 12 },
|
||||
},
|
||||
{
|
||||
label: '周期',
|
||||
field: 'fcycle',
|
||||
component: 'Select',
|
||||
defaultValue: defaultFormValues.fcycle,
|
||||
componentProps: {
|
||||
dictType: 'report_cycle',
|
||||
allowClear: true,
|
||||
onChange: (value: string) => {
|
||||
currentFormValues.value.fcycle = value;
|
||||
fetchList(currentFormValues.value);
|
||||
}
|
||||
},
|
||||
colProps: { md: 12, lg: 12 },
|
||||
},
|
||||
];
|
||||
const props = defineProps<{
|
||||
formParams: Record<string, any>; // 接收周期等参数
|
||||
}>();
|
||||
|
||||
const listSummary = ref<ErpSummaryAll[]>([]);
|
||||
const chartDom = ref<HTMLDivElement | null>(null);
|
||||
@@ -117,23 +69,20 @@ const lineLabelConfig = {
|
||||
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)} %`
|
||||
};
|
||||
|
||||
// 表单提交处理
|
||||
const handleFormSubmit = (values: Record<string, any>) => {
|
||||
currentFormValues.value = { ...values };
|
||||
fetchList(currentFormValues.value);
|
||||
};
|
||||
|
||||
// 日期格式化(统一格式)
|
||||
const formatDate = (date: string | undefined): string => {
|
||||
if (!date) return '';
|
||||
return date.replace(/\//g, '-').trim();
|
||||
};
|
||||
|
||||
// 获取数据列表
|
||||
const fetchList = async (params: Record<string, any>) => {
|
||||
try {
|
||||
const result = await erpSummaryAllListAll(params);
|
||||
console.log('接口原始数据(元):', result);
|
||||
const requestParams = {
|
||||
...params,
|
||||
ctype: '1'
|
||||
};
|
||||
|
||||
const result = await erpSummaryAllListAll(requestParams);
|
||||
|
||||
// 过滤空数据 + 统一日期格式 + 排序
|
||||
const validData = (result || [])
|
||||
@@ -149,7 +98,6 @@ const fetchList = async (params: Record<string, any>) => {
|
||||
} catch (error) {
|
||||
console.error('获取数据列表失败:', error);
|
||||
listSummary.value = [];
|
||||
createMessage.error('数据加载失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -436,7 +384,7 @@ const debounceResize = () => {
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await fetchList(currentFormValues.value);
|
||||
await fetchList(props.formParams);
|
||||
window.addEventListener('resize', debounceResize);
|
||||
});
|
||||
|
||||
@@ -448,6 +396,14 @@ onUnmounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.formParams,
|
||||
async (newParams) => {
|
||||
await fetchList(newParams);
|
||||
},
|
||||
{ deep: true, immediate: false }
|
||||
);
|
||||
|
||||
// 监听数据变化自动更新图表
|
||||
watch(listSummary, () => {
|
||||
if (myChart) {
|
||||
@@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<Card title="收入柱线图" style="width: 100%; height: 400px; margin: 4px 0;">
|
||||
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
|
||||
</Card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Card } from 'ant-design-vue';
|
||||
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
|
||||
import { ErpSummaryAll, erpSummaryAllListAll } from '@jeesite/erp/api/erp/summaryAll';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const props = defineProps<{
|
||||
formParams: Record<string, any>; // 接收周期等参数
|
||||
}>();
|
||||
|
||||
const listSummary = ref<ErpSummaryAll[]>([]);
|
||||
const chartDom = ref<HTMLDivElement | null>(null);
|
||||
let myChart: echarts.ECharts | null = null;
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
// ========== 核心工具函数 ==========
|
||||
// 保留2位小数(增强容错)
|
||||
const formatNumber = (num: number | string | undefined, decimal = 2): number => {
|
||||
const parsed = Number(num);
|
||||
if (isNaN(parsed)) return 0;
|
||||
return Number(parsed.toFixed(decimal));
|
||||
};
|
||||
|
||||
// 元转万元(仅用于柱状图/Y轴展示)
|
||||
const toTenThousandYuan = (num: number | string | undefined): number => {
|
||||
const rawNum = formatNumber(num);
|
||||
return formatNumber(rawNum / 10000);
|
||||
};
|
||||
|
||||
// 千分位格式化(Tooltip显示原始元时使用)
|
||||
const formatWithThousandsSeparator = (num: number | string | undefined): string => {
|
||||
const parsed = Number(num);
|
||||
if (isNaN(parsed)) return '0.00';
|
||||
return parsed.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
|
||||
// ========== 柱状图标签仅显示数值(无单位) ==========
|
||||
const barLabelConfig = {
|
||||
show: true,
|
||||
position: 'top',
|
||||
distance: 3,
|
||||
textStyle: {
|
||||
fontSize: 11,
|
||||
color: '#333',
|
||||
fontWeight: '500'
|
||||
},
|
||||
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)}`
|
||||
};
|
||||
|
||||
// 折线图标签配置:百分比格式(保留单位)
|
||||
const lineLabelConfig = {
|
||||
show: true,
|
||||
position: 'top',
|
||||
distance: 5,
|
||||
textStyle: {
|
||||
fontSize: 11,
|
||||
color: '#f5222d',
|
||||
fontWeight: '500'
|
||||
},
|
||||
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)} %`
|
||||
};
|
||||
|
||||
// 日期格式化(统一格式)
|
||||
const formatDate = (date: string | undefined): string => {
|
||||
if (!date) return '';
|
||||
return date.replace(/\//g, '-').trim();
|
||||
};
|
||||
|
||||
const fetchList = async (params: Record<string, any>) => {
|
||||
try {
|
||||
const requestParams = {
|
||||
...params,
|
||||
ctype: '2'
|
||||
};
|
||||
|
||||
const result = await erpSummaryAllListAll(requestParams);
|
||||
|
||||
// 过滤空数据 + 统一日期格式 + 排序
|
||||
const validData = (result || [])
|
||||
.filter(item => item.cdate)
|
||||
.map(item => ({
|
||||
...item,
|
||||
cdate: formatDate(item.cdate)
|
||||
}))
|
||||
.sort((a, b) => a.cdate.localeCompare(b.cdate));
|
||||
|
||||
listSummary.value = validData;
|
||||
initChart();
|
||||
} catch (error) {
|
||||
console.error('获取数据列表失败:', error);
|
||||
listSummary.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 计算Y轴极值(适配万元展示层)
|
||||
const calculateYAxisExtent = (data: number[], isRate = false) => {
|
||||
if (data.length === 0) return isRate ? [-10, 10] : [0, 10]; // 万元默认范围
|
||||
|
||||
const formattedData = data.map(num => formatNumber(num));
|
||||
const min = Math.min(...formattedData);
|
||||
const max = Math.max(...formattedData);
|
||||
|
||||
// 万元轴内边距:10% 或最小1万元
|
||||
const padding = isRate
|
||||
? Math.max(Math.abs(max) * 0.2, Math.abs(min) * 0.2, 5)
|
||||
: Math.max((max - min) * 0.1, 1);
|
||||
|
||||
let minExtent = min - padding;
|
||||
let maxExtent = max + padding;
|
||||
|
||||
// 强制包含0点
|
||||
if (minExtent > 0) minExtent = 0;
|
||||
if (maxExtent < 0) maxExtent = 0;
|
||||
|
||||
// 百分比轴正负对称
|
||||
if (isRate) {
|
||||
const absMax = Math.max(Math.abs(minExtent), Math.abs(maxExtent));
|
||||
minExtent = -absMax;
|
||||
maxExtent = absMax;
|
||||
}
|
||||
|
||||
return [minExtent, maxExtent];
|
||||
};
|
||||
|
||||
// 初始化图表(核心修复:Tooltip兼容处理)
|
||||
const initChart = () => {
|
||||
if (!chartDom.value) return;
|
||||
|
||||
if (myChart) {
|
||||
myChart.dispose();
|
||||
}
|
||||
|
||||
myChart = echarts.init(chartDom.value);
|
||||
|
||||
// 提取X轴类目
|
||||
const xAxisData = listSummary.value.map(item => item.cdate).filter(Boolean);
|
||||
|
||||
// ========== 核心分离:展示数据(万元)和原始数据(元) ==========
|
||||
// 1. 展示层数据(万元):用于柱状图/Y轴/标签
|
||||
const thisValueShow = xAxisData.map(date => {
|
||||
const item = listSummary.value.find(i => i.cdate === date);
|
||||
return toTenThousandYuan(item?.thisValue);
|
||||
});
|
||||
const prevValueShow = xAxisData.map(date => {
|
||||
const item = listSummary.value.find(i => i.cdate === date);
|
||||
return toTenThousandYuan(item?.prevValue);
|
||||
});
|
||||
|
||||
// 2. 原始数据(元):直接从接口数据提取,不做任何换算,用于Tooltip
|
||||
const rawDataMap = new Map<string, { thisValue: any; prevValue: any; momRate: any }>();
|
||||
listSummary.value.forEach(item => {
|
||||
rawDataMap.set(item.cdate, {
|
||||
thisValue: item.thisValue, // 接口原始值(元)
|
||||
prevValue: item.prevValue, // 接口原始值(元)
|
||||
momRate: item.momRate // 接口原始值(%)
|
||||
});
|
||||
});
|
||||
|
||||
// 环比数据(不变)
|
||||
const rateData = xAxisData.map(date => {
|
||||
const item = listSummary.value.find(i => i.cdate === date);
|
||||
return formatNumber(item?.momRate);
|
||||
});
|
||||
|
||||
// 计算Y轴范围(基于万元展示数据)
|
||||
const amountData = [...thisValueShow, ...prevValueShow];
|
||||
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||
const [rateMin, rateMax] = calculateYAxisExtent(rateData, true);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 18, color: '#333' }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontSize: 12 },
|
||||
padding: 10,
|
||||
// ========== 核心修复:兼容params长度不固定 + 空值容错 ==========
|
||||
formatter: (params: any[]) => {
|
||||
if (!params || params.length === 0) return '暂无数据';
|
||||
|
||||
const date = params[0]?.axisValue || '';
|
||||
const rawData = rawDataMap.get(date) || { thisValue: 0, prevValue: 0, momRate: 0 };
|
||||
|
||||
let res = `<div style="font-weight: 500; margin-bottom: 4px;">${date || '未知日期'}</div>`;
|
||||
|
||||
// 动态遍历params,不依赖固定索引
|
||||
params.forEach((param) => {
|
||||
// 跳过无效参数
|
||||
if (!param || !param.seriesName) return;
|
||||
|
||||
let displayText = '';
|
||||
const seriesName = param.seriesName;
|
||||
|
||||
// 根据系列名称匹配原始数据,避免依赖param.value
|
||||
if (seriesName === '本期金额') {
|
||||
displayText = `${param.marker || ''}本期金额:<span style="font-weight: 500;">${
|
||||
formatWithThousandsSeparator(rawData.thisValue)
|
||||
}</span> 元`;
|
||||
} else if (seriesName === '上期金额') {
|
||||
displayText = `${param.marker || ''}上期金额:<span style="font-weight: 500;">${
|
||||
formatWithThousandsSeparator(rawData.prevValue)
|
||||
}</span> 元`;
|
||||
} else if (seriesName === '环比') {
|
||||
displayText = `${param.marker || ''}环比:<span style="font-weight: 500;">${
|
||||
formatNumber(rawData.momRate).toFixed(2)
|
||||
}</span> %`;
|
||||
}
|
||||
|
||||
// 只有有效文本才添加到Tooltip
|
||||
if (displayText) {
|
||||
res += `<div style="margin: 2px 0;">${displayText}</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['本期金额', '上期金额', '环比'],
|
||||
top: 10,
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
grid: {
|
||||
left: '8%',
|
||||
right: '12%',
|
||||
bottom: '10%',
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLabel: { fontSize: 12 },
|
||||
axisLine: { onZero: true }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '交易金额(万元)', // Y轴仍保留万元单位
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => formatNumber(value).toFixed(2) // 万元数值
|
||||
},
|
||||
min: amountMin,
|
||||
max: amountMax,
|
||||
axisLine: { onZero: true },
|
||||
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 5,
|
||||
scale: true
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '环比(%)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => `${formatNumber(value).toFixed(2)} %`
|
||||
},
|
||||
min: rateMin,
|
||||
max: rateMax,
|
||||
position: 'right',
|
||||
offset: 0,
|
||||
splitLine: { show: false },
|
||||
axisLine: { onZero: true },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
noDataLoadingOption: {
|
||||
text: '暂无数据',
|
||||
textStyle: { fontSize: 14, color: '#999' },
|
||||
effect: 'bubble',
|
||||
effectOption: { effect: { n: 0 } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '本期金额',
|
||||
type: 'bar',
|
||||
data: thisValueShow, // 万元展示数据
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#1890ff' },
|
||||
{ offset: 1, color: '#096dd9' }
|
||||
]),
|
||||
borderRadius: [8, 8, 0, 0]
|
||||
},
|
||||
barWidth: 25,
|
||||
barBorderRadius: [8, 8, 0, 0],
|
||||
label: barLabelConfig, // 仅显示数值的标签
|
||||
yAxisIndex: 0,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#40a9ff' },
|
||||
{ offset: 1, color: '#1890ff' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '上期金额',
|
||||
type: 'bar',
|
||||
data: prevValueShow, // 万元展示数据
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#52c41a' },
|
||||
{ offset: 1, color: '#389e0d' }
|
||||
]),
|
||||
borderRadius: [8, 8, 0, 0]
|
||||
},
|
||||
barWidth: 25,
|
||||
barBorderRadius: [8, 8, 0, 0],
|
||||
label: barLabelConfig, // 仅显示数值的标签
|
||||
yAxisIndex: 0,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#73d13d' },
|
||||
{ offset: 1, color: '#52c41a' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '环比',
|
||||
type: 'line',
|
||||
data: rateData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: { width: 3, color: '#f5222d' },
|
||||
itemStyle: {
|
||||
color: '#f5222d',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: lineLabelConfig,
|
||||
emphasis: { itemStyle: { symbolSize: 12 } },
|
||||
yAxisIndex: 1,
|
||||
markLine: {
|
||||
silent: true,
|
||||
lineStyle: { color: '#ccc', type: 'dashed', width: 1 },
|
||||
data: [{ yAxis: 0, lineStyle: { color: '#999', type: 'dashed' } }]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
myChart.setOption(option);
|
||||
};
|
||||
|
||||
// 调整图表大小(防抖+动画)
|
||||
const resizeChart = () => {
|
||||
if (myChart) {
|
||||
myChart.resize({
|
||||
animation: {
|
||||
duration: 300,
|
||||
easing: 'quadraticInOut'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口 resize 防抖处理
|
||||
let resizeTimer: number;
|
||||
const debounceResize = () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await fetchList(props.formParams);
|
||||
window.addEventListener('resize', debounceResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', debounceResize);
|
||||
if (myChart) {
|
||||
myChart.dispose();
|
||||
myChart = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.formParams,
|
||||
async (newParams) => {
|
||||
await fetchList(newParams);
|
||||
},
|
||||
{ deep: true, immediate: false }
|
||||
);
|
||||
|
||||
// 监听数据变化自动更新图表
|
||||
watch(listSummary, () => {
|
||||
if (myChart) {
|
||||
initChart();
|
||||
}
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-card) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.ant-card:hover) {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 优化图表tooltip样式 */
|
||||
:deep(.echarts-tooltip) {
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 优化表单样式 */
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.ant-select) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 优化柱状图标签溢出处理 */
|
||||
:deep(.echarts-label) {
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 优化Y轴名称间距 */
|
||||
:deep(.echarts-yaxis-name) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,550 @@
|
||||
<template>
|
||||
<Card title="收支柱线图" style="width: 100%; height: 400px; margin: 4px 0;">
|
||||
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Card } from 'ant-design-vue';
|
||||
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
|
||||
import { ErpIncExpRatio, erpIncExpRatioListAll } from '@jeesite/erp/api/erp/incExpRatio';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const props = defineProps<{
|
||||
formParams: Record<string, any>; // 接收周期等参数
|
||||
}>();
|
||||
|
||||
const rawData = ref<ErpIncExpRatio[]>([]); // 存储原始日期维度数据
|
||||
const chartDom = ref<HTMLDivElement | null>(null);
|
||||
let myChart: echarts.ECharts | null = null;
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
// ========== 核心工具函数 ==========
|
||||
// 保留2位小数(增强容错)
|
||||
const formatNumber = (num: number | string | undefined, decimal = 2): number => {
|
||||
const parsed = Number(num);
|
||||
if (isNaN(parsed)) return 0;
|
||||
return Number(parsed.toFixed(decimal));
|
||||
};
|
||||
|
||||
// 元转万元(用于柱状图/Y轴展示)
|
||||
const toTenThousandYuan = (num: number | string | undefined): number => {
|
||||
const rawNum = formatNumber(num);
|
||||
return formatNumber(rawNum / 10000);
|
||||
};
|
||||
|
||||
// 千分位格式化(Tooltip显示原始元时使用)
|
||||
const formatWithThousandsSeparator = (num: number | string | undefined): string => {
|
||||
const parsed = Number(num);
|
||||
if (isNaN(parsed)) return '0.00';
|
||||
return parsed.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
|
||||
// 计算净收益(收入 - 支出)
|
||||
const calculateNetProfit = (income: number | string | undefined, expense: number | string | undefined): number => {
|
||||
const incomeNum = formatNumber(income);
|
||||
const expenseNum = formatNumber(expense);
|
||||
return formatNumber(incomeNum - expenseNum);
|
||||
};
|
||||
|
||||
// 日期格式化(统一格式)
|
||||
const formatDate = (date: string | undefined): string => {
|
||||
if (!date) return '';
|
||||
return date.replace(/\//g, '-').trim();
|
||||
};
|
||||
|
||||
// 日期排序函数(X轴按时间升序)
|
||||
const sortDates = (data: ErpIncExpRatio[]): ErpIncExpRatio[] => {
|
||||
return data.sort((a, b) => {
|
||||
const dateA = new Date(a.statDate || '');
|
||||
const dateB = new Date(b.statDate || '');
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
});
|
||||
};
|
||||
|
||||
// ========== 图表标签配置 ==========
|
||||
// 柱状图标签配置(万元,保留2位小数)
|
||||
const barLabelConfig = {
|
||||
show: true,
|
||||
position: 'top',
|
||||
distance: 3,
|
||||
textStyle: {
|
||||
fontSize: 11,
|
||||
color: '#333',
|
||||
fontWeight: '500'
|
||||
},
|
||||
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)}`
|
||||
};
|
||||
|
||||
// 折线图标签配置(百分比格式)
|
||||
const lineLabelConfig = {
|
||||
show: true,
|
||||
position: 'top',
|
||||
distance: 5,
|
||||
textStyle: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
color: '#52c41a'
|
||||
},
|
||||
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)} %`
|
||||
};
|
||||
|
||||
// ========== 数据请求与处理 ==========
|
||||
const fetchList = async (params: Record<string, any>) => {
|
||||
try {
|
||||
const result = await erpIncExpRatioListAll(params);
|
||||
|
||||
// 数据处理:过滤空日期 + 格式化 + 排序
|
||||
const validData = (result || [])
|
||||
.filter(item => item.statDate) // 过滤无日期的数据
|
||||
.map(item => ({
|
||||
statDate: formatDate(item.statDate),
|
||||
incomeAmount: formatNumber(item.incomeAmount),
|
||||
expenseAmount: formatNumber(item.expenseAmount),
|
||||
expenseRatio: formatNumber(item.expenseRatio)
|
||||
})) as ErpIncExpRatio[];
|
||||
|
||||
// 按日期升序排序
|
||||
rawData.value = sortDates(validData);
|
||||
} catch (error) {
|
||||
console.error('获取数据列表失败:', error);
|
||||
rawData.value = [];
|
||||
createMessage.error('数据加载失败,请稍后重试');
|
||||
} finally {
|
||||
initChart();
|
||||
}
|
||||
};
|
||||
|
||||
// 计算Y轴极值
|
||||
const calculateYAxisExtent = (data: number[], isRate = false) => {
|
||||
if (data.length === 0) return isRate ? [0, 100] : [0, 10];
|
||||
|
||||
const formattedData = data.map(num => formatNumber(num));
|
||||
const min = Math.min(...formattedData);
|
||||
const max = Math.max(...formattedData);
|
||||
|
||||
const padding = isRate
|
||||
? Math.max((100 - max) * 0.1, 5)
|
||||
: Math.max((max - min) * 0.1, 1);
|
||||
|
||||
let minExtent = min - padding;
|
||||
let maxExtent = max + padding;
|
||||
|
||||
if (minExtent > 0) minExtent = 0;
|
||||
if (maxExtent < 0) maxExtent = 0;
|
||||
|
||||
// 占比轴限制0-100%
|
||||
if (isRate) {
|
||||
minExtent = Math.max(minExtent, 0);
|
||||
maxExtent = Math.min(maxExtent, 100);
|
||||
}
|
||||
|
||||
return [minExtent, maxExtent];
|
||||
};
|
||||
|
||||
// ========== 图表初始化 ==========
|
||||
const initChart = () => {
|
||||
if (!chartDom.value) return;
|
||||
|
||||
// 销毁旧实例
|
||||
if (myChart) {
|
||||
myChart.dispose();
|
||||
}
|
||||
|
||||
myChart = echarts.init(chartDom.value);
|
||||
|
||||
// 空数据处理
|
||||
if (rawData.value.length === 0) {
|
||||
myChart.setOption({
|
||||
title: {
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 18, color: '#333' }
|
||||
},
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
legend: { data: ['收入', '支出', '支出占比'], top: 10, textStyle: { fontSize: 12 } },
|
||||
grid: {
|
||||
left: '8%',
|
||||
right: '12%',
|
||||
bottom: '20%',
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
rotate: 45,
|
||||
overflow: 'truncate',
|
||||
width: 80
|
||||
},
|
||||
axisLine: { onZero: true }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额(万元)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => formatNumber(value).toFixed(2)
|
||||
},
|
||||
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 5,
|
||||
scale: true
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '支出占比(%)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => `${formatNumber(value).toFixed(2)} %`
|
||||
},
|
||||
position: 'right',
|
||||
offset: 0,
|
||||
splitLine: { show: false },
|
||||
axisLine: { onZero: true },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
series: [],
|
||||
noDataLoadingOption: {
|
||||
text: '暂无收入支出数据',
|
||||
textStyle: { fontSize: 16, color: '#666', fontWeight: '500' },
|
||||
position: 'center',
|
||||
effect: 'none'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取X轴日期
|
||||
const xAxisData = rawData.value.map(item => item.statDate!).filter(Boolean);
|
||||
|
||||
// 提取图表数据
|
||||
const incomeData = rawData.value.map(item => toTenThousandYuan(item.incomeAmount));
|
||||
const expenseData = rawData.value.map(item => toTenThousandYuan(item.expenseAmount));
|
||||
const ratioData = rawData.value.map(item => formatNumber(item.expenseRatio));
|
||||
|
||||
// 计算Y轴范围
|
||||
const amountData = [...incomeData, ...expenseData];
|
||||
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||
const [ratioMin, ratioMax] = calculateYAxisExtent(ratioData, true);
|
||||
|
||||
// 图表配置
|
||||
const option = {
|
||||
title: {
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 18, color: '#333' }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontSize: 12 },
|
||||
padding: 12,
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#e8e8e8',
|
||||
borderWidth: 1,
|
||||
formatter: (params: any[]) => {
|
||||
if (!params || params.length === 0) return '<div style="padding: 8px;">暂无数据</div>';
|
||||
|
||||
const currentDate = params[0]?.axisValue || '';
|
||||
const item = rawData.value.find(i => i.statDate === currentDate);
|
||||
|
||||
if (!item) return `<div style="padding: 8px;">${currentDate}:暂无明细</div>`;
|
||||
|
||||
// 计算净收益
|
||||
const netProfit = calculateNetProfit(item.incomeAmount, item.expenseAmount);
|
||||
// 净收益颜色:正数绿色,负数红色,零灰色
|
||||
const netProfitColor = netProfit > 0 ? '#52c41a' : netProfit < 0 ? '#f5222d' : '#666';
|
||||
|
||||
return `
|
||||
<div style="font-weight: 600; color: #333; margin-bottom: 8px; text-align: center;">${currentDate}</div>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 11px; min-width: 400px; margin-bottom: 8px;">
|
||||
<thead>
|
||||
<tr style="background-color: #f8f8f8;">
|
||||
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">总收入</th>
|
||||
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">总支出</th>
|
||||
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">净收益</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: #1890ff;">${formatWithThousandsSeparator(item.incomeAmount)} 元</td>
|
||||
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: #f5222d;">${formatWithThousandsSeparator(item.expenseAmount)} 元</td>
|
||||
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: ${netProfitColor};">${formatWithThousandsSeparator(netProfit)} 元</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="padding: 8px; border: 1px solid #e8e8e8; border-radius: 4px; background-color: #fafafa; font-size: 11px;">
|
||||
<span style="font-weight: 500; color: #333;">支出占比:</span>
|
||||
<span style="color: #52c41a;">${formatNumber(item.expenseRatio).toFixed(2)} %</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['收入', '支出', '支出占比'],
|
||||
top: 10,
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 12 }
|
||||
},
|
||||
grid: {
|
||||
left: '8%',
|
||||
right: '12%',
|
||||
bottom: '20%',
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
rotate: 45,
|
||||
overflow: 'truncate',
|
||||
width: 80,
|
||||
lineHeight: 1.5
|
||||
},
|
||||
axisLine: { onZero: true },
|
||||
axisTick: { alignWithLabel: true }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额(万元)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => formatNumber(value).toFixed(2)
|
||||
},
|
||||
min: amountMin,
|
||||
max: amountMax,
|
||||
axisLine: { onZero: true },
|
||||
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 5,
|
||||
scale: true
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '支出占比(%)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => `${formatNumber(value).toFixed(2)} %`
|
||||
},
|
||||
min: ratioMin,
|
||||
max: ratioMax,
|
||||
position: 'right',
|
||||
offset: 0,
|
||||
splitLine: { show: false },
|
||||
axisLine: { onZero: true },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
noDataLoadingOption: {
|
||||
text: '暂无数据',
|
||||
textStyle: { fontSize: 14, color: '#999' },
|
||||
effect: 'bubble',
|
||||
effectOption: { effect: { n: 0 } }
|
||||
},
|
||||
series: [
|
||||
// 收入柱状图
|
||||
{
|
||||
name: '收入',
|
||||
type: 'bar',
|
||||
data: incomeData,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#1890ff' },
|
||||
{ offset: 1, color: '#096dd9' }
|
||||
]),
|
||||
borderRadius: [8, 8, 0, 0]
|
||||
},
|
||||
barWidth: 25,
|
||||
label: barLabelConfig,
|
||||
yAxisIndex: 0,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#40a9ff' },
|
||||
{ offset: 1, color: '#1890ff' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
// 支出柱状图
|
||||
{
|
||||
name: '支出',
|
||||
type: 'bar',
|
||||
data: expenseData,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#f5222d' },
|
||||
{ offset: 1, color: '#cf1322' }
|
||||
]),
|
||||
borderRadius: [8, 8, 0, 0]
|
||||
},
|
||||
barWidth: 25,
|
||||
label: barLabelConfig,
|
||||
yAxisIndex: 0,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#ff4d4f' },
|
||||
{ offset: 1, color: '#f5222d' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
// 支出占比折线图
|
||||
{
|
||||
name: '支出占比',
|
||||
type: 'line',
|
||||
data: ratioData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: { width: 3, color: '#52c41a' },
|
||||
itemStyle: {
|
||||
color: '#52c41a',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: lineLabelConfig,
|
||||
emphasis: { itemStyle: { symbolSize: 12 } },
|
||||
yAxisIndex: 1,
|
||||
markLine: {
|
||||
silent: true,
|
||||
lineStyle: { color: '#ccc', type: 'dashed', width: 1 },
|
||||
data: [{ yAxis: 0, lineStyle: { color: '#999', type: 'dashed' } }]
|
||||
}
|
||||
}
|
||||
],
|
||||
animationDuration: 500,
|
||||
animationEasingUpdate: 'quinticInOut'
|
||||
};
|
||||
|
||||
myChart.setOption(option);
|
||||
};
|
||||
|
||||
// ========== 响应式调整 ==========
|
||||
const resizeChart = () => {
|
||||
if (myChart) {
|
||||
myChart.resize({
|
||||
animation: {
|
||||
duration: 300,
|
||||
easing: 'quadraticInOut'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖处理
|
||||
let resizeTimer: number;
|
||||
const debounceResize = () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||
};
|
||||
|
||||
// ========== 生命周期 ==========
|
||||
onMounted(async () => {
|
||||
await fetchList(props.formParams);
|
||||
window.addEventListener('resize', debounceResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', debounceResize);
|
||||
if (myChart) {
|
||||
myChart.dispose();
|
||||
myChart = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听参数变化重新请求数据
|
||||
watch(
|
||||
() => props.formParams,
|
||||
async (newParams) => {
|
||||
await fetchList(newParams);
|
||||
},
|
||||
{ deep: true, immediate: false }
|
||||
);
|
||||
|
||||
// 监听数据变化更新图表
|
||||
watch(rawData, () => {
|
||||
initChart();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-card) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.ant-card:hover) {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 优化Tooltip样式 */
|
||||
:deep(.echarts-tooltip) {
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e8e8e8 !important;
|
||||
max-width: 450px;
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
:deep(.echarts-tooltip table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/* 优化坐标轴样式 */
|
||||
:deep(.echarts-xaxis-label) {
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
:deep(.echarts-yaxis-name) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* 优化图例样式 */
|
||||
:deep(.echarts-legend-item) {
|
||||
margin-right: 15px !important;
|
||||
}
|
||||
|
||||
/* 优化柱状图标签 */
|
||||
:deep(.echarts-label) {
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Tooltip滚动条优化 */
|
||||
:deep(.echarts-tooltip::-webkit-scrollbar) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
:deep(.echarts-tooltip::-webkit-scrollbar-thumb) {
|
||||
background: #d9d9d9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:deep(.echarts-tooltip::-webkit-scrollbar-track) {
|
||||
background: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,859 @@
|
||||
<template>
|
||||
<Card title="用途柱线图" style="width: 100%; height: 400px; margin: 4px 0;">
|
||||
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { Card } from 'ant-design-vue';
|
||||
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
|
||||
import { ErpCategoryFlow, erpCategoryFlowListAll } from '@jeesite/erp/api/erp/categoryFlow';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
// 定义类型增强
|
||||
interface CategoryFlowDetail extends ErpCategoryFlow {
|
||||
statDate: string;
|
||||
categoryName: string;
|
||||
thisIncome?: number;
|
||||
prevIncome?: number;
|
||||
incomeMom?: number;
|
||||
thisExpense?: number;
|
||||
prevExpense?: number;
|
||||
expenseMom?: number;
|
||||
// 新增净收益相关字段
|
||||
thisNetProfit?: number; // 本期净收益 = 本期收入 - 本期支出
|
||||
prevNetProfit?: number; // 上期净收益 = 上期收入 - 上期支出
|
||||
netProfitMom?: number; // 净收益环比
|
||||
}
|
||||
|
||||
// 聚合后的数据类型(按日期聚合)
|
||||
interface AggregatedData {
|
||||
statDate: string; // 日期
|
||||
totalThisIncome: number; // 当日所有分类本期收入总和(元)
|
||||
totalPrevIncome: number; // 当日所有分类上期收入总和(元)
|
||||
totalThisExpense: number; // 当日所有分类本期支出总和(元)
|
||||
totalPrevExpense: number; // 当日所有分类上期支出总和(元)
|
||||
totalThisNetProfit: number; // 当日所有分类本期净收益总和(元)
|
||||
totalPrevNetProfit: number; // 当日所有分类上期净收益总和(元)
|
||||
avgIncomeMom: number; // 当日所有分类收入环比平均值(%)
|
||||
avgExpenseMom: number; // 当日所有分类支出环比平均值(%)
|
||||
avgNetProfitMom: number; // 当日所有分类净收益环比平均值(%)
|
||||
details: CategoryFlowDetail[]; // 当日所有分类的明细数据
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
formParams: Record<string, any>; // 接收周期等参数
|
||||
}>();
|
||||
|
||||
const listErpCategory = ref<CategoryFlowDetail[]>([]);
|
||||
const aggregatedData = ref<AggregatedData[]>([]); // 按日期聚合后的数据
|
||||
const chartDom = ref<HTMLDivElement | null>(null);
|
||||
let myChart: echarts.ECharts | null = null;
|
||||
const { createMessage } = useMessage();
|
||||
|
||||
// ========== 核心工具函数 ==========
|
||||
// 保留2位小数(增强容错)
|
||||
const formatNumber = (num: number | string | undefined, decimal = 2): number => {
|
||||
const parsed = Number(num);
|
||||
if (isNaN(parsed)) return 0;
|
||||
return Number(parsed.toFixed(decimal));
|
||||
};
|
||||
|
||||
// 元转万元(仅用于柱状图/Y轴展示)
|
||||
const toTenThousandYuan = (num: number | string | undefined): number => {
|
||||
const rawNum = formatNumber(num);
|
||||
return formatNumber(rawNum / 10000);
|
||||
};
|
||||
|
||||
// 千分位格式化(Tooltip显示原始元时使用)
|
||||
const formatWithThousandsSeparator = (num: number | string | undefined): string => {
|
||||
const parsed = Number(num);
|
||||
if (isNaN(parsed)) return '0.00';
|
||||
return parsed.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
|
||||
// 日期比较函数(升序,保证X轴日期从早到晚)
|
||||
const compareDateAsc = (a: AggregatedData, b: AggregatedData): number => {
|
||||
const dateA = new Date(a.statDate || '');
|
||||
const dateB = new Date(b.statDate || '');
|
||||
return dateA.getTime() - dateB.getTime();
|
||||
};
|
||||
|
||||
// ========== 柱状图标签仅显示数值(无单位) ==========
|
||||
const barLabelConfig = {
|
||||
show: true,
|
||||
position: 'top',
|
||||
distance: 3,
|
||||
textStyle: {
|
||||
fontSize: 11,
|
||||
color: '#333',
|
||||
fontWeight: '500'
|
||||
},
|
||||
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)}`
|
||||
};
|
||||
|
||||
// 折线图标签配置:百分比格式(保留单位)
|
||||
const lineLabelConfig = {
|
||||
show: true,
|
||||
position: 'top',
|
||||
distance: 5,
|
||||
textStyle: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500'
|
||||
},
|
||||
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)} %`
|
||||
};
|
||||
|
||||
// 日期格式化(统一格式)
|
||||
const formatDate = (date: string | undefined): string => {
|
||||
if (!date) return '';
|
||||
return date.replace(/\//g, '-').trim();
|
||||
};
|
||||
|
||||
// 计算环比值
|
||||
const calculateMom = (current: number, previous: number): number => {
|
||||
if (previous === 0) return 0;
|
||||
return formatNumber(((current - previous) / previous) * 100);
|
||||
};
|
||||
|
||||
// ========== 数据聚合函数:按日期聚合 + 汇总当日所有分类 ==========
|
||||
const aggregateDataByDate = (rawData: CategoryFlowDetail[]): AggregatedData[] => {
|
||||
const dateMap = new Map<string, AggregatedData>();
|
||||
|
||||
rawData.forEach(item => {
|
||||
if (!item.statDate || !item.categoryName) return;
|
||||
const formattedDate = formatDate(item.statDate);
|
||||
|
||||
// 计算净收益
|
||||
const thisNetProfit = formatNumber(item.thisIncome) - formatNumber(item.thisExpense);
|
||||
const prevNetProfit = formatNumber(item.prevIncome) - formatNumber(item.prevExpense);
|
||||
const netProfitMom = calculateMom(thisNetProfit, prevNetProfit);
|
||||
|
||||
// 补全净收益字段
|
||||
const itemWithNetProfit = {
|
||||
...item,
|
||||
thisNetProfit,
|
||||
prevNetProfit,
|
||||
netProfitMom
|
||||
};
|
||||
|
||||
// 初始化日期数据
|
||||
if (!dateMap.has(formattedDate)) {
|
||||
dateMap.set(formattedDate, {
|
||||
statDate: formattedDate,
|
||||
totalThisIncome: 0,
|
||||
totalPrevIncome: 0,
|
||||
totalThisExpense: 0,
|
||||
totalPrevExpense: 0,
|
||||
totalThisNetProfit: 0,
|
||||
totalPrevNetProfit: 0,
|
||||
avgIncomeMom: 0,
|
||||
avgExpenseMom: 0,
|
||||
avgNetProfitMom: 0,
|
||||
details: []
|
||||
});
|
||||
}
|
||||
|
||||
const dateData = dateMap.get(formattedDate)!;
|
||||
|
||||
// 累加当日金额(原始元)
|
||||
dateData.totalThisIncome += formatNumber(itemWithNetProfit.thisIncome);
|
||||
dateData.totalPrevIncome += formatNumber(itemWithNetProfit.prevIncome);
|
||||
dateData.totalThisExpense += formatNumber(itemWithNetProfit.thisExpense);
|
||||
dateData.totalPrevExpense += formatNumber(itemWithNetProfit.prevExpense);
|
||||
dateData.totalThisNetProfit += thisNetProfit;
|
||||
dateData.totalPrevNetProfit += prevNetProfit;
|
||||
|
||||
// 收集当日分类明细
|
||||
dateData.details.push({
|
||||
...itemWithNetProfit,
|
||||
statDate: formattedDate,
|
||||
categoryName: itemWithNetProfit.categoryName.trim()
|
||||
});
|
||||
});
|
||||
|
||||
// 处理每个日期的最终数据
|
||||
const result: AggregatedData[] = Array.from(dateMap.values()).map(item => {
|
||||
// 1. 计算当日环比平均值(过滤0值)
|
||||
const incomeMomList = item.details.map(d => formatNumber(d.incomeMom)).filter(v => v !== 0);
|
||||
const expenseMomList = item.details.map(d => formatNumber(d.expenseMom)).filter(v => v !== 0);
|
||||
const netProfitMomList = item.details.map(d => formatNumber(d.netProfitMom)).filter(v => v !== 0);
|
||||
|
||||
item.avgIncomeMom = incomeMomList.length
|
||||
? formatNumber(incomeMomList.reduce((a, b) => a + b, 0) / incomeMomList.length)
|
||||
: 0;
|
||||
|
||||
item.avgExpenseMom = expenseMomList.length
|
||||
? formatNumber(expenseMomList.reduce((a, b) => a + b, 0) / expenseMomList.length)
|
||||
: 0;
|
||||
|
||||
item.avgNetProfitMom = netProfitMomList.length
|
||||
? formatNumber(netProfitMomList.reduce((a, b) => a + b, 0) / netProfitMomList.length)
|
||||
: 0;
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
// 按日期升序排序(X轴从早到晚)
|
||||
return result.sort(compareDateAsc);
|
||||
};
|
||||
|
||||
const fetchList = async (params: Record<string, any>) => {
|
||||
try {
|
||||
const result = await erpCategoryFlowListAll(params);
|
||||
|
||||
// 过滤空数据 + 统一日期格式 + 类型转换 + 计算净收益
|
||||
const validData = (result || [])
|
||||
.filter(item => item.categoryName && item.statDate)
|
||||
.map(item => {
|
||||
// 基础字段格式化
|
||||
const thisIncome = formatNumber(item.thisIncome);
|
||||
const prevIncome = formatNumber(item.prevIncome);
|
||||
const thisExpense = formatNumber(item.thisExpense);
|
||||
const prevExpense = formatNumber(item.prevExpense);
|
||||
|
||||
// 计算净收益
|
||||
const thisNetProfit = thisIncome - thisExpense;
|
||||
const prevNetProfit = prevIncome - prevExpense;
|
||||
const netProfitMom = calculateMom(thisNetProfit, prevNetProfit);
|
||||
|
||||
return {
|
||||
...item,
|
||||
statDate: formatDate(item.statDate),
|
||||
categoryName: item.categoryName.trim(),
|
||||
thisIncome,
|
||||
prevIncome,
|
||||
incomeMom: formatNumber(item.incomeMom),
|
||||
thisExpense,
|
||||
prevExpense,
|
||||
expenseMom: formatNumber(item.expenseMom),
|
||||
thisNetProfit: formatNumber(thisNetProfit),
|
||||
prevNetProfit: formatNumber(prevNetProfit),
|
||||
netProfitMom: formatNumber(netProfitMom)
|
||||
} as CategoryFlowDetail;
|
||||
});
|
||||
|
||||
listErpCategory.value = validData;
|
||||
// 按日期聚合数据
|
||||
aggregatedData.value = aggregateDataByDate(validData);
|
||||
} catch (error) {
|
||||
console.error('获取数据列表失败:', error);
|
||||
// 异常时强制清空所有数据
|
||||
listErpCategory.value = [];
|
||||
aggregatedData.value = [];
|
||||
createMessage.error('数据加载失败,请稍后重试');
|
||||
} finally {
|
||||
// 无论成功/失败/空数据,最终都执行图表初始化
|
||||
initChart();
|
||||
}
|
||||
};
|
||||
|
||||
// 计算Y轴极值(适配万元展示层)
|
||||
const calculateYAxisExtent = (data: number[], isRate = false) => {
|
||||
if (data.length === 0) return isRate ? [-10, 10] : [0, 10]; // 万元默认范围
|
||||
|
||||
const formattedData = data.map(num => formatNumber(num));
|
||||
const min = Math.min(...formattedData);
|
||||
const max = Math.max(...formattedData);
|
||||
|
||||
// 万元轴内边距:10% 或最小1万元
|
||||
const padding = isRate
|
||||
? Math.max(Math.abs(max) * 0.2, Math.abs(min) * 0.2, 5)
|
||||
: Math.max((max - min) * 0.1, 1);
|
||||
|
||||
let minExtent = min - padding;
|
||||
let maxExtent = max + padding;
|
||||
|
||||
// 强制包含0点
|
||||
if (minExtent > 0) minExtent = 0;
|
||||
if (maxExtent < 0) maxExtent = 0;
|
||||
|
||||
// 百分比轴正负对称
|
||||
if (isRate) {
|
||||
const absMax = Math.max(Math.abs(minExtent), Math.abs(maxExtent));
|
||||
minExtent = -absMax;
|
||||
maxExtent = absMax;
|
||||
}
|
||||
|
||||
return [minExtent, maxExtent];
|
||||
};
|
||||
|
||||
// 初始化图表(核心优化:X轴为日期 + 按日期汇总金额 + Tooltip展示分类明细)
|
||||
const initChart = () => {
|
||||
if (!chartDom.value) return;
|
||||
|
||||
// 销毁旧实例,避免数据残留
|
||||
if (myChart) {
|
||||
myChart.dispose();
|
||||
}
|
||||
|
||||
myChart = echarts.init(chartDom.value);
|
||||
|
||||
// ========== 空数据处理逻辑 ==========
|
||||
if (aggregatedData.value.length === 0) {
|
||||
myChart.setOption({
|
||||
title: {
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 18, color: '#333' }
|
||||
},
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
legend: { data: [], top: 10, textStyle: { fontSize: 12 } },
|
||||
grid: {
|
||||
left: '8%',
|
||||
right: '12%',
|
||||
bottom: '15%', // 增加底部间距适配日期标签
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [],
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
rotate: 45, // 日期旋转45度避免重叠
|
||||
overflow: 'truncate',
|
||||
width: 80
|
||||
},
|
||||
axisLine: { onZero: true }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额(万元)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => formatNumber(value).toFixed(2)
|
||||
},
|
||||
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 5,
|
||||
scale: true
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '环比(%)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => `${formatNumber(value).toFixed(2)} %`
|
||||
},
|
||||
position: 'right',
|
||||
offset: 0,
|
||||
splitLine: { show: false },
|
||||
axisLine: { onZero: true },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
series: [], // 清空所有系列数据
|
||||
noDataLoadingOption: {
|
||||
text: '暂无收入支出数据',
|
||||
textStyle: { fontSize: 16, color: '#666', fontWeight: '500' },
|
||||
position: 'center',
|
||||
effect: 'none' // 关闭空数据动画,提升体验
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常数据处理逻辑
|
||||
// 提取X轴类目(日期)
|
||||
const xAxisData = aggregatedData.value.map(item => item.statDate).filter(Boolean);
|
||||
|
||||
// 按日期汇总的金额数据(转万元)
|
||||
const thisIncomeShow = xAxisData.map(date => {
|
||||
const item = aggregatedData.value.find(i => i.statDate === date);
|
||||
return toTenThousandYuan(item?.totalThisIncome);
|
||||
});
|
||||
const prevIncomeShow = xAxisData.map(date => {
|
||||
const item = aggregatedData.value.find(i => i.statDate === date);
|
||||
return toTenThousandYuan(item?.totalPrevIncome);
|
||||
});
|
||||
const thisExpenseShow = xAxisData.map(date => {
|
||||
const item = aggregatedData.value.find(i => i.statDate === date);
|
||||
return toTenThousandYuan(item?.totalThisExpense);
|
||||
});
|
||||
const prevExpenseShow = xAxisData.map(date => {
|
||||
const item = aggregatedData.value.find(i => i.statDate === date);
|
||||
return toTenThousandYuan(item?.totalPrevExpense);
|
||||
});
|
||||
|
||||
// 环比数据(当日平均值)
|
||||
const incomeMomData = xAxisData.map(date => {
|
||||
const item = aggregatedData.value.find(i => i.statDate === date);
|
||||
return formatNumber(item?.avgIncomeMom);
|
||||
});
|
||||
const expenseMomData = xAxisData.map(date => {
|
||||
const item = aggregatedData.value.find(i => i.statDate === date);
|
||||
return formatNumber(item?.avgExpenseMom);
|
||||
});
|
||||
|
||||
// 计算Y轴范围(基于万元展示数据)
|
||||
const amountData = [...thisIncomeShow, ...prevIncomeShow, ...thisExpenseShow, ...prevExpenseShow];
|
||||
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||
const rateData = [...incomeMomData, ...expenseMomData];
|
||||
const [rateMin, rateMax] = calculateYAxisExtent(rateData, true);
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
left: 'center',
|
||||
textStyle: { fontSize: 18, color: '#333' }
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
textStyle: { fontSize: 12 },
|
||||
padding: 12,
|
||||
backgroundColor: '#fff',
|
||||
borderColor: '#e8e8e8',
|
||||
borderWidth: 1,
|
||||
formatter: (params: any[]) => {
|
||||
if (!params || params.length === 0) return '<div style="padding: 8px;">暂无数据</div>';
|
||||
|
||||
const currentDate = params[0]?.axisValue || '';
|
||||
const dateData = aggregatedData.value.find(item => item.statDate === currentDate);
|
||||
|
||||
if (!dateData) return `<div style="padding: 8px;">${currentDate}:暂无明细</div>`;
|
||||
|
||||
// 样式定义
|
||||
const tableHeaderStyle = 'background: #f8f9fa; padding: 6px 8px; text-align: center; font-weight: 600; border: 1px solid #e8e8e8;';
|
||||
const tableCellStyle = 'padding: 6px 8px; text-align: center; border: 1px solid #e8e8e8;';
|
||||
const totalCellStyle = 'padding: 6px 8px; text-align: center; border: 1px solid #e8e8e8; background: #e6f7ff; font-weight: 600; color: #1890ff;';
|
||||
const summaryStyle = 'font-weight: 600; color: #333; margin-bottom: 8px; text-align: center;';
|
||||
|
||||
// 当日汇总数据
|
||||
const totalThisIncome = formatWithThousandsSeparator(dateData.totalThisIncome);
|
||||
const totalPrevIncome = formatWithThousandsSeparator(dateData.totalPrevIncome);
|
||||
const totalThisExpense = formatWithThousandsSeparator(dateData.totalThisExpense);
|
||||
const totalPrevExpense = formatWithThousandsSeparator(dateData.totalPrevExpense);
|
||||
const totalThisNetProfit = formatWithThousandsSeparator(dateData.totalThisNetProfit);
|
||||
const totalPrevNetProfit = formatWithThousandsSeparator(dateData.totalPrevNetProfit);
|
||||
|
||||
// 构建Tooltip内容
|
||||
let tooltipContent = `
|
||||
<div style="${summaryStyle}">${currentDate}</div>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 11px; min-width: 900px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="${tableHeaderStyle}">分类名称</th>
|
||||
<th style="${tableHeaderStyle}">本期收入(元)</th>
|
||||
<th style="${tableHeaderStyle}">上期收入(元)</th>
|
||||
<th style="${tableHeaderStyle}">收入环比(%)</th>
|
||||
<th style="${tableHeaderStyle}">本期支出(元)</th>
|
||||
<th style="${tableHeaderStyle}">上期支出(元)</th>
|
||||
<th style="${tableHeaderStyle}">支出环比(%)</th>
|
||||
<th style="${tableHeaderStyle}">本期净收益(元)</th>
|
||||
<th style="${tableHeaderStyle}">上期净收益(元)</th>
|
||||
<th style="${tableHeaderStyle}">净收益环比(%)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
// 添加分类明细行
|
||||
dateData.details.forEach(detail => {
|
||||
tooltipContent += `
|
||||
<tr>
|
||||
<td style="${tableCellStyle}; text-align: left; padding-left: 12px;">${detail.categoryName || '-'}</td>
|
||||
<td style="${tableCellStyle}">${formatWithThousandsSeparator(detail.thisIncome)}</td>
|
||||
<td style="${tableCellStyle}">${formatWithThousandsSeparator(detail.prevIncome)}</td>
|
||||
<td style="${tableCellStyle}">${detail.incomeMom?.toFixed(2) || '0.00'}</td>
|
||||
<td style="${tableCellStyle}">${formatWithThousandsSeparator(detail.thisExpense)}</td>
|
||||
<td style="${tableCellStyle}">${formatWithThousandsSeparator(detail.prevExpense)}</td>
|
||||
<td style="${tableCellStyle}">${detail.expenseMom?.toFixed(2) || '0.00'}</td>
|
||||
<td style="${tableCellStyle}">${formatWithThousandsSeparator(detail.thisNetProfit)}</td>
|
||||
<td style="${tableCellStyle}">${formatWithThousandsSeparator(detail.prevNetProfit)}</td>
|
||||
<td style="${tableCellStyle}">${detail.netProfitMom?.toFixed(2) || '0.00'}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
// 添加当日合计行
|
||||
tooltipContent += `
|
||||
<tr>
|
||||
<td style="${totalCellStyle}; text-align: left; padding-left: 12px;">本期合计</td>
|
||||
<td style="${totalCellStyle}">${totalThisIncome}</td>
|
||||
<td style="${totalCellStyle}">${totalPrevIncome}</td>
|
||||
<td style="${totalCellStyle}">${dateData.avgIncomeMom.toFixed(2)}</td>
|
||||
<td style="${totalCellStyle}">${totalThisExpense}</td>
|
||||
<td style="${totalCellStyle}">${totalPrevExpense}</td>
|
||||
<td style="${totalCellStyle}">${dateData.avgExpenseMom.toFixed(2)}</td>
|
||||
<td style="${totalCellStyle}">${totalThisNetProfit}</td>
|
||||
<td style="${totalCellStyle}">${totalPrevNetProfit}</td>
|
||||
<td style="${totalCellStyle}">${dateData.avgNetProfitMom.toFixed(2)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
return tooltipContent;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['本期收入', '上期收入', '本期支出', '上期支出', '收入环比', '支出环比'],
|
||||
top: 10,
|
||||
textStyle: { fontSize: 12 },
|
||||
// 自动换行防止图例溢出
|
||||
formatter: (name: string) => {
|
||||
const maxLength = 6;
|
||||
if (name.length > maxLength) {
|
||||
return name.substring(0, maxLength) + '...';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '8%',
|
||||
right: '12%',
|
||||
bottom: '15%', // 增加底部间距适配日期标签
|
||||
top: '20%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
rotate: 45, // 日期旋转45度避免重叠
|
||||
overflow: 'truncate',
|
||||
width: 80
|
||||
},
|
||||
axisLine: { onZero: true }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '金额(万元)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => formatNumber(value).toFixed(2)
|
||||
},
|
||||
min: amountMin,
|
||||
max: amountMax,
|
||||
axisLine: { onZero: true },
|
||||
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 5,
|
||||
scale: true
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '环比(%)',
|
||||
nameTextStyle: { fontSize: 12 },
|
||||
axisLabel: {
|
||||
fontSize: 11,
|
||||
formatter: (value: number) => `${formatNumber(value).toFixed(2)} %`
|
||||
},
|
||||
min: rateMin,
|
||||
max: rateMax,
|
||||
position: 'right',
|
||||
offset: 0,
|
||||
splitLine: { show: false },
|
||||
axisLine: { onZero: true },
|
||||
axisTick: { alignWithLabel: true },
|
||||
splitNumber: 4
|
||||
}
|
||||
],
|
||||
noDataLoadingOption: {
|
||||
text: '暂无数据',
|
||||
textStyle: { fontSize: 14, color: '#999' },
|
||||
effect: 'bubble',
|
||||
effectOption: { effect: { n: 0 } }
|
||||
},
|
||||
series: [
|
||||
// 本期收入(当日汇总)- 深蓝色系
|
||||
{
|
||||
name: '本期收入',
|
||||
type: 'bar',
|
||||
data: thisIncomeShow,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#1890ff' },
|
||||
{ offset: 1, color: '#096dd9' }
|
||||
]),
|
||||
borderRadius: [8, 8, 0, 0]
|
||||
},
|
||||
barWidth: 20,
|
||||
barBorderRadius: [8, 8, 0, 0],
|
||||
label: barLabelConfig,
|
||||
yAxisIndex: 0,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#40a9ff' },
|
||||
{ offset: 1, color: '#1890ff' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
// 上期收入(当日汇总)- 天蓝色系
|
||||
{
|
||||
name: '上期收入',
|
||||
type: 'bar',
|
||||
data: prevIncomeShow,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#722ed1' },
|
||||
{ offset: 1, color: '#531dab' }
|
||||
]),
|
||||
borderRadius: [8, 8, 0, 0]
|
||||
},
|
||||
barWidth: 20,
|
||||
barBorderRadius: [8, 8, 0, 0],
|
||||
label: barLabelConfig,
|
||||
yAxisIndex: 0,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#9254de' },
|
||||
{ offset: 1, color: '#722ed1' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
// 本期支出(当日汇总)- 红色系
|
||||
{
|
||||
name: '本期支出',
|
||||
type: 'bar',
|
||||
data: thisExpenseShow,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#f5222d' },
|
||||
{ offset: 1, color: '#cf1322' }
|
||||
]),
|
||||
borderRadius: [8, 8, 0, 0]
|
||||
},
|
||||
barWidth: 20,
|
||||
barBorderRadius: [8, 8, 0, 0],
|
||||
label: barLabelConfig,
|
||||
yAxisIndex: 0,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#ff4d4f' },
|
||||
{ offset: 1, color: '#f5222d' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
// 上期支出(当日汇总)- 橙色系
|
||||
{
|
||||
name: '上期支出',
|
||||
type: 'bar',
|
||||
data: prevExpenseShow,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#fa8c16' },
|
||||
{ offset: 1, color: '#d46b08' }
|
||||
]),
|
||||
borderRadius: [8, 8, 0, 0]
|
||||
},
|
||||
barWidth: 20,
|
||||
barBorderRadius: [8, 8, 0, 0],
|
||||
label: barLabelConfig,
|
||||
yAxisIndex: 0,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#ffa940' },
|
||||
{ offset: 1, color: '#fa8c16' }
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
// 收入环比(当日平均值)- 绿色系
|
||||
{
|
||||
name: '收入环比',
|
||||
type: 'line',
|
||||
data: incomeMomData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: { width: 3, color: '#52c41a' },
|
||||
itemStyle: {
|
||||
color: '#52c41a',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
...lineLabelConfig,
|
||||
textStyle: { ...lineLabelConfig.textStyle, color: '#52c41a' }
|
||||
},
|
||||
emphasis: { itemStyle: { symbolSize: 12 } },
|
||||
yAxisIndex: 1,
|
||||
markLine: {
|
||||
silent: true,
|
||||
lineStyle: { color: '#ccc', type: 'dashed', width: 1 },
|
||||
data: [{ yAxis: 0, lineStyle: { color: '#999', type: 'dashed' } }]
|
||||
}
|
||||
},
|
||||
// 支出环比(当日平均值)- 紫色系
|
||||
{
|
||||
name: '支出环比',
|
||||
type: 'line',
|
||||
data: expenseMomData,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
lineStyle: { width: 3, color: '#eb2f96' },
|
||||
itemStyle: {
|
||||
color: '#eb2f96',
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
...lineLabelConfig,
|
||||
textStyle: { ...lineLabelConfig.textStyle, color: '#eb2f96' }
|
||||
},
|
||||
emphasis: { itemStyle: { symbolSize: 12 } },
|
||||
yAxisIndex: 1,
|
||||
markLine: {
|
||||
silent: true,
|
||||
lineStyle: { color: '#ccc', type: 'dashed', width: 1 },
|
||||
data: [{ yAxis: 0, lineStyle: { color: '#999', type: 'dashed' } }]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
myChart.setOption(option);
|
||||
};
|
||||
|
||||
// 调整图表大小(防抖+动画)
|
||||
const resizeChart = () => {
|
||||
if (myChart) {
|
||||
myChart.resize({
|
||||
animation: {
|
||||
duration: 300,
|
||||
easing: 'quadraticInOut'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口 resize 防抖处理
|
||||
let resizeTimer: number;
|
||||
const debounceResize = () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||
};
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
await fetchList(props.formParams);
|
||||
window.addEventListener('resize', debounceResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', debounceResize);
|
||||
if (myChart) {
|
||||
myChart.dispose();
|
||||
myChart = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.formParams,
|
||||
async (newParams) => {
|
||||
await fetchList(newParams);
|
||||
},
|
||||
{ deep: true, immediate: false }
|
||||
);
|
||||
|
||||
// 监听聚合数据变化自动更新图表(空数据也触发)
|
||||
watch(aggregatedData, () => {
|
||||
initChart();
|
||||
}, { deep: true });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.ant-card) {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.ant-card:hover) {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* 优化图表tooltip样式 */
|
||||
:deep(.echarts-tooltip) {
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e8e8e8 !important;
|
||||
max-width: 1050px; /* 扩大Tooltip最大宽度适配新增列 */
|
||||
max-height: 500px; /* 扩大Tooltip最大高度 */
|
||||
overflow-y: auto; /* 超出滚动 */
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
/* 优化Tooltip表格样式 */
|
||||
:deep(.echarts-tooltip table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 900px; /* 适配新增净收益列的表格宽度 */
|
||||
}
|
||||
|
||||
:deep(.echarts-tooltip th) {
|
||||
background: #f8f9fa !important;
|
||||
font-weight: 600 !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.echarts-tooltip td) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 合计行样式增强 */
|
||||
:deep(.echarts-tooltip tr:last-child td) {
|
||||
background: #e6f7ff !important;
|
||||
color: #1890ff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* 优化柱状图标签溢出处理 */
|
||||
:deep(.echarts-label) {
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 优化Y轴名称间距 */
|
||||
:deep(.echarts-yaxis-name) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* 优化X轴日期标签显示 */
|
||||
:deep(.echarts-xaxis-label) {
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.5; /* 增加行高适配旋转后的日期 */
|
||||
}
|
||||
|
||||
/* 调整图例间距 */
|
||||
:deep(.echarts-legend-item) {
|
||||
margin-right: 15px !important;
|
||||
}
|
||||
|
||||
/* Tooltip滚动条样式优化 */
|
||||
:deep(.echarts-tooltip::-webkit-scrollbar) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
:deep(.echarts-tooltip::-webkit-scrollbar-thumb) {
|
||||
background: #d9d9d9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:deep(.echarts-tooltip::-webkit-scrollbar-track) {
|
||||
background: #f5f5f5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,67 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<PageWrapper class="dashboard-container">
|
||||
<template #extra>
|
||||
<BasicForm
|
||||
:labelWidth="100"
|
||||
:schemas="schemaForm.schemas"
|
||||
style="width: 245px;"
|
||||
/>
|
||||
</template>
|
||||
<div class="two-column-layout">
|
||||
<ChartPie />
|
||||
<ChartLine />
|
||||
<ChartLineRatio :formParams="FormValues" />
|
||||
</div>
|
||||
<div class="two-column-layout">
|
||||
<ChartBarCycle />
|
||||
<ChartBarAccount />
|
||||
<ChartBarCycle :formParams="FormValues" />
|
||||
<ChartBarAccount :formParams="FormValues" />
|
||||
</div>
|
||||
</div>
|
||||
<ChartLineType :formParams="FormValues" />
|
||||
<div class="two-column-layout">
|
||||
<ChartLineExp :formParams="FormValues" />
|
||||
<ChartLineInc :formParams="FormValues" />
|
||||
</div>
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { Card } from 'ant-design-vue';
|
||||
import ChartPie from './components/ChartPie.vue';
|
||||
import ChartLine from './components/ChartLine.vue';
|
||||
import ChartBarCycle from './components/ChartBarCycle.vue';
|
||||
import ChartBarAccount from './components/ChartBarAccount.vue';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { Card } from 'ant-design-vue';
|
||||
import { BasicForm, FormSchema, FormProps } from '@jeesite/core/components/Form';
|
||||
import { PageWrapper } from '@jeesite/core/components/Page';
|
||||
import ChartPie from './components/ChartPie.vue';
|
||||
import ChartLineExp from './components/ChartLineExp.vue';
|
||||
import ChartLineInc from './components/ChartLineInc.vue';
|
||||
import ChartBarCycle from './components/ChartBarCycle.vue';
|
||||
import ChartBarAccount from './components/ChartBarAccount.vue';
|
||||
import ChartLineType from './components/ChartLineType.vue';
|
||||
import ChartLineRatio from './components/ChartLineRatio.vue';
|
||||
|
||||
|
||||
|
||||
const FormValues = ref<Record<string, any>>({
|
||||
cycleType: 'M'
|
||||
});
|
||||
|
||||
const schemaForm: FormProps = {
|
||||
baseColProps: { md: 8, lg: 6 },
|
||||
labelWidth: 90,
|
||||
schemas: [
|
||||
{
|
||||
label: '周期',
|
||||
field: 'cycleType',
|
||||
defaultValue: 'M',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
dictType: 'report_cycle',
|
||||
allowClear: true,
|
||||
onChange: (value: string) => {
|
||||
FormValues.value.cycleType = value || '';
|
||||
}
|
||||
},
|
||||
colProps: { md: 24, lg: 24 },
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
|
||||
40
web-vue/packages/erp/api/erp/categoryFlow.ts
Normal file
40
web-vue/packages/erp/api/erp/categoryFlow.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
|
||||
* No deletion without permission, or be held responsible to law.
|
||||
* @author gaoxq
|
||||
*/
|
||||
import { defHttp } from '@jeesite/core/utils/http/axios';
|
||||
import { useGlobSetting } from '@jeesite/core/hooks/setting';
|
||||
import { BasicModel, Page } from '@jeesite/core/api/model/baseModel';
|
||||
|
||||
const { adminPath } = useGlobSetting();
|
||||
|
||||
export interface ErpCategoryFlow extends BasicModel<ErpCategoryFlow> {
|
||||
cycleType: string; // cycle_type
|
||||
categoryName: string; // 分类名称
|
||||
statDate?: string; // stat_date
|
||||
thisIncome: number; // this_income
|
||||
prevIncome: number; // prev_income
|
||||
incomeMom?: number; // income_mom
|
||||
thisExpense: number; // this_expense
|
||||
prevExpense: number; // prev_expense
|
||||
expenseMom?: number; // expense_mom
|
||||
}
|
||||
|
||||
export const erpCategoryFlowList = (params?: ErpCategoryFlow | any) =>
|
||||
defHttp.get<ErpCategoryFlow>({ url: adminPath + '/erp/categoryFlow/list', params });
|
||||
|
||||
export const erpCategoryFlowListAll = (params?: ErpCategoryFlow | any) =>
|
||||
defHttp.get<ErpCategoryFlow[]>({ url: adminPath + '/erp/categoryFlow/listAll', params });
|
||||
|
||||
export const erpCategoryFlowListData = (params?: ErpCategoryFlow | any) =>
|
||||
defHttp.post<Page<ErpCategoryFlow>>({ url: adminPath + '/erp/categoryFlow/listData', params });
|
||||
|
||||
export const erpCategoryFlowForm = (params?: ErpCategoryFlow | any) =>
|
||||
defHttp.get<ErpCategoryFlow>({ url: adminPath + '/erp/categoryFlow/form', params });
|
||||
|
||||
export const erpCategoryFlowSave = (params?: any, data?: ErpCategoryFlow | any) =>
|
||||
defHttp.postJson<ErpCategoryFlow>({ url: adminPath + '/erp/categoryFlow/save', params, data });
|
||||
|
||||
export const erpCategoryFlowDelete = (params?: ErpCategoryFlow | any) =>
|
||||
defHttp.get<ErpCategoryFlow>({ url: adminPath + '/erp/categoryFlow/delete', params });
|
||||
36
web-vue/packages/erp/api/erp/incExpRatio.ts
Normal file
36
web-vue/packages/erp/api/erp/incExpRatio.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
|
||||
* No deletion without permission, or be held responsible to law.
|
||||
* @author gaoxq
|
||||
*/
|
||||
import { defHttp } from '@jeesite/core/utils/http/axios';
|
||||
import { useGlobSetting } from '@jeesite/core/hooks/setting';
|
||||
import { BasicModel, Page } from '@jeesite/core/api/model/baseModel';
|
||||
|
||||
const { adminPath } = useGlobSetting();
|
||||
|
||||
export interface ErpIncExpRatio extends BasicModel<ErpIncExpRatio> {
|
||||
cycleType: string; // cycle_type
|
||||
statDate?: string; // stat_date
|
||||
incomeAmount?: number; // income_amount
|
||||
expenseAmount?: number; // expense_amount
|
||||
expenseRatio: number; // expense_ratio
|
||||
}
|
||||
|
||||
export const erpIncExpRatioList = (params?: ErpIncExpRatio | any) =>
|
||||
defHttp.get<ErpIncExpRatio>({ url: adminPath + '/erp/incExpRatio/list', params });
|
||||
|
||||
export const erpIncExpRatioListAll = (params?: ErpIncExpRatio | any) =>
|
||||
defHttp.get<ErpIncExpRatio[]>({ url: adminPath + '/erp/incExpRatio/listAll', params });
|
||||
|
||||
export const erpIncExpRatioListData = (params?: ErpIncExpRatio | any) =>
|
||||
defHttp.post<Page<ErpIncExpRatio>>({ url: adminPath + '/erp/incExpRatio/listData', params });
|
||||
|
||||
export const erpIncExpRatioForm = (params?: ErpIncExpRatio | any) =>
|
||||
defHttp.get<ErpIncExpRatio>({ url: adminPath + '/erp/incExpRatio/form', params });
|
||||
|
||||
export const erpIncExpRatioSave = (params?: any, data?: ErpIncExpRatio | any) =>
|
||||
defHttp.postJson<ErpIncExpRatio>({ url: adminPath + '/erp/incExpRatio/save', params, data });
|
||||
|
||||
export const erpIncExpRatioDelete = (params?: ErpIncExpRatio | any) =>
|
||||
defHttp.get<ErpIncExpRatio>({ url: adminPath + '/erp/incExpRatio/delete', params });
|
||||
2441
web-vue/pnpm-lock.yaml
generated
2441
web-vue/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,6 @@ import { setupRouterGuard } from '@jeesite/core/router/guard';
|
||||
import { setupStore } from '@jeesite/core/store';
|
||||
import { setupDForm } from '@jeesite/dfm';
|
||||
|
||||
// 1. 引入 ECharts(全量引入,兼容Jeesite所有场景)
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
async function bootstrap() {
|
||||
@@ -57,7 +56,9 @@ async function bootstrap() {
|
||||
setupDForm();
|
||||
|
||||
app.config.globalProperties.$echarts = echarts;
|
||||
if (window) window.echarts = echarts;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.echarts = echarts;
|
||||
}
|
||||
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user