财务门户设计
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.ErpAccountCycle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VIEWDAO接口
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@MyBatisDao(dataSourceName="work")
|
||||||
|
public interface ErpAccountCycleDao extends CrudDao<ErpAccountCycle> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.ErpAccountIncomeExpense;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户收支DAO接口
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@MyBatisDao(dataSourceName="work")
|
||||||
|
public interface ErpAccountIncomeExpenseDao extends CrudDao<ErpAccountIncomeExpense> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.ErpCategoryIncomeExpense;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类别收支DAO接口
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@MyBatisDao(dataSourceName="work")
|
||||||
|
public interface ErpCategoryIncomeExpenseDao extends CrudDao<ErpCategoryIncomeExpense> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.ErpIncomeExpenseCycle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年度收支信息DAO接口
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@MyBatisDao(dataSourceName="work")
|
||||||
|
public interface ErpIncomeExpenseCycleDao extends CrudDao<ErpIncomeExpenseCycle> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
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.JoinTable;
|
||||||
|
import com.jeesite.common.mybatis.annotation.Table;
|
||||||
|
import com.jeesite.common.utils.excel.annotation.ExcelField;
|
||||||
|
import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
|
||||||
|
import com.jeesite.common.utils.excel.annotation.ExcelFields;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VIEWEntity
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Table(name = "erp_account_cycle_view", alias = "a", label = "VIEW信息", columns = {
|
||||||
|
@Column(name = "account_id", attrName = "accountId", label = "交易账户", isPK = true),
|
||||||
|
@Column(name = "transaction_type", attrName = "transactionType", label = "交易类型", isPK = true),
|
||||||
|
@Column(name = "year_date", attrName = "yearDate", label = "年度", isPK = true),
|
||||||
|
@Column(name = "amount_01", attrName = "amount01", label = "01月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_02", attrName = "amount02", label = "02月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_03", attrName = "amount03", label = "03月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_04", attrName = "amount04", label = "04月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_05", attrName = "amount05", label = "05月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_06", attrName = "amount06", label = "06月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_07", attrName = "amount07", label = "07月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_08", attrName = "amount08", label = "08月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_09", attrName = "amount09", label = "09月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_10", attrName = "amount10", label = "10月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_11", attrName = "amount11", label = "11月", isUpdateForce = true),
|
||||||
|
@Column(name = "amount_12", attrName = "amount12", label = "12月", isUpdateForce = true),
|
||||||
|
}, joinTable = {
|
||||||
|
@JoinTable(type = JoinTable.Type.LEFT_JOIN, entity = ErpAccount.class, attrName = "this", alias = "b",
|
||||||
|
on = "a.account_id = b.account_id",
|
||||||
|
columns = {
|
||||||
|
@Column(name = "account_name", attrName = "accountName", label = "账户名称"),
|
||||||
|
}),
|
||||||
|
}, orderBy = "a.account_id DESC, a.transaction_type DESC, a.year_date DESC"
|
||||||
|
)
|
||||||
|
@Data
|
||||||
|
public class ErpAccountCycle extends DataEntity<ErpAccountCycle> implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private String accountId; // 交易账户
|
||||||
|
private String transactionType; // 交易类型
|
||||||
|
private String yearDate; // 年度
|
||||||
|
private Double amount01; // 01月
|
||||||
|
private Double amount02; // 02月
|
||||||
|
private Double amount03; // 03月
|
||||||
|
private Double amount04; // 04月
|
||||||
|
private Double amount05; // 05月
|
||||||
|
private Double amount06; // 06月
|
||||||
|
private Double amount07; // 07月
|
||||||
|
private Double amount08; // 08月
|
||||||
|
private Double amount09; // 09月
|
||||||
|
private Double amount10; // 10月
|
||||||
|
private Double amount11; // 11月
|
||||||
|
private Double amount12; // 12月
|
||||||
|
|
||||||
|
private String accountName;
|
||||||
|
|
||||||
|
@ExcelFields({
|
||||||
|
@ExcelField(title = "交易账户", attrName = "accountName", align = Align.CENTER, sort = 10),
|
||||||
|
@ExcelField(title = "交易类型", attrName = "transactionType", dictType = "transaction_type", align = Align.CENTER, sort = 20),
|
||||||
|
@ExcelField(title = "年度", attrName = "yearDate", align = Align.CENTER, sort = 30),
|
||||||
|
@ExcelField(title = "01月", attrName = "amount01", align = Align.CENTER, sort = 40),
|
||||||
|
@ExcelField(title = "02月", attrName = "amount02", align = Align.CENTER, sort = 50),
|
||||||
|
@ExcelField(title = "03月", attrName = "amount03", align = Align.CENTER, sort = 60),
|
||||||
|
@ExcelField(title = "04月", attrName = "amount04", align = Align.CENTER, sort = 70),
|
||||||
|
@ExcelField(title = "05月", attrName = "amount05", align = Align.CENTER, sort = 80),
|
||||||
|
@ExcelField(title = "06月", attrName = "amount06", align = Align.CENTER, sort = 90),
|
||||||
|
@ExcelField(title = "07月", attrName = "amount07", align = Align.CENTER, sort = 100),
|
||||||
|
@ExcelField(title = "08月", attrName = "amount08", align = Align.CENTER, sort = 110),
|
||||||
|
@ExcelField(title = "09月", attrName = "amount09", align = Align.CENTER, sort = 120),
|
||||||
|
@ExcelField(title = "10月", attrName = "amount10", align = Align.CENTER, sort = 130),
|
||||||
|
@ExcelField(title = "11月", attrName = "amount11", align = Align.CENTER, sort = 140),
|
||||||
|
@ExcelField(title = "12月", attrName = "amount12", align = Align.CENTER, sort = 150),
|
||||||
|
})
|
||||||
|
public ErpAccountCycle() {
|
||||||
|
this(null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErpAccountCycle(String accountId, String transactionType, String yearDate) {
|
||||||
|
this.accountId = accountId;
|
||||||
|
this.transactionType = transactionType;
|
||||||
|
this.yearDate = yearDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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.JoinTable;
|
||||||
|
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 2026-02-17
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Table(name = "erp_account_income_expense_view", alias = "a", label = "账户收支信息", columns = {
|
||||||
|
@Column(name = "account_id", attrName = "accountId", label = "交易账户", isPK = true),
|
||||||
|
@Column(name = "year_date", attrName = "yearDate", label = "year_date", isPK = true),
|
||||||
|
@Column(name = "income_amount", attrName = "incomeAmount", label = "income_amount", isUpdateForce = true),
|
||||||
|
@Column(name = "expense_amount", attrName = "expenseAmount", label = "expense_amount", isUpdateForce = true),
|
||||||
|
}, joinTable = {
|
||||||
|
@JoinTable(type = JoinTable.Type.LEFT_JOIN, entity = ErpAccount.class, attrName = "this", alias = "b",
|
||||||
|
on = "a.account_id = b.account_id",
|
||||||
|
columns = {
|
||||||
|
@Column(name = "account_name", attrName = "accountName", label = "账户名称"),
|
||||||
|
}),
|
||||||
|
}, orderBy = "a.account_id DESC, a.year_date DESC"
|
||||||
|
)
|
||||||
|
@Data
|
||||||
|
public class ErpAccountIncomeExpense extends DataEntity<ErpAccountIncomeExpense> implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private String accountId; // 交易账户
|
||||||
|
private String yearDate; // year_date
|
||||||
|
private Double incomeAmount; // income_amount
|
||||||
|
private Double expenseAmount; // expense_amount
|
||||||
|
|
||||||
|
private String accountName;
|
||||||
|
|
||||||
|
public ErpAccountIncomeExpense() {
|
||||||
|
this(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErpAccountIncomeExpense(String accountId, String yearDate) {
|
||||||
|
this.accountId = accountId;
|
||||||
|
this.yearDate = yearDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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.JoinTable;
|
||||||
|
import com.jeesite.common.mybatis.annotation.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类别收支Entity
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Table(name = "erp_category_income_expense_view", alias = "a", label = "类别收支信息", columns = {
|
||||||
|
@Column(name = "category_id", attrName = "categoryId", label = "交易分类", isPK = true),
|
||||||
|
@Column(name = "year_date", attrName = "yearDate", label = "year_date", isPK = true),
|
||||||
|
@Column(name = "income_amount", attrName = "incomeAmount", label = "income_amount", isUpdateForce = true),
|
||||||
|
@Column(name = "expense_amount", attrName = "expenseAmount", label = "expense_amount", isUpdateForce = true),
|
||||||
|
}, joinTable = {
|
||||||
|
@JoinTable(type = JoinTable.Type.LEFT_JOIN, entity = ErpCategory.class, attrName = "this", alias = "b",
|
||||||
|
on = "a.category_id = b.category_id",
|
||||||
|
columns = {
|
||||||
|
@Column(name = "parent_id", attrName = "parentId", label = "父级分类"),
|
||||||
|
@Column(name = "category_name", attrName = "categoryName", label = "分类名称"),
|
||||||
|
}),
|
||||||
|
}, orderBy = "a.category_id DESC, a.year_date DESC"
|
||||||
|
)
|
||||||
|
@Data
|
||||||
|
public class ErpCategoryIncomeExpense extends DataEntity<ErpCategoryIncomeExpense> implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private String categoryId; // 交易分类
|
||||||
|
private String yearDate; // year_date
|
||||||
|
private Double incomeAmount; // income_amount
|
||||||
|
private Double expenseAmount; // expense_amount
|
||||||
|
|
||||||
|
private String categoryName;
|
||||||
|
|
||||||
|
public ErpCategoryIncomeExpense() {
|
||||||
|
this(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErpCategoryIncomeExpense(String categoryId, String yearDate) {
|
||||||
|
this.categoryId = categoryId;
|
||||||
|
this.yearDate = yearDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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.utils.excel.annotation.ExcelField;
|
||||||
|
import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
|
||||||
|
import com.jeesite.common.utils.excel.annotation.ExcelFields;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年度收支信息Entity
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Table(name = "erp_income_expense_cycle_view", alias = "a", label = "年度收支信息信息", columns = {
|
||||||
|
@Column(name = "year_date", attrName = "yearDate", label = "year_date", isPK = true),
|
||||||
|
@Column(name = "month_date", attrName = "monthDate", label = "month_date", isPK = true),
|
||||||
|
@Column(name = "income_amount", attrName = "incomeAmount", label = "income_amount", isUpdateForce = true),
|
||||||
|
@Column(name = "expense_amount", attrName = "expenseAmount", label = "expense_amount", isUpdateForce = true),
|
||||||
|
}, orderBy = "a.year_date DESC, a.month_date DESC"
|
||||||
|
)
|
||||||
|
@Data
|
||||||
|
public class ErpIncomeExpenseCycle extends DataEntity<ErpIncomeExpenseCycle> implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private String yearDate; // year_date
|
||||||
|
private String monthDate; // month_date
|
||||||
|
private Double incomeAmount; // income_amount
|
||||||
|
private Double expenseAmount; // expense_amount
|
||||||
|
|
||||||
|
@ExcelFields({
|
||||||
|
@ExcelField(title = "year_date", attrName = "yearDate", align = Align.CENTER, sort = 10),
|
||||||
|
@ExcelField(title = "month_date", attrName = "monthDate", align = Align.CENTER, sort = 20),
|
||||||
|
@ExcelField(title = "income_amount", attrName = "incomeAmount", align = Align.CENTER, sort = 30),
|
||||||
|
@ExcelField(title = "expense_amount", attrName = "expenseAmount", align = Align.CENTER, sort = 40),
|
||||||
|
})
|
||||||
|
public ErpIncomeExpenseCycle() {
|
||||||
|
this(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErpIncomeExpenseCycle(String yearDate, String monthDate) {
|
||||||
|
this.yearDate = yearDate;
|
||||||
|
this.monthDate = monthDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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.ErpAccountCycle;
|
||||||
|
import com.jeesite.modules.erp.dao.ErpAccountCycleDao;
|
||||||
|
import com.jeesite.common.service.ServiceException;
|
||||||
|
import com.jeesite.common.config.Global;
|
||||||
|
import com.jeesite.common.validator.ValidatorUtils;
|
||||||
|
import com.jeesite.common.utils.excel.ExcelImport;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VIEWService
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ErpAccountCycleService extends CrudService<ErpAccountCycleDao, ErpAccountCycle> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单条数据
|
||||||
|
* @param erpAccountCycle 主键
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ErpAccountCycle get(ErpAccountCycle erpAccountCycle) {
|
||||||
|
return super.get(erpAccountCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分页数据
|
||||||
|
* @param erpAccountCycle 查询条件
|
||||||
|
* @param erpAccountCycle page 分页对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<ErpAccountCycle> findPage(ErpAccountCycle erpAccountCycle) {
|
||||||
|
return super.findPage(erpAccountCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表数据
|
||||||
|
* @param erpAccountCycle 查询条件
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<ErpAccountCycle> findList(ErpAccountCycle erpAccountCycle) {
|
||||||
|
return super.findList(erpAccountCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据(插入或更新)
|
||||||
|
* @param erpAccountCycle 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void save(ErpAccountCycle erpAccountCycle) {
|
||||||
|
super.save(erpAccountCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入数据
|
||||||
|
* @param file 导入的数据文件
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public String importData(MultipartFile file) {
|
||||||
|
if (file == null){
|
||||||
|
throw new ServiceException(text("请选择导入的数据文件!"));
|
||||||
|
}
|
||||||
|
int successNum = 0; int failureNum = 0;
|
||||||
|
StringBuilder successMsg = new StringBuilder();
|
||||||
|
StringBuilder failureMsg = new StringBuilder();
|
||||||
|
try(ExcelImport ei = new ExcelImport(file, 2, 0)){
|
||||||
|
List<ErpAccountCycle> list = ei.getDataList(ErpAccountCycle.class);
|
||||||
|
for (ErpAccountCycle erpAccountCycle : list) {
|
||||||
|
try{
|
||||||
|
ValidatorUtils.validateWithException(erpAccountCycle);
|
||||||
|
this.save(erpAccountCycle);
|
||||||
|
successNum++;
|
||||||
|
successMsg.append("<br/>" + successNum + "、编号 " + erpAccountCycle.getId() + " 导入成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
failureNum++;
|
||||||
|
String msg = "<br/>" + failureNum + "、编号 " + erpAccountCycle.getId() + " 导入失败:";
|
||||||
|
if (e instanceof ConstraintViolationException){
|
||||||
|
ConstraintViolationException cve = (ConstraintViolationException)e;
|
||||||
|
for (ConstraintViolation<?> violation : cve.getConstraintViolations()) {
|
||||||
|
msg += Global.getText(violation.getMessage()) + " ("+violation.getPropertyPath()+")";
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
msg += e.getMessage();
|
||||||
|
}
|
||||||
|
failureMsg.append(msg);
|
||||||
|
logger.error(msg, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
failureMsg.append(e.getMessage());
|
||||||
|
return failureMsg.toString();
|
||||||
|
}
|
||||||
|
if (failureNum > 0) {
|
||||||
|
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
|
||||||
|
throw new ServiceException(failureMsg.toString());
|
||||||
|
}else{
|
||||||
|
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
|
||||||
|
}
|
||||||
|
return successMsg.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新状态
|
||||||
|
* @param erpAccountCycle 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updateStatus(ErpAccountCycle erpAccountCycle) {
|
||||||
|
super.updateStatus(erpAccountCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
* @param erpAccountCycle 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(ErpAccountCycle erpAccountCycle) {
|
||||||
|
erpAccountCycle.sqlMap().markIdDelete(); // 逻辑删除时标记ID值
|
||||||
|
super.delete(erpAccountCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.ErpAccountIncomeExpense;
|
||||||
|
import com.jeesite.modules.erp.dao.ErpAccountIncomeExpenseDao;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户收支Service
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ErpAccountIncomeExpenseService extends CrudService<ErpAccountIncomeExpenseDao, ErpAccountIncomeExpense> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单条数据
|
||||||
|
* @param erpAccountIncomeExpense 主键
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ErpAccountIncomeExpense get(ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
return super.get(erpAccountIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分页数据
|
||||||
|
* @param erpAccountIncomeExpense 查询条件
|
||||||
|
* @param erpAccountIncomeExpense page 分页对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<ErpAccountIncomeExpense> findPage(ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
return super.findPage(erpAccountIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表数据
|
||||||
|
* @param erpAccountIncomeExpense 查询条件
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<ErpAccountIncomeExpense> findList(ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
return super.findList(erpAccountIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据(插入或更新)
|
||||||
|
* @param erpAccountIncomeExpense 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void save(ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
super.save(erpAccountIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新状态
|
||||||
|
* @param erpAccountIncomeExpense 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updateStatus(ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
super.updateStatus(erpAccountIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
* @param erpAccountIncomeExpense 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
erpAccountIncomeExpense.sqlMap().markIdDelete(); // 逻辑删除时标记ID值
|
||||||
|
super.delete(erpAccountIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.ErpCategoryIncomeExpense;
|
||||||
|
import com.jeesite.modules.erp.dao.ErpCategoryIncomeExpenseDao;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类别收支Service
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ErpCategoryIncomeExpenseService extends CrudService<ErpCategoryIncomeExpenseDao, ErpCategoryIncomeExpense> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单条数据
|
||||||
|
* @param erpCategoryIncomeExpense 主键
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ErpCategoryIncomeExpense get(ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
return super.get(erpCategoryIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分页数据
|
||||||
|
* @param erpCategoryIncomeExpense 查询条件
|
||||||
|
* @param erpCategoryIncomeExpense page 分页对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<ErpCategoryIncomeExpense> findPage(ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
return super.findPage(erpCategoryIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表数据
|
||||||
|
* @param erpCategoryIncomeExpense 查询条件
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<ErpCategoryIncomeExpense> findList(ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
return super.findList(erpCategoryIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据(插入或更新)
|
||||||
|
* @param erpCategoryIncomeExpense 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void save(ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
super.save(erpCategoryIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新状态
|
||||||
|
* @param erpCategoryIncomeExpense 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updateStatus(ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
super.updateStatus(erpCategoryIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
* @param erpCategoryIncomeExpense 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
erpCategoryIncomeExpense.sqlMap().markIdDelete(); // 逻辑删除时标记ID值
|
||||||
|
super.delete(erpCategoryIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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.ErpIncomeExpenseCycle;
|
||||||
|
import com.jeesite.modules.erp.dao.ErpIncomeExpenseCycleDao;
|
||||||
|
import com.jeesite.common.service.ServiceException;
|
||||||
|
import com.jeesite.common.config.Global;
|
||||||
|
import com.jeesite.common.validator.ValidatorUtils;
|
||||||
|
import com.jeesite.common.utils.excel.ExcelImport;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年度收支信息Service
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ErpIncomeExpenseCycleService extends CrudService<ErpIncomeExpenseCycleDao, ErpIncomeExpenseCycle> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单条数据
|
||||||
|
* @param erpIncomeExpenseCycle 主键
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ErpIncomeExpenseCycle get(ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
return super.get(erpIncomeExpenseCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询分页数据
|
||||||
|
* @param erpIncomeExpenseCycle 查询条件
|
||||||
|
* @param erpIncomeExpenseCycle page 分页对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Page<ErpIncomeExpenseCycle> findPage(ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
return super.findPage(erpIncomeExpenseCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表数据
|
||||||
|
* @param erpIncomeExpenseCycle 查询条件
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<ErpIncomeExpenseCycle> findList(ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
return super.findList(erpIncomeExpenseCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据(插入或更新)
|
||||||
|
* @param erpIncomeExpenseCycle 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void save(ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
super.save(erpIncomeExpenseCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入数据
|
||||||
|
* @param file 导入的数据文件
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public String importData(MultipartFile file) {
|
||||||
|
if (file == null){
|
||||||
|
throw new ServiceException(text("请选择导入的数据文件!"));
|
||||||
|
}
|
||||||
|
int successNum = 0; int failureNum = 0;
|
||||||
|
StringBuilder successMsg = new StringBuilder();
|
||||||
|
StringBuilder failureMsg = new StringBuilder();
|
||||||
|
try(ExcelImport ei = new ExcelImport(file, 2, 0)){
|
||||||
|
List<ErpIncomeExpenseCycle> list = ei.getDataList(ErpIncomeExpenseCycle.class);
|
||||||
|
for (ErpIncomeExpenseCycle erpIncomeExpenseCycle : list) {
|
||||||
|
try{
|
||||||
|
ValidatorUtils.validateWithException(erpIncomeExpenseCycle);
|
||||||
|
this.save(erpIncomeExpenseCycle);
|
||||||
|
successNum++;
|
||||||
|
successMsg.append("<br/>" + successNum + "、编号 " + erpIncomeExpenseCycle.getId() + " 导入成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
failureNum++;
|
||||||
|
String msg = "<br/>" + failureNum + "、编号 " + erpIncomeExpenseCycle.getId() + " 导入失败:";
|
||||||
|
if (e instanceof ConstraintViolationException){
|
||||||
|
ConstraintViolationException cve = (ConstraintViolationException)e;
|
||||||
|
for (ConstraintViolation<?> violation : cve.getConstraintViolations()) {
|
||||||
|
msg += Global.getText(violation.getMessage()) + " ("+violation.getPropertyPath()+")";
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
msg += e.getMessage();
|
||||||
|
}
|
||||||
|
failureMsg.append(msg);
|
||||||
|
logger.error(msg, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error(e.getMessage(), e);
|
||||||
|
failureMsg.append(e.getMessage());
|
||||||
|
return failureMsg.toString();
|
||||||
|
}
|
||||||
|
if (failureNum > 0) {
|
||||||
|
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
|
||||||
|
throw new ServiceException(failureMsg.toString());
|
||||||
|
}else{
|
||||||
|
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
|
||||||
|
}
|
||||||
|
return successMsg.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新状态
|
||||||
|
* @param erpIncomeExpenseCycle 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void updateStatus(ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
super.updateStatus(erpIncomeExpenseCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
* @param erpIncomeExpenseCycle 数据对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void delete(ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
erpIncomeExpenseCycle.sqlMap().markIdDelete(); // 逻辑删除时标记ID值
|
||||||
|
super.delete(erpIncomeExpenseCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.jeesite.modules.erp.web;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
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.collect.ListUtils;
|
||||||
|
import com.jeesite.common.entity.Page;
|
||||||
|
import com.jeesite.common.lang.DateUtils;
|
||||||
|
import com.jeesite.common.utils.excel.ExcelExport;
|
||||||
|
import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.jeesite.common.web.BaseController;
|
||||||
|
import com.jeesite.modules.erp.entity.ErpAccountCycle;
|
||||||
|
import com.jeesite.modules.erp.service.ErpAccountCycleService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VIEWController
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = "${adminPath}/erp/accountCycle")
|
||||||
|
public class ErpAccountCycleController extends BaseController {
|
||||||
|
|
||||||
|
private final ErpAccountCycleService erpAccountCycleService;
|
||||||
|
|
||||||
|
public ErpAccountCycleController(ErpAccountCycleService erpAccountCycleService) {
|
||||||
|
this.erpAccountCycleService = erpAccountCycleService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据
|
||||||
|
*/
|
||||||
|
@ModelAttribute
|
||||||
|
public ErpAccountCycle get(String accountId, String transactionType, String yearDate, boolean isNewRecord) {
|
||||||
|
ErpAccountCycle erpAccountCycle = new ErpAccountCycle();
|
||||||
|
erpAccountCycle.setAccountId(accountId);
|
||||||
|
erpAccountCycle.setTransactionType(transactionType);
|
||||||
|
erpAccountCycle.setYearDate(yearDate);
|
||||||
|
erpAccountCycle.setIsNewRecord(isNewRecord);
|
||||||
|
return erpAccountCycleService.getAndValid(erpAccountCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = {"list", ""})
|
||||||
|
public String list(ErpAccountCycle erpAccountCycle, Model model) {
|
||||||
|
model.addAttribute("erpAccountCycle", erpAccountCycle);
|
||||||
|
return "modules/erp/erpAccountCycleList";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "listData")
|
||||||
|
@ResponseBody
|
||||||
|
public Page<ErpAccountCycle> listData(ErpAccountCycle erpAccountCycle, HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
erpAccountCycle.setPage(new Page<>(request, response));
|
||||||
|
Page<ErpAccountCycle> page = erpAccountCycleService.findPage(erpAccountCycle);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看编辑表单
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "form")
|
||||||
|
public String form(ErpAccountCycle erpAccountCycle, Model model) {
|
||||||
|
model.addAttribute("erpAccountCycle", erpAccountCycle);
|
||||||
|
return "modules/erp/erpAccountCycleForm";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "save")
|
||||||
|
@ResponseBody
|
||||||
|
public String save(@Validated ErpAccountCycle erpAccountCycle) {
|
||||||
|
erpAccountCycleService.save(erpAccountCycle);
|
||||||
|
return renderResult(Global.TRUE, text("保存账户明细成功!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "exportData")
|
||||||
|
public void exportData(ErpAccountCycle erpAccountCycle, HttpServletResponse response) {
|
||||||
|
List<ErpAccountCycle> list = erpAccountCycleService.findList(erpAccountCycle);
|
||||||
|
String fileName = "账户明细" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
|
||||||
|
try (ExcelExport ee = new ExcelExport("账户明细", ErpAccountCycle.class)) {
|
||||||
|
ee.setDataList(list).write(response, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载模板
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "importTemplate")
|
||||||
|
public void importTemplate(HttpServletResponse response) {
|
||||||
|
ErpAccountCycle erpAccountCycle = new ErpAccountCycle();
|
||||||
|
List<ErpAccountCycle> list = ListUtils.newArrayList(erpAccountCycle);
|
||||||
|
String fileName = "账户明细模板.xlsx";
|
||||||
|
try (ExcelExport ee = new ExcelExport("账户明细", ErpAccountCycle.class, Type.IMPORT)) {
|
||||||
|
ee.setDataList(list).write(response, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入数据
|
||||||
|
*/
|
||||||
|
@ResponseBody
|
||||||
|
@PostMapping(value = "importData")
|
||||||
|
public String importData(MultipartFile file) {
|
||||||
|
try {
|
||||||
|
String message = erpAccountCycleService.importData(file);
|
||||||
|
return renderResult(Global.TRUE, "posfull:" + message);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return renderResult(Global.FALSE, "posfull:" + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "delete")
|
||||||
|
@ResponseBody
|
||||||
|
public String delete(ErpAccountCycle erpAccountCycle) {
|
||||||
|
erpAccountCycleService.delete(erpAccountCycle);
|
||||||
|
return renderResult(Global.TRUE, text("删除账户明细成功!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.jeesite.modules.erp.web;
|
||||||
|
|
||||||
|
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.ErpAccountIncomeExpense;
|
||||||
|
import com.jeesite.modules.erp.service.ErpAccountIncomeExpenseService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户收支Controller
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = "${adminPath}/erp/accountIncomeExpense")
|
||||||
|
public class ErpAccountIncomeExpenseController extends BaseController {
|
||||||
|
|
||||||
|
private final ErpAccountIncomeExpenseService erpAccountIncomeExpenseService;
|
||||||
|
|
||||||
|
public ErpAccountIncomeExpenseController(ErpAccountIncomeExpenseService erpAccountIncomeExpenseService) {
|
||||||
|
this.erpAccountIncomeExpenseService = erpAccountIncomeExpenseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据
|
||||||
|
*/
|
||||||
|
@ModelAttribute
|
||||||
|
public ErpAccountIncomeExpense get(String accountId, String yearDate, boolean isNewRecord) {
|
||||||
|
ErpAccountIncomeExpense erpAccountIncomeExpense = new ErpAccountIncomeExpense();
|
||||||
|
erpAccountIncomeExpense.setAccountId(accountId);
|
||||||
|
erpAccountIncomeExpense.setYearDate(yearDate);
|
||||||
|
erpAccountIncomeExpense.setIsNewRecord(isNewRecord);
|
||||||
|
return erpAccountIncomeExpenseService.getAndValid(erpAccountIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = {"list", ""})
|
||||||
|
public String list(ErpAccountIncomeExpense erpAccountIncomeExpense, Model model) {
|
||||||
|
model.addAttribute("erpAccountIncomeExpense", erpAccountIncomeExpense);
|
||||||
|
return "modules/erp/erpAccountIncomeExpenseList";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "listData")
|
||||||
|
@ResponseBody
|
||||||
|
public Page<ErpAccountIncomeExpense> listData(ErpAccountIncomeExpense erpAccountIncomeExpense, HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
erpAccountIncomeExpense.setPage(new Page<>(request, response));
|
||||||
|
Page<ErpAccountIncomeExpense> page = erpAccountIncomeExpenseService.findPage(erpAccountIncomeExpense);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看编辑表单
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "form")
|
||||||
|
public String form(ErpAccountIncomeExpense erpAccountIncomeExpense, Model model) {
|
||||||
|
model.addAttribute("erpAccountIncomeExpense", erpAccountIncomeExpense);
|
||||||
|
return "modules/erp/erpAccountIncomeExpenseForm";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "save")
|
||||||
|
@ResponseBody
|
||||||
|
public String save(@Validated ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
erpAccountIncomeExpenseService.save(erpAccountIncomeExpense);
|
||||||
|
return renderResult(Global.TRUE, text("保存账户收支成功!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "delete")
|
||||||
|
@ResponseBody
|
||||||
|
public String delete(ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
erpAccountIncomeExpenseService.delete(erpAccountIncomeExpense);
|
||||||
|
return renderResult(Global.TRUE, text("删除账户收支成功!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "listAll")
|
||||||
|
@ResponseBody
|
||||||
|
public List<ErpAccountIncomeExpense> listAll(ErpAccountIncomeExpense erpAccountIncomeExpense) {
|
||||||
|
return erpAccountIncomeExpenseService.findList(erpAccountIncomeExpense);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.jeesite.modules.erp.web;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
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.ErpCategoryIncomeExpense;
|
||||||
|
import com.jeesite.modules.erp.service.ErpCategoryIncomeExpenseService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类别收支Controller
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = "${adminPath}/erp/categoryIncomeExpense")
|
||||||
|
public class ErpCategoryIncomeExpenseController extends BaseController {
|
||||||
|
|
||||||
|
private final ErpCategoryIncomeExpenseService erpCategoryIncomeExpenseService;
|
||||||
|
|
||||||
|
public ErpCategoryIncomeExpenseController(ErpCategoryIncomeExpenseService erpCategoryIncomeExpenseService) {
|
||||||
|
this.erpCategoryIncomeExpenseService = erpCategoryIncomeExpenseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据
|
||||||
|
*/
|
||||||
|
@ModelAttribute
|
||||||
|
public ErpCategoryIncomeExpense get(String categoryId, String yearDate, boolean isNewRecord) {
|
||||||
|
ErpCategoryIncomeExpense erpCategoryIncomeExpense = new ErpCategoryIncomeExpense();
|
||||||
|
erpCategoryIncomeExpense.setCategoryId(categoryId);
|
||||||
|
erpCategoryIncomeExpense.setYearDate(yearDate);
|
||||||
|
erpCategoryIncomeExpense.setIsNewRecord(isNewRecord);
|
||||||
|
return erpCategoryIncomeExpenseService.getAndValid(erpCategoryIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = {"list", ""})
|
||||||
|
public String list(ErpCategoryIncomeExpense erpCategoryIncomeExpense, Model model) {
|
||||||
|
model.addAttribute("erpCategoryIncomeExpense", erpCategoryIncomeExpense);
|
||||||
|
return "modules/erp/erpCategoryIncomeExpenseList";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "listData")
|
||||||
|
@ResponseBody
|
||||||
|
public Page<ErpCategoryIncomeExpense> listData(ErpCategoryIncomeExpense erpCategoryIncomeExpense, HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
erpCategoryIncomeExpense.setPage(new Page<>(request, response));
|
||||||
|
Page<ErpCategoryIncomeExpense> page = erpCategoryIncomeExpenseService.findPage(erpCategoryIncomeExpense);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看编辑表单
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "form")
|
||||||
|
public String form(ErpCategoryIncomeExpense erpCategoryIncomeExpense, Model model) {
|
||||||
|
model.addAttribute("erpCategoryIncomeExpense", erpCategoryIncomeExpense);
|
||||||
|
return "modules/erp/erpCategoryIncomeExpenseForm";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "save")
|
||||||
|
@ResponseBody
|
||||||
|
public String save(@Validated ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
erpCategoryIncomeExpenseService.save(erpCategoryIncomeExpense);
|
||||||
|
return renderResult(Global.TRUE, text("保存类别收支成功!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "delete")
|
||||||
|
@ResponseBody
|
||||||
|
public String delete(ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
erpCategoryIncomeExpenseService.delete(erpCategoryIncomeExpense);
|
||||||
|
return renderResult(Global.TRUE, text("删除类别收支成功!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(value = "listAll")
|
||||||
|
@ResponseBody
|
||||||
|
public List<ErpCategoryIncomeExpense> listAll(ErpCategoryIncomeExpense erpCategoryIncomeExpense) {
|
||||||
|
return erpCategoryIncomeExpenseService.findList(erpCategoryIncomeExpense);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package com.jeesite.modules.erp.web;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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.collect.ListUtils;
|
||||||
|
import com.jeesite.common.entity.Page;
|
||||||
|
import com.jeesite.common.lang.DateUtils;
|
||||||
|
import com.jeesite.common.utils.excel.ExcelExport;
|
||||||
|
import com.jeesite.common.utils.excel.annotation.ExcelField.Type;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.jeesite.common.web.BaseController;
|
||||||
|
import com.jeesite.modules.erp.entity.ErpIncomeExpenseCycle;
|
||||||
|
import com.jeesite.modules.erp.service.ErpIncomeExpenseCycleService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 年度收支信息Controller
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @version 2026-02-17
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@RequestMapping(value = "${adminPath}/erp/incomeExpenseCycle")
|
||||||
|
public class ErpIncomeExpenseCycleController extends BaseController {
|
||||||
|
|
||||||
|
private final ErpIncomeExpenseCycleService erpIncomeExpenseCycleService;
|
||||||
|
|
||||||
|
public ErpIncomeExpenseCycleController(ErpIncomeExpenseCycleService erpIncomeExpenseCycleService) {
|
||||||
|
this.erpIncomeExpenseCycleService = erpIncomeExpenseCycleService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据
|
||||||
|
*/
|
||||||
|
@ModelAttribute
|
||||||
|
public ErpIncomeExpenseCycle get(String yearDate, String monthDate, boolean isNewRecord) {
|
||||||
|
ErpIncomeExpenseCycle erpIncomeExpenseCycle = new ErpIncomeExpenseCycle();
|
||||||
|
erpIncomeExpenseCycle.setYearDate(yearDate);
|
||||||
|
erpIncomeExpenseCycle.setMonthDate(monthDate);
|
||||||
|
erpIncomeExpenseCycle.setIsNewRecord(isNewRecord);
|
||||||
|
return erpIncomeExpenseCycleService.getAndValid(erpIncomeExpenseCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = {"list", ""})
|
||||||
|
public String list(ErpIncomeExpenseCycle erpIncomeExpenseCycle, Model model) {
|
||||||
|
model.addAttribute("erpIncomeExpenseCycle", erpIncomeExpenseCycle);
|
||||||
|
return "modules/erp/erpIncomeExpenseCycleList";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询列表数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "listData")
|
||||||
|
@ResponseBody
|
||||||
|
public Page<ErpIncomeExpenseCycle> listData(ErpIncomeExpenseCycle erpIncomeExpenseCycle, HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
erpIncomeExpenseCycle.setPage(new Page<>(request, response));
|
||||||
|
Page<ErpIncomeExpenseCycle> page = erpIncomeExpenseCycleService.findPage(erpIncomeExpenseCycle);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看编辑表单
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "form")
|
||||||
|
public String form(ErpIncomeExpenseCycle erpIncomeExpenseCycle, Model model) {
|
||||||
|
model.addAttribute("erpIncomeExpenseCycle", erpIncomeExpenseCycle);
|
||||||
|
return "modules/erp/erpIncomeExpenseCycleForm";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存数据
|
||||||
|
*/
|
||||||
|
@PostMapping(value = "save")
|
||||||
|
@ResponseBody
|
||||||
|
public String save(@Validated ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
erpIncomeExpenseCycleService.save(erpIncomeExpenseCycle);
|
||||||
|
return renderResult(Global.TRUE, text("保存年度收支信息成功!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "exportData")
|
||||||
|
public void exportData(ErpIncomeExpenseCycle erpIncomeExpenseCycle, HttpServletResponse response) {
|
||||||
|
List<ErpIncomeExpenseCycle> list = erpIncomeExpenseCycleService.findList(erpIncomeExpenseCycle);
|
||||||
|
String fileName = "年度收支信息" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
|
||||||
|
try (ExcelExport ee = new ExcelExport("年度收支信息", ErpIncomeExpenseCycle.class)) {
|
||||||
|
ee.setDataList(list).write(response, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载模板
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "importTemplate")
|
||||||
|
public void importTemplate(HttpServletResponse response) {
|
||||||
|
ErpIncomeExpenseCycle erpIncomeExpenseCycle = new ErpIncomeExpenseCycle();
|
||||||
|
List<ErpIncomeExpenseCycle> list = ListUtils.newArrayList(erpIncomeExpenseCycle);
|
||||||
|
String fileName = "年度收支信息模板.xlsx";
|
||||||
|
try (ExcelExport ee = new ExcelExport("年度收支信息", ErpIncomeExpenseCycle.class, Type.IMPORT)) {
|
||||||
|
ee.setDataList(list).write(response, fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入数据
|
||||||
|
*/
|
||||||
|
@ResponseBody
|
||||||
|
@PostMapping(value = "importData")
|
||||||
|
public String importData(MultipartFile file) {
|
||||||
|
try {
|
||||||
|
String message = erpIncomeExpenseCycleService.importData(file);
|
||||||
|
return renderResult(Global.TRUE, "posfull:" + message);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return renderResult(Global.FALSE, "posfull:" + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除数据
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "delete")
|
||||||
|
@ResponseBody
|
||||||
|
public String delete(ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
erpIncomeExpenseCycleService.delete(erpIncomeExpenseCycle);
|
||||||
|
return renderResult(Global.TRUE, text("删除年度收支信息成功!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@RequestMapping(value = "listAll")
|
||||||
|
@ResponseBody
|
||||||
|
public List<ErpIncomeExpenseCycle> listAll(ErpIncomeExpenseCycle erpIncomeExpenseCycle) {
|
||||||
|
return erpIncomeExpenseCycleService.findList(erpIncomeExpenseCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.ErpAccountCycleDao">
|
||||||
|
|
||||||
|
<!-- 查询数据
|
||||||
|
<select id="findList" resultType="ErpAccountCycle">
|
||||||
|
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.ErpAccountIncomeExpenseDao">
|
||||||
|
|
||||||
|
<!-- 查询数据
|
||||||
|
<select id="findList" resultType="ErpAccountIncomeExpense">
|
||||||
|
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.ErpCategoryIncomeExpenseDao">
|
||||||
|
|
||||||
|
<!-- 查询数据
|
||||||
|
<select id="findList" resultType="ErpCategoryIncomeExpense">
|
||||||
|
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.ErpIncomeExpenseCycleDao">
|
||||||
|
|
||||||
|
<!-- 查询数据
|
||||||
|
<select id="findList" resultType="ErpIncomeExpenseCycle">
|
||||||
|
SELECT ${sqlMap.column.toSql()}
|
||||||
|
FROM ${sqlMap.table.toSql()}
|
||||||
|
<where>
|
||||||
|
${sqlMap.where.toSql()}
|
||||||
|
</where>
|
||||||
|
ORDER BY ${sqlMap.order.toSql()}
|
||||||
|
</select> -->
|
||||||
|
|
||||||
|
</mapper>
|
||||||
58
web-vue/packages/erp/api/erp/accountCycle.ts
Normal file
58
web-vue/packages/erp/api/erp/accountCycle.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
import { UploadApiResult } from '@jeesite/core/api/sys/upload';
|
||||||
|
import { UploadFileParams } from '@jeesite/types/axios';
|
||||||
|
import { AxiosProgressEvent } from 'axios';
|
||||||
|
|
||||||
|
const { ctxPath, adminPath } = useGlobSetting();
|
||||||
|
|
||||||
|
export interface ErpAccountCycle extends BasicModel<ErpAccountCycle> {
|
||||||
|
accountId: string; // 交易账户
|
||||||
|
transactionType: string; // 交易类型
|
||||||
|
yearDate?: string; // 年度
|
||||||
|
amount01?: number; // 01月
|
||||||
|
amount02?: number; // 02月
|
||||||
|
amount03?: number; // 03月
|
||||||
|
amount04?: number; // 04月
|
||||||
|
amount05?: number; // 05月
|
||||||
|
amount06?: number; // 06月
|
||||||
|
amount07?: number; // 07月
|
||||||
|
amount08?: number; // 08月
|
||||||
|
amount09?: number; // 09月
|
||||||
|
amount10?: number; // 10月
|
||||||
|
amount11?: number; // 11月
|
||||||
|
amount12?: number; // 12月
|
||||||
|
}
|
||||||
|
|
||||||
|
export const erpAccountCycleList = (params?: ErpAccountCycle | any) =>
|
||||||
|
defHttp.get<ErpAccountCycle>({ url: adminPath + '/erp/accountCycle/list', params });
|
||||||
|
|
||||||
|
export const erpAccountCycleListData = (params?: ErpAccountCycle | any) =>
|
||||||
|
defHttp.post<Page<ErpAccountCycle>>({ url: adminPath + '/erp/accountCycle/listData', params });
|
||||||
|
|
||||||
|
export const erpAccountCycleForm = (params?: ErpAccountCycle | any) =>
|
||||||
|
defHttp.get<ErpAccountCycle>({ url: adminPath + '/erp/accountCycle/form', params });
|
||||||
|
|
||||||
|
export const erpAccountCycleSave = (params?: any, data?: ErpAccountCycle | any) =>
|
||||||
|
defHttp.postJson<ErpAccountCycle>({ url: adminPath + '/erp/accountCycle/save', params, data });
|
||||||
|
|
||||||
|
export const erpAccountCycleImportData = (
|
||||||
|
params: UploadFileParams,
|
||||||
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => void,
|
||||||
|
) =>
|
||||||
|
defHttp.uploadFile<UploadApiResult>(
|
||||||
|
{
|
||||||
|
url: ctxPath + adminPath + '/erp/accountCycle/importData',
|
||||||
|
onUploadProgress,
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const erpAccountCycleDelete = (params?: ErpAccountCycle | any) =>
|
||||||
|
defHttp.get<ErpAccountCycle>({ url: adminPath + '/erp/accountCycle/delete', params });
|
||||||
35
web-vue/packages/erp/api/erp/accountIncomeExpense.ts
Normal file
35
web-vue/packages/erp/api/erp/accountIncomeExpense.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 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 ErpAccountIncomeExpense extends BasicModel<ErpAccountIncomeExpense> {
|
||||||
|
accountId: string; // 交易账户
|
||||||
|
yearDate?: string; // year_date
|
||||||
|
incomeAmount?: number; // income_amount
|
||||||
|
expenseAmount?: number; // expense_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
export const erpAccountIncomeExpenseList = (params?: ErpAccountIncomeExpense | any) =>
|
||||||
|
defHttp.get<ErpAccountIncomeExpense>({ url: adminPath + '/erp/accountIncomeExpense/list', params });
|
||||||
|
|
||||||
|
export const erpAccountIncomeExpenseListAll = (params?: ErpAccountIncomeExpense | any) =>
|
||||||
|
defHttp.get<ErpAccountIncomeExpense[]>({ url: adminPath + '/erp/accountIncomeExpense/listAll', params });
|
||||||
|
|
||||||
|
export const erpAccountIncomeExpenseListData = (params?: ErpAccountIncomeExpense | any) =>
|
||||||
|
defHttp.post<Page<ErpAccountIncomeExpense>>({ url: adminPath + '/erp/accountIncomeExpense/listData', params });
|
||||||
|
|
||||||
|
export const erpAccountIncomeExpenseForm = (params?: ErpAccountIncomeExpense | any) =>
|
||||||
|
defHttp.get<ErpAccountIncomeExpense>({ url: adminPath + '/erp/accountIncomeExpense/form', params });
|
||||||
|
|
||||||
|
export const erpAccountIncomeExpenseSave = (params?: any, data?: ErpAccountIncomeExpense | any) =>
|
||||||
|
defHttp.postJson<ErpAccountIncomeExpense>({ url: adminPath + '/erp/accountIncomeExpense/save', params, data });
|
||||||
|
|
||||||
|
export const erpAccountIncomeExpenseDelete = (params?: ErpAccountIncomeExpense | any) =>
|
||||||
|
defHttp.get<ErpAccountIncomeExpense>({ url: adminPath + '/erp/accountIncomeExpense/delete', params });
|
||||||
35
web-vue/packages/erp/api/erp/categoryIncomeExpense.ts
Normal file
35
web-vue/packages/erp/api/erp/categoryIncomeExpense.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 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 ErpCategoryIncomeExpense extends BasicModel<ErpCategoryIncomeExpense> {
|
||||||
|
categoryId: string; // 交易分类
|
||||||
|
yearDate?: string; // year_date
|
||||||
|
incomeAmount?: number; // income_amount
|
||||||
|
expenseAmount?: number; // expense_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
export const erpCategoryIncomeExpenseList = (params?: ErpCategoryIncomeExpense | any) =>
|
||||||
|
defHttp.get<ErpCategoryIncomeExpense>({ url: adminPath + '/erp/categoryIncomeExpense/list', params });
|
||||||
|
|
||||||
|
export const erpCategoryIncomeExpenseListAll = (params?: ErpCategoryIncomeExpense | any) =>
|
||||||
|
defHttp.get<ErpCategoryIncomeExpense[]>({ url: adminPath + '/erp/categoryIncomeExpense/listAll', params });
|
||||||
|
|
||||||
|
export const erpCategoryIncomeExpenseListData = (params?: ErpCategoryIncomeExpense | any) =>
|
||||||
|
defHttp.post<Page<ErpCategoryIncomeExpense>>({ url: adminPath + '/erp/categoryIncomeExpense/listData', params });
|
||||||
|
|
||||||
|
export const erpCategoryIncomeExpenseForm = (params?: ErpCategoryIncomeExpense | any) =>
|
||||||
|
defHttp.get<ErpCategoryIncomeExpense>({ url: adminPath + '/erp/categoryIncomeExpense/form', params });
|
||||||
|
|
||||||
|
export const erpCategoryIncomeExpenseSave = (params?: any, data?: ErpCategoryIncomeExpense | any) =>
|
||||||
|
defHttp.postJson<ErpCategoryIncomeExpense>({ url: adminPath + '/erp/categoryIncomeExpense/save', params, data });
|
||||||
|
|
||||||
|
export const erpCategoryIncomeExpenseDelete = (params?: ErpCategoryIncomeExpense | any) =>
|
||||||
|
defHttp.get<ErpCategoryIncomeExpense>({ url: adminPath + '/erp/categoryIncomeExpense/delete', params });
|
||||||
50
web-vue/packages/erp/api/erp/incomeExpenseCycle.ts
Normal file
50
web-vue/packages/erp/api/erp/incomeExpenseCycle.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
import { UploadApiResult } from '@jeesite/core/api/sys/upload';
|
||||||
|
import { UploadFileParams } from '@jeesite/types/axios';
|
||||||
|
import { AxiosProgressEvent } from 'axios';
|
||||||
|
|
||||||
|
const { ctxPath, adminPath } = useGlobSetting();
|
||||||
|
|
||||||
|
export interface ErpIncomeExpenseCycle extends BasicModel<ErpIncomeExpenseCycle> {
|
||||||
|
yearDate?: string; // year_date
|
||||||
|
monthDate?: string; // month_date
|
||||||
|
incomeAmount?: number; // income_amount
|
||||||
|
expenseAmount?: number; // expense_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
export const erpIncomeExpenseCycleList = (params?: ErpIncomeExpenseCycle | any) =>
|
||||||
|
defHttp.get<ErpIncomeExpenseCycle>({ url: adminPath + '/erp/incomeExpenseCycle/list', params });
|
||||||
|
|
||||||
|
export const erpIncomeExpenseCycleListAll = (params?: ErpIncomeExpenseCycle | any) =>
|
||||||
|
defHttp.get<ErpIncomeExpenseCycle[]>({ url: adminPath + '/erp/incomeExpenseCycle/listAll', params });
|
||||||
|
|
||||||
|
export const erpIncomeExpenseCycleListData = (params?: ErpIncomeExpenseCycle | any) =>
|
||||||
|
defHttp.post<Page<ErpIncomeExpenseCycle>>({ url: adminPath + '/erp/incomeExpenseCycle/listData', params });
|
||||||
|
|
||||||
|
export const erpIncomeExpenseCycleForm = (params?: ErpIncomeExpenseCycle | any) =>
|
||||||
|
defHttp.get<ErpIncomeExpenseCycle>({ url: adminPath + '/erp/incomeExpenseCycle/form', params });
|
||||||
|
|
||||||
|
export const erpIncomeExpenseCycleSave = (params?: any, data?: ErpIncomeExpenseCycle | any) =>
|
||||||
|
defHttp.postJson<ErpIncomeExpenseCycle>({ url: adminPath + '/erp/incomeExpenseCycle/save', params, data });
|
||||||
|
|
||||||
|
export const erpIncomeExpenseCycleImportData = (
|
||||||
|
params: UploadFileParams,
|
||||||
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => void,
|
||||||
|
) =>
|
||||||
|
defHttp.uploadFile<UploadApiResult>(
|
||||||
|
{
|
||||||
|
url: ctxPath + adminPath + '/erp/incomeExpenseCycle/importData',
|
||||||
|
onUploadProgress,
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const erpIncomeExpenseCycleDelete = (params?: ErpIncomeExpenseCycle | any) =>
|
||||||
|
defHttp.get<ErpIncomeExpenseCycle>({ url: adminPath + '/erp/incomeExpenseCycle/delete', params });
|
||||||
238
web-vue/packages/erp/views/erp/erpInc/account/list.vue
Normal file
238
web-vue/packages/erp/views/erp/erpInc/account/list.vue
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<!--
|
||||||
|
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
|
||||||
|
* No deletion without permission, or be held responsible to law.
|
||||||
|
* @author gaoxq
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<BasicTable @register="registerTable">
|
||||||
|
<template #tableTitle>
|
||||||
|
<Icon :icon="getTitle.icon" class="m-1 pr-1" />
|
||||||
|
<span> {{ getTitle.value }} </span>
|
||||||
|
</template>
|
||||||
|
<template #toolbar>
|
||||||
|
<a-button type="default" :loading="loading" @click="handleExport()">
|
||||||
|
<Icon icon="i-ant-design:download-outlined" /> {{ t('导出') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</BasicTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup name="ViewsErpAccountCycleList">
|
||||||
|
import { onMounted, ref, unref } from 'vue';
|
||||||
|
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
|
||||||
|
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
|
||||||
|
import { useGlobSetting } from '@jeesite/core/hooks/setting';
|
||||||
|
import { downloadByUrl } from '@jeesite/core/utils/file/download';
|
||||||
|
import { router } from '@jeesite/core/router';
|
||||||
|
import { Icon } from '@jeesite/core/components/Icon';
|
||||||
|
import { BasicTable, BasicColumn, useTable } from '@jeesite/core/components/Table';
|
||||||
|
import { ErpAccountCycle, erpAccountCycleList } from '@jeesite/erp/api/erp/accountCycle';
|
||||||
|
import { erpAccountCycleDelete, erpAccountCycleListData } from '@jeesite/erp/api/erp/accountCycle';
|
||||||
|
import { useDrawer } from '@jeesite/core/components/Drawer';
|
||||||
|
import { useModal } from '@jeesite/core/components/Modal';
|
||||||
|
import { FormProps } from '@jeesite/core/components/Form';
|
||||||
|
|
||||||
|
const { t } = useI18n('erp.accountCycle');
|
||||||
|
const { showMessage } = useMessage();
|
||||||
|
const { meta } = unref(router.currentRoute);
|
||||||
|
const record = ref<ErpAccountCycle>({} as ErpAccountCycle);
|
||||||
|
|
||||||
|
const getTitle = {
|
||||||
|
icon: meta.icon || 'i-ant-design:book-outlined',
|
||||||
|
value: meta.title || t('账户分析管理'),
|
||||||
|
};
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const searchForm: FormProps<ErpAccountCycle> = {
|
||||||
|
baseColProps: { md: 8, lg: 6 },
|
||||||
|
labelWidth: 90,
|
||||||
|
schemas: [
|
||||||
|
{
|
||||||
|
label: t('交易账户'),
|
||||||
|
field: 'accountId',
|
||||||
|
fieldLabel: 'accountName',
|
||||||
|
component: 'ListSelect',
|
||||||
|
componentProps: {
|
||||||
|
selectType: 'erpAccountSelect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('交易类型'),
|
||||||
|
field: 'transactionType',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
dictType: 'transaction_type',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('年度'),
|
||||||
|
field: 'yearDate',
|
||||||
|
component: 'Input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableColumns: BasicColumn<ErpAccountCycle>[] = [
|
||||||
|
{
|
||||||
|
title: t('交易账户'),
|
||||||
|
dataIndex: 'accountName',
|
||||||
|
key: 'b.account_name',
|
||||||
|
sorter: true,
|
||||||
|
width: 100,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('交易类型'),
|
||||||
|
dataIndex: 'transactionType',
|
||||||
|
key: 'a.transaction_type',
|
||||||
|
sorter: true,
|
||||||
|
width: 100,
|
||||||
|
align: 'left',
|
||||||
|
dictType: 'transaction_type',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('年度'),
|
||||||
|
dataIndex: 'yearDate',
|
||||||
|
key: 'a.year_date',
|
||||||
|
sorter: true,
|
||||||
|
width: 80,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('01月'),
|
||||||
|
dataIndex: 'amount01',
|
||||||
|
key: 'a.amount_01',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('02月'),
|
||||||
|
dataIndex: 'amount02',
|
||||||
|
key: 'a.amount_02',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('03月'),
|
||||||
|
dataIndex: 'amount03',
|
||||||
|
key: 'a.amount_03',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('04月'),
|
||||||
|
dataIndex: 'amount04',
|
||||||
|
key: 'a.amount_04',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('05月'),
|
||||||
|
dataIndex: 'amount05',
|
||||||
|
key: 'a.amount_05',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('06月'),
|
||||||
|
dataIndex: 'amount06',
|
||||||
|
key: 'a.amount_06',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('07月'),
|
||||||
|
dataIndex: 'amount07',
|
||||||
|
key: 'a.amount_07',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('08月'),
|
||||||
|
dataIndex: 'amount08',
|
||||||
|
key: 'a.amount_08',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('09月'),
|
||||||
|
dataIndex: 'amount09',
|
||||||
|
key: 'a.amount_09',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('10月'),
|
||||||
|
dataIndex: 'amount10',
|
||||||
|
key: 'a.amount_10',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('11月'),
|
||||||
|
dataIndex: 'amount11',
|
||||||
|
key: 'a.amount_11',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('12月'),
|
||||||
|
dataIndex: 'amount12',
|
||||||
|
key: 'a.amount_12',
|
||||||
|
sorter: true,
|
||||||
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [registerTable, { reload, getForm }] = useTable<ErpAccountCycle>({
|
||||||
|
api: erpAccountCycleListData,
|
||||||
|
beforeFetch: (params) => {
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
columns: tableColumns,
|
||||||
|
formConfig: searchForm,
|
||||||
|
showTableSetting: true,
|
||||||
|
useSearchForm: true,
|
||||||
|
canResize: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const res = await erpAccountCycleList();
|
||||||
|
record.value = (res.erpAccountCycle || {}) as ErpAccountCycle;
|
||||||
|
await getForm().setFieldsValue(record.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [registerDrawer, { openDrawer }] = useDrawer();
|
||||||
|
|
||||||
|
function handleForm(record: Recordable) {
|
||||||
|
openDrawer(true, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExport() {
|
||||||
|
loading.value = true;
|
||||||
|
const { ctxAdminPath } = useGlobSetting();
|
||||||
|
await downloadByUrl({
|
||||||
|
url: ctxAdminPath + '/erp/accountCycle/exportData',
|
||||||
|
params: getForm().getFieldsValue(),
|
||||||
|
});
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSuccess(record: Recordable) {
|
||||||
|
await reload({ record });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
512
web-vue/packages/erp/views/erp/green/components/ChartAccount.vue
Normal file
512
web-vue/packages/erp/views/erp/green/components/ChartAccount.vue
Normal file
@@ -0,0 +1,512 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度账户收支情况" style="width: 100%; height: 100%;">
|
||||||
|
<template #extra>
|
||||||
|
<div class="status-filter-container">
|
||||||
|
<div class="status-filter">
|
||||||
|
<span
|
||||||
|
v-for="item in statusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:class="['status-item', { active: currentStatus === item.value }]"
|
||||||
|
@click="handleStatusChange(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="chartDom" style="width: 100%; height: 100%;"></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 { erpAccountIncomeExpenseListAll } from '@jeesite/erp/api/erp/accountIncomeExpense';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const statusOptions = ref([
|
||||||
|
{ value: String(currentYear), label: '今年' },
|
||||||
|
{ value: String(currentYear - 1), label: '去年' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentStatus = ref<string>(String(currentYear));
|
||||||
|
|
||||||
|
const handleStatusChange = (status: string) => {
|
||||||
|
if (currentStatus.value === status) return;
|
||||||
|
currentStatus.value = status;
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ErpIncExpRatio {
|
||||||
|
incomeAmount?: number | string;
|
||||||
|
expenseAmount?: number | string;
|
||||||
|
accountName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawData = ref<ErpIncExpRatio[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let myChart: echarts.ECharts | null = null;
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
const formatNumber = (num: number | string | undefined, decimal = 2): number => {
|
||||||
|
const parsed = Number(num);
|
||||||
|
if (isNaN(parsed)) return 0;
|
||||||
|
return Number(parsed.toFixed(decimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTenThousandYuan = (num: number | string | undefined): number => {
|
||||||
|
const rawNum = formatNumber(num);
|
||||||
|
return formatNumber(rawNum / 10000);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 sortByCategory = (data: ErpIncExpRatio[]): ErpIncExpRatio[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const nameA = a.accountName || '';
|
||||||
|
const nameB = b.accountName || '';
|
||||||
|
return nameA.localeCompare(nameB, 'zh-CN');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const barLabelConfig = {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
distance: 3,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: '500'
|
||||||
|
},
|
||||||
|
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
yearDate: currentStatus.value
|
||||||
|
}
|
||||||
|
const result = await erpAccountIncomeExpenseListAll(params);
|
||||||
|
const validData = (result || [])
|
||||||
|
.filter(item => item.accountName)
|
||||||
|
.map(item => ({
|
||||||
|
monthDate: item.monthDate,
|
||||||
|
accountName: item.accountName,
|
||||||
|
incomeAmount: formatNumber(item.incomeAmount),
|
||||||
|
expenseAmount: formatNumber(item.expenseAmount)
|
||||||
|
})) as ErpIncExpRatio[];
|
||||||
|
rawData.value = sortByCategory(validData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据列表失败:', error);
|
||||||
|
rawData.value = [];
|
||||||
|
} finally {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateYAxisExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [0, 10];
|
||||||
|
|
||||||
|
const formattedData = data.map(num => formatNumber(num));
|
||||||
|
const min = Math.min(...formattedData);
|
||||||
|
const max = Math.max(...formattedData);
|
||||||
|
|
||||||
|
const padding = 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;
|
||||||
|
|
||||||
|
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: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: [],
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => formatNumber(value).toFixed(2)
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [],
|
||||||
|
noDataLoadingOption: {
|
||||||
|
text: '暂无收入支出数据',
|
||||||
|
textStyle: { fontSize: 16, color: '#666', fontWeight: '500' },
|
||||||
|
position: 'center',
|
||||||
|
effect: 'none'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xAxisData = rawData.value.map(item => item.accountName || '').filter(Boolean);
|
||||||
|
const incomeData = rawData.value.map(item => toTenThousandYuan(item.incomeAmount));
|
||||||
|
const expenseData = rawData.value.map(item => toTenThousandYuan(item.expenseAmount));
|
||||||
|
|
||||||
|
const amountData = [...incomeData, ...expenseData];
|
||||||
|
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||||
|
|
||||||
|
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 currentCategory = params[0]?.axisValue || '';
|
||||||
|
const item = rawData.value.find(i => i.accountName === currentCategory);
|
||||||
|
|
||||||
|
if (!item) return `<div style="padding: 8px;">${currentCategory}:暂无明细</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;">${currentCategory}</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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['收入', '支出'],
|
||||||
|
top: 10,
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xAxisData,
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
margin: 8
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
},
|
||||||
|
boundaryGap: true
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
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: 0, color: '#096dd9' }
|
||||||
|
]),
|
||||||
|
borderRadius: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
barGap: '30%',
|
||||||
|
barCategoryGap: '20%',
|
||||||
|
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: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
label: barLabelConfig,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#ff4d4f' },
|
||||||
|
{ offset: 1, color: '#f5222d' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
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();
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
if (myChart) {
|
||||||
|
myChart.dispose();
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(rawData, () => {
|
||||||
|
initChart();
|
||||||
|
}, { deep: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:not(.active):hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card:hover) {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-yaxis-name) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-item) {
|
||||||
|
margin-right: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-label) {
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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,512 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度分类收支情况" style="width: 100%; height: 100%;">
|
||||||
|
<template #extra>
|
||||||
|
<div class="status-filter-container">
|
||||||
|
<div class="status-filter">
|
||||||
|
<span
|
||||||
|
v-for="item in statusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:class="['status-item', { active: currentStatus === item.value }]"
|
||||||
|
@click="handleStatusChange(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="chartDom" style="width: 100%; height: 100%;"></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 { erpCategoryIncomeExpenseListAll } from '@jeesite/erp/api/erp/categoryIncomeExpense';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const statusOptions = ref([
|
||||||
|
{ value: String(currentYear), label: '今年' },
|
||||||
|
{ value: String(currentYear - 1), label: '去年' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentStatus = ref<string>(String(currentYear));
|
||||||
|
|
||||||
|
const handleStatusChange = (status: string) => {
|
||||||
|
if (currentStatus.value === status) return;
|
||||||
|
currentStatus.value = status;
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ErpIncExpRatio {
|
||||||
|
incomeAmount?: number | string;
|
||||||
|
expenseAmount?: number | string;
|
||||||
|
categoryName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawData = ref<ErpIncExpRatio[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let myChart: echarts.ECharts | null = null;
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
const formatNumber = (num: number | string | undefined, decimal = 2): number => {
|
||||||
|
const parsed = Number(num);
|
||||||
|
if (isNaN(parsed)) return 0;
|
||||||
|
return Number(parsed.toFixed(decimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTenThousandYuan = (num: number | string | undefined): number => {
|
||||||
|
const rawNum = formatNumber(num);
|
||||||
|
return formatNumber(rawNum / 10000);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 sortByCategory = (data: ErpIncExpRatio[]): ErpIncExpRatio[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const nameA = a.categoryName || '';
|
||||||
|
const nameB = b.categoryName || '';
|
||||||
|
return nameA.localeCompare(nameB, 'zh-CN');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const barLabelConfig = {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
distance: 3,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: '500'
|
||||||
|
},
|
||||||
|
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
yearDate: currentStatus.value
|
||||||
|
}
|
||||||
|
const result = await erpCategoryIncomeExpenseListAll(params);
|
||||||
|
const validData = (result || [])
|
||||||
|
.filter(item => item.categoryName)
|
||||||
|
.map(item => ({
|
||||||
|
monthDate: item.monthDate,
|
||||||
|
categoryName: item.categoryName,
|
||||||
|
incomeAmount: formatNumber(item.incomeAmount),
|
||||||
|
expenseAmount: formatNumber(item.expenseAmount)
|
||||||
|
})) as ErpIncExpRatio[];
|
||||||
|
rawData.value = sortByCategory(validData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据列表失败:', error);
|
||||||
|
rawData.value = [];
|
||||||
|
} finally {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateYAxisExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [0, 10];
|
||||||
|
|
||||||
|
const formattedData = data.map(num => formatNumber(num));
|
||||||
|
const min = Math.min(...formattedData);
|
||||||
|
const max = Math.max(...formattedData);
|
||||||
|
|
||||||
|
const padding = 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;
|
||||||
|
|
||||||
|
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: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: [],
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => formatNumber(value).toFixed(2)
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [],
|
||||||
|
noDataLoadingOption: {
|
||||||
|
text: '暂无收入支出数据',
|
||||||
|
textStyle: { fontSize: 16, color: '#666', fontWeight: '500' },
|
||||||
|
position: 'center',
|
||||||
|
effect: 'none'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xAxisData = rawData.value.map(item => item.categoryName || '').filter(Boolean);
|
||||||
|
const incomeData = rawData.value.map(item => toTenThousandYuan(item.incomeAmount));
|
||||||
|
const expenseData = rawData.value.map(item => toTenThousandYuan(item.expenseAmount));
|
||||||
|
|
||||||
|
const amountData = [...incomeData, ...expenseData];
|
||||||
|
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||||
|
|
||||||
|
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 currentCategory = params[0]?.axisValue || '';
|
||||||
|
const item = rawData.value.find(i => i.categoryName === currentCategory);
|
||||||
|
|
||||||
|
if (!item) return `<div style="padding: 8px;">${currentCategory}:暂无明细</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;">${currentCategory}</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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['收入', '支出'],
|
||||||
|
top: 10,
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xAxisData,
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
margin: 8
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
},
|
||||||
|
boundaryGap: true
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
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: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
barGap: '30%',
|
||||||
|
barCategoryGap: '20%',
|
||||||
|
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: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
label: barLabelConfig,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#ff4d4f' },
|
||||||
|
{ offset: 1, color: '#f5222d' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
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();
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
if (myChart) {
|
||||||
|
myChart.dispose();
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(rawData, () => {
|
||||||
|
initChart();
|
||||||
|
}, { deep: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:not(.active):hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card:hover) {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-yaxis-name) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-item) {
|
||||||
|
margin-right: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-label) {
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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>
|
||||||
524
web-vue/packages/erp/views/erp/green/components/ChartLine.vue
Normal file
524
web-vue/packages/erp/views/erp/green/components/ChartLine.vue
Normal file
@@ -0,0 +1,524 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度各月收支情况" style="width: 100%; height: 100%;">
|
||||||
|
<template #extra>
|
||||||
|
<div class="status-filter-container">
|
||||||
|
<div class="status-filter">
|
||||||
|
<span
|
||||||
|
v-for="item in statusOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:class="['status-item', { active: currentStatus === item.value }]"
|
||||||
|
@click="handleStatusChange(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="chartDom" style="width: 100%; height: 100%;"></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 { erpIncomeExpenseCycleListAll } from '@jeesite/erp/api/erp/incomeExpenseCycle';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const statusOptions = ref([
|
||||||
|
{ value: String(currentYear), label: '今年' },
|
||||||
|
{ value: String(currentYear - 1), label: '去年' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentStatus = ref<string>(String(currentYear));
|
||||||
|
|
||||||
|
const handleStatusChange = (status: string) => {
|
||||||
|
if (currentStatus.value === status) return;
|
||||||
|
currentStatus.value = status;
|
||||||
|
fetchList();
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ErpIncExpRatio {
|
||||||
|
monthDate?: string;
|
||||||
|
incomeAmount?: number | string;
|
||||||
|
expenseAmount?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawData = ref<ErpIncExpRatio[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let myChart: echarts.ECharts | null = null;
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
const formatNumber = (num: number | string | undefined, decimal = 2): number => {
|
||||||
|
const parsed = Number(num);
|
||||||
|
if (isNaN(parsed)) return 0;
|
||||||
|
return Number(parsed.toFixed(decimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTenThousandYuan = (num: number | string | undefined): number => {
|
||||||
|
const rawNum = formatNumber(num);
|
||||||
|
return formatNumber(rawNum / 10000);
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortDates = (data: ErpIncExpRatio[]): ErpIncExpRatio[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const dateA = new Date(a.monthDate || '');
|
||||||
|
const dateB = new Date(b.monthDate || '');
|
||||||
|
return dateA.getTime() - dateB.getTime();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const barLabelConfig = {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
distance: 3,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: '500'
|
||||||
|
},
|
||||||
|
formatter: (params: any) => `${formatNumber(params.value).toFixed(2)}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
yearDate: currentStatus.value
|
||||||
|
}
|
||||||
|
const result = await erpIncomeExpenseCycleListAll(params);
|
||||||
|
const validData = (result || [])
|
||||||
|
.filter(item => item.monthDate)
|
||||||
|
.map(item => ({
|
||||||
|
monthDate: formatDate(item.monthDate),
|
||||||
|
incomeAmount: formatNumber(item.incomeAmount),
|
||||||
|
expenseAmount: formatNumber(item.expenseAmount)
|
||||||
|
})) as ErpIncExpRatio[];
|
||||||
|
rawData.value = sortDates(validData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据列表失败:', error);
|
||||||
|
rawData.value = [];
|
||||||
|
} finally {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateYAxisExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [0, 10];
|
||||||
|
|
||||||
|
const formattedData = data.map(num => formatNumber(num));
|
||||||
|
const min = Math.min(...formattedData);
|
||||||
|
const max = Math.max(...formattedData);
|
||||||
|
|
||||||
|
const padding = 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;
|
||||||
|
|
||||||
|
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: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: [],
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => formatNumber(value).toFixed(2)
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [],
|
||||||
|
noDataLoadingOption: {
|
||||||
|
text: '暂无收入支出数据',
|
||||||
|
textStyle: { fontSize: 16, color: '#666', fontWeight: '500' },
|
||||||
|
position: 'center',
|
||||||
|
effect: 'none'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xAxisData = rawData.value.map(item => {
|
||||||
|
const dateStr = item.monthDate || '';
|
||||||
|
const monthMatch = dateStr.match(/(\d{1,2})$/);
|
||||||
|
return monthMatch ? `${monthMatch[1]}月` : dateStr;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
const incomeData = rawData.value.map(item => toTenThousandYuan(item.incomeAmount));
|
||||||
|
const expenseData = rawData.value.map(item => toTenThousandYuan(item.expenseAmount));
|
||||||
|
|
||||||
|
const amountData = [...incomeData, ...expenseData];
|
||||||
|
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||||
|
|
||||||
|
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 => {
|
||||||
|
const monthMatch = (i.monthDate || '').match(/(\d{1,2})$/);
|
||||||
|
return monthMatch && `${monthMatch[1]}月` === 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['收入', '支出'],
|
||||||
|
top: 10,
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xAxisData,
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
margin: 8
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
},
|
||||||
|
boundaryGap: true
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
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: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
barGap: '30%',
|
||||||
|
barCategoryGap: '20%',
|
||||||
|
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: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
label: barLabelConfig,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#ff4d4f' },
|
||||||
|
{ offset: 1, color: '#f5222d' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
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();
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
if (myChart) {
|
||||||
|
myChart.dispose();
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(rawData, () => {
|
||||||
|
initChart();
|
||||||
|
}, { deep: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:not(.active):hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card:hover) {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-yaxis-name) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-item) {
|
||||||
|
margin-right: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-label) {
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
: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>
|
||||||
506
web-vue/packages/erp/views/erp/green/components/ChartPie.vue
Normal file
506
web-vue/packages/erp/views/erp/green/components/ChartPie.vue
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="账户余额" class="account-balance-card">
|
||||||
|
<template #extra>
|
||||||
|
<div class="total-amount">
|
||||||
|
总金额:<span class="amount-value">{{ totalAmountText }}</span> 元
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="layout-container">
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-title">本年收入</span>
|
||||||
|
<div class="stat-content">
|
||||||
|
<Icon icon="icons/erp-income.png" size="28"/>
|
||||||
|
<span class="stat-value">{{ thisIncome }} 元</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-title">本年支出</span>
|
||||||
|
<div class="stat-content">
|
||||||
|
<Icon icon="icons/erp-expense.png" size="28"/>
|
||||||
|
<span class="stat-value">{{ thisExpense }} 元</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-title">消费占比</span>
|
||||||
|
<div class="stat-content">
|
||||||
|
<Icon icon="icons/erp-share.png" size="28"/>
|
||||||
|
<span class="stat-value">{{ thisShare }} %</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-panel">
|
||||||
|
<div ref="chartDom" class="chart-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue';
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
import { Icon } from '@jeesite/core/components/Icon';
|
||||||
|
import { erpAccountListAll } from '@jeesite/erp/api/erp/account';
|
||||||
|
import { erpIncExpRatioListAll } from '@jeesite/erp/api/erp/incExpRatio';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
import type { ECharts, EChartsOption } from 'echarts';
|
||||||
|
|
||||||
|
// 核心:初始化时明确赋值,避免null
|
||||||
|
const listAccount = ref<any[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let myChart: ECharts | null = null;
|
||||||
|
let resizeTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const thisIncome = ref(0);
|
||||||
|
const thisExpense = ref(0);
|
||||||
|
const thisShare = ref(0);
|
||||||
|
|
||||||
|
const allLegendNames = ref<string[]>([]);
|
||||||
|
const selectedMap = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// 总金额计算(增加空值防护)
|
||||||
|
const totalAmountText = computed(() => {
|
||||||
|
if (!listAccount.value.length) return '0.00';
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
listAccount.value.forEach(item => {
|
||||||
|
const name = item.accountName || '未知账户';
|
||||||
|
// 未显式取消的都算选中
|
||||||
|
if (selectedMap.value[name] !== false) {
|
||||||
|
total += Number(item.currentBalance || 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return total.toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 百分比格式化(增强容错)
|
||||||
|
const formatPercent = (value: number) => {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (isNaN(parsed) || !isFinite(parsed)) return '0.0%';
|
||||||
|
return `${parsed.toFixed(1)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 颜色生成(简化逻辑)
|
||||||
|
const generateRandomColors = (count: number): string[] => {
|
||||||
|
const colorLibrary = [
|
||||||
|
'#1890ff', '#52c41a', '#f5a623', '#fa8c16', '#722ed1', '#eb2f96',
|
||||||
|
'#13c2c2', '#2f54eb', '#f7ba1e', '#f5222d', '#8543e0', '#0fc6c2',
|
||||||
|
'#7cb305', '#ff7a45', '#ff4d4f', '#6b778c', '#5d7092', '#91d5ff'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (count <= colorLibrary.length) {
|
||||||
|
return colorLibrary.slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = [...colorLibrary];
|
||||||
|
while (colors.length < count) {
|
||||||
|
const hue = Math.floor(Math.random() * 360);
|
||||||
|
colors.push(`hsl(${hue}, 75%, 55%)`);
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取本年收支数据(增加错误捕获)
|
||||||
|
const getThisData = async () => {
|
||||||
|
try {
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
const res = await erpIncExpRatioListAll({ cycleType: 'Y', statDate: year });
|
||||||
|
thisIncome.value = Number(res?.[0]?.incomeAmount) || 0;
|
||||||
|
thisExpense.value = Number(res?.[0]?.expenseAmount) || 0;
|
||||||
|
thisShare.value = Number(res?.[0]?.expenseRatio) || 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取收支数据失败:', error);
|
||||||
|
thisIncome.value = 0;
|
||||||
|
thisExpense.value = 0;
|
||||||
|
thisShare.value = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取账户列表(增加错误捕获)
|
||||||
|
const fetchList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await erpAccountListAll();
|
||||||
|
listAccount.value = res || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取账户列表失败:', error);
|
||||||
|
listAccount.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化图表(核心修复:全链路null防护)
|
||||||
|
const initChart = () => {
|
||||||
|
// 1. 先检查DOM是否存在
|
||||||
|
if (!chartDom.value) {
|
||||||
|
console.warn('图表容器DOM不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 安全销毁旧实例
|
||||||
|
if (myChart) {
|
||||||
|
try {
|
||||||
|
myChart.off('legendselectchanged'); // 先解绑事件
|
||||||
|
myChart.dispose();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('销毁旧图表实例失败:', e);
|
||||||
|
}
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 3. 重新创建实例(强制指定尺寸)
|
||||||
|
myChart = echarts.init(chartDom.value, undefined, {
|
||||||
|
width: chartDom.value.clientWidth || 'auto',
|
||||||
|
height: chartDom.value.clientHeight || 'auto'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('创建图表实例失败:', e);
|
||||||
|
myChart = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 空数据处理(避免操作null实例)
|
||||||
|
if (!listAccount.value.length) {
|
||||||
|
const emptyOption: EChartsOption = {
|
||||||
|
title: {
|
||||||
|
text: '暂无账户数据',
|
||||||
|
left: 'center',
|
||||||
|
top: 'middle',
|
||||||
|
textStyle: { fontSize: 14, color: '#999' }
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'item' },
|
||||||
|
legend: { show: false },
|
||||||
|
series: []
|
||||||
|
};
|
||||||
|
myChart.setOption(emptyOption);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 处理有效数据
|
||||||
|
const pieData = listAccount.value
|
||||||
|
.map(item => ({
|
||||||
|
name: item.accountName || '未知账户',
|
||||||
|
value: Number(item.currentBalance) || 0
|
||||||
|
}))
|
||||||
|
.filter(item => item.value > 0 && item.name); // 过滤空名称和0值
|
||||||
|
|
||||||
|
// 6. 再次空数据检查
|
||||||
|
if (!pieData.length) {
|
||||||
|
const emptyOption: EChartsOption = {
|
||||||
|
title: {
|
||||||
|
text: '暂无有效账户金额数据',
|
||||||
|
left: 'center',
|
||||||
|
top: 'middle',
|
||||||
|
textStyle: { fontSize: 14, color: '#999' }
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'item' },
|
||||||
|
legend: { show: false },
|
||||||
|
series: []
|
||||||
|
};
|
||||||
|
myChart.setOption(emptyOption);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 准备图表数据
|
||||||
|
allLegendNames.value = pieData.map(p => p.name);
|
||||||
|
const colors = generateRandomColors(pieData.length);
|
||||||
|
const total = pieData.reduce((sum, item) => sum + item.value, 0);
|
||||||
|
|
||||||
|
// 8. 构建选中状态映射
|
||||||
|
const selected: Record<string, boolean> = {};
|
||||||
|
pieData.forEach(item => {
|
||||||
|
// 未设置过的默认选中
|
||||||
|
selected[item.name] = selectedMap.value[item.name] !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 9. 图表配置(简化且安全)
|
||||||
|
const option: EChartsOption = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
formatter: ({ name, value, percent }: any) => `${name}: ${value} 元 (${(percent * 100).toFixed(1)}%)`,
|
||||||
|
textStyle: { fontSize: 11 },
|
||||||
|
padding: 8,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderColor: '#e8e8e8',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
right: 10,
|
||||||
|
top: 'center',
|
||||||
|
textStyle: { fontSize: 11, color: '#666' },
|
||||||
|
itemWidth: 10,
|
||||||
|
itemHeight: 10,
|
||||||
|
itemGap: 8,
|
||||||
|
selected: selected, // 绑定选中状态
|
||||||
|
formatter: (name: string) => name.length > 8 ? `${name.slice(0, 8)}...` : name
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '账户金额',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['30%', '70%'],
|
||||||
|
center: ['40%', '50%'],
|
||||||
|
avoidLabelOverlap: true,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 6,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: 11,
|
||||||
|
formatter: (params: any) => {
|
||||||
|
if (total <= 0) return params.name;
|
||||||
|
return `${params.name} ${formatPercent((params.value / total) * 100)}`;
|
||||||
|
},
|
||||||
|
color: '#333',
|
||||||
|
distance: 10
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: true,
|
||||||
|
length: 10,
|
||||||
|
length2: 8,
|
||||||
|
smooth: 0.2,
|
||||||
|
lineStyle: { width: 1, color: '#999' }
|
||||||
|
},
|
||||||
|
data: pieData.map((item, index) => ({
|
||||||
|
name: item.name,
|
||||||
|
value: item.value,
|
||||||
|
itemStyle: { color: colors[index] }
|
||||||
|
}))
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 10. 设置配置项(安全执行)
|
||||||
|
try {
|
||||||
|
myChart.setOption(option);
|
||||||
|
|
||||||
|
// 11. 绑定图例事件(先解绑再绑定,避免重复)
|
||||||
|
myChart.off('legendselectchanged');
|
||||||
|
myChart.on('legendselectchanged', (params: any) => {
|
||||||
|
if (params && params.name !== undefined) {
|
||||||
|
selectedMap.value[params.name] = params.selected[params.name];
|
||||||
|
// 无需重新初始化,ECharts会自动更新视图
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('设置图表配置失败:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 防抖调整图表尺寸(增加null防护)
|
||||||
|
const resizeChart = () => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(() => {
|
||||||
|
if (myChart && chartDom.value) {
|
||||||
|
try {
|
||||||
|
myChart.resize({
|
||||||
|
width: chartDom.value.clientWidth || 'auto',
|
||||||
|
height: chartDom.value.clientHeight || 'auto'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('调整图表尺寸失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听数据变化(使用nextTick确保DOM就绪)
|
||||||
|
watch(listAccount, () => {
|
||||||
|
nextTick(() => initChart());
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// 生命周期(增加安全防护)
|
||||||
|
onMounted(async () => {
|
||||||
|
// 先加载数据
|
||||||
|
await Promise.all([fetchList(), getThisData()]);
|
||||||
|
|
||||||
|
// DOM渲染完成后初始化图表
|
||||||
|
nextTick(() => {
|
||||||
|
initChart();
|
||||||
|
// 绑定窗口大小变化事件
|
||||||
|
window.addEventListener('resize', resizeChart);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理定时器
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
|
||||||
|
// 解绑事件
|
||||||
|
window.removeEventListener('resize', resizeChart);
|
||||||
|
|
||||||
|
// 安全销毁图表实例
|
||||||
|
if (myChart) {
|
||||||
|
try {
|
||||||
|
myChart.off('legendselectchanged');
|
||||||
|
myChart.dispose();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('销毁图表实例失败:', e);
|
||||||
|
}
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
selectedMap.value = {};
|
||||||
|
allLegendNames.value = [];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.account-balance-card {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-head) {
|
||||||
|
padding: 0 16px !important;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
height: 56px !important;
|
||||||
|
line-height: 56px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
height: calc(100% - 56px) !important;
|
||||||
|
width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-amount {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-value {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
width: 150px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background-color: #f0f7ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #e6f4ff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.stat-content .icon) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1890ff;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100% !important;
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.left-panel::-webkit-scrollbar) {
|
||||||
|
width: 4px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.left-panel::-webkit-scrollbar-thumb) {
|
||||||
|
background: #d9d9d9;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.layout-container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
width: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
max-height: 120px;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-panel {
|
||||||
|
height: calc(100% - 132px) !important;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend) {
|
||||||
|
top: auto !important;
|
||||||
|
bottom: 10px !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
orient: 'horizontal' !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
115
web-vue/packages/erp/views/erp/green/index.vue
Normal file
115
web-vue/packages/erp/views/erp/green/index.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="erp-green-page-container">
|
||||||
|
<div class="top-section">
|
||||||
|
<div class="top-left">
|
||||||
|
<ChartPie />
|
||||||
|
</div>
|
||||||
|
<div class="top-middle">
|
||||||
|
<ChartLine />
|
||||||
|
</div>
|
||||||
|
<div class="top-right">上右区域</div>
|
||||||
|
</div>
|
||||||
|
<div class="middle-section">
|
||||||
|
<div class="middle-left">
|
||||||
|
<ChartAccount />
|
||||||
|
</div>
|
||||||
|
<div class="middle-right">
|
||||||
|
<ChartCategory />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-section">
|
||||||
|
<div class="bottom-left">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="bottom-right">下右区域</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup name="ErpGreenPage">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { BasicForm, FormProps } from '@jeesite/core/components/Form';
|
||||||
|
|
||||||
|
import ChartPie from './components/ChartPie.vue';
|
||||||
|
import ChartLine from './components/ChartLine.vue';
|
||||||
|
|
||||||
|
import ChartAccount from './components/ChartAccount.vue'
|
||||||
|
import ChartCategory from './components/ChartCategory.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.erp-green-page-container {
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-section {
|
||||||
|
height: 30%; /* 顶部占30% */
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-left, .top-middle, .top-right {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
/* 子元素垂直居中 */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-section {
|
||||||
|
height: 30%;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-left, .middle-right {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-section {
|
||||||
|
height: 40%;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-left, .bottom-right {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.top-middle) {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user