1. 指标子集映射

This commit is contained in:
李玉东 2025-08-17 14:19:25 +08:00
parent c1a401fcc9
commit 49f0bbcc01
45 changed files with 2415 additions and 1061 deletions

View File

@ -1,240 +1,205 @@
<!DOCTYPE html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<title>AHP 两两比较矩阵Tabler风格</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!-- Tabler Core CSS如果项目已全局引入可移除 -->
<link href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css" rel="stylesheet"/>
<title>指标设置 Demo</title>
<!-- Tabler 样式(也可换成你项目内置的 tabler.min.css -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css"/>
<style>
.matrix-wrap {
max-height: 60vh;
overflow: auto; /* 同时允许横向+纵向滚动 */
border: 1px solid #e3e8ee;
border-radius: .5rem;
background: #fff;
:root{
/* 你的主题主色,可按项目实际调整 */
--accent: #206bc4;
}
table.matrix { min-width: 720px; table-layout: fixed; }
.matrix thead th {
position: sticky; top: 0; z-index: 3;
background: #fff; box-shadow: 0 2px 0 rgba(0,0,0,.03);
/* 强调卡片 */
.card-priority{
border: 2px solid var(--accent);
box-shadow: 0 10px 30px rgba(32,107,196,.16);
background: linear-gradient(180deg, rgba(32,107,196,.04), rgba(32,107,196,.02));
position: sticky; /* 吸顶 */
top: 12px;
z-index: 3;
transition: box-shadow .18s ease, transform .18s ease;
}
.matrix th:first-child, .matrix td:first-child {
position: sticky; left: 0; z-index: 2; background: #fff;
box-shadow: 2px 0 0 rgba(0,0,0,.03);
.card-priority .card-header{
background: linear-gradient(90deg, rgba(32,107,196,.10), rgba(32,107,196,0));
border-bottom: 1px solid rgba(32,107,196,.2);
}
.priority-dot{
width: 10px; height: 10px; border-radius: 999px;
background: var(--accent);
box-shadow: 0 0 0 4px rgba(32,107,196,.15);
margin-right: .5rem;
}
.card-priority:hover,
.card-priority:focus-within{
box-shadow: 0 14px 40px rgba(32,107,196,.24);
transform: translateY(-1px);
}
/* 切换时的短暂高亮 */
@keyframes flashHighlight{
0% { box-shadow: 0 0 0 0 rgba(32,107,196,0); }
20% { box-shadow: 0 0 0 6px rgba(32,107,196,.25); }
100% { box-shadow: 0 10px 30px rgba(32,107,196,.16); }
}
.card-priority.flash{ animation: flashHighlight .9s ease-out; }
/* 次级卡片更弱一些,拉开层级 */
.card-secondary{
border: 1px solid rgba(0,0,0,.06);
box-shadow: 0 4px 14px rgba(0,0,0,.06);
}
.matrix .diag { background: #f6f8fb; text-align: center; font-weight: 600; }
.w-col { width: 110px; }
.select-judge { width: 100%; }
.cell-readonly { background: #fafbfc; color:#666; }
.small-note { font-size: .875rem; color: #666; }
.badge-cr { margin-left: .5rem; }
</style>
</head>
<body class="layout-fluid">
<body class="theme-light">
<div class="page">
<header class="navbar navbar-expand-md navbar-light d-print-none" style="border-bottom:1px solid rgba(0,0,0,.06);">
<div class="container-xl">
<h2 class="navbar-brand">海上评估系统</h2>
<div class="ms-auto text-muted">管理员</div>
</div>
</header>
<div class="page-body">
<div class="container-xl">
<div class="card">
<div class="card-header">
<h3 class="card-title">两两比较矩阵</h3>
<!-- 重点卡片:选择指标 -->
<div class="card card-priority mb-3" id="cardPriority">
<div class="card-header d-flex align-items-center">
<span class="priority-dot"></span>
<h3 class="card-title mb-0">选择指标
<span class="badge bg-primary ms-2">必填</span>
</h3>
<div class="ms-auto">
<span class="small-note">选择“行 相对 列”的重要性1=同等3/5/7/9=逐级更重要)</span>
<button class="btn btn-primary" id="btnNext">下一步</button>
</div>
</div>
<div class="card-body">
<div id="matrixContainer" class="matrix-wrap">
<table id="matrixTable" class="table table-vcenter card-table matrix">
<!-- JS 动态生成 -->
</table>
</div>
<div class="mt-3 d-flex flex-wrap gap-2">
<button id="btnEven" class="btn btn-outline-secondary">上三角置1同等重要</button>
<button id="btnReset" class="btn btn-outline-warning">重置</button>
<button id="btnExport" class="btn btn-outline-primary">导出矩阵JSON</button>
<span id="crBadge" class="badge badge-cr">CR: --</span>
<div class="row g-3 align-items-center">
<div class="col-auto">
<label for="metricSelect" class="col-form-label">指标列表</label>
</div>
<div class="col-12 col-sm-6 col-lg-4">
<select id="metricSelect" class="form-select">
<option value="" selected disabled>请选择一个指标</option>
<option value="aaa">AAA</option>
<option value="bbb">BBB</option>
<option value="ccc">CCC</option>
</select>
</div>
<div class="col-12 col-lg-auto">
<span class="form-hint">选择后将影响下方所有设置。</span>
</div>
</div>
</div>
</div>
<!-- 次级卡片:评价集设置 -->
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title mb-0">评价集设置</h3>
<div class="ms-auto">
<button class="btn btn-outline-primary" id="btnAdd">增加评价</button>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<!-- 左侧:子集列表 -->
<div class="col-12 col-md-3">
<div class="list-group">
<label class="list-group-item">
<input class="form-check-input me-2" type="radio" name="subset" value="x1"> x1
</label>
<label class="list-group-item">
<input class="form-check-input me-2" type="radio" name="subset" value="c2" checked> c2
</label>
</div>
</div>
<!-- 右侧:表格占位 -->
<div class="col-12 col-md-9">
<div class="table-responsive">
<table class="table table-vcenter">
<thead>
<tr>
<th>序号</th>
<th>名称</th>
<th>符号</th>
<th>下限值</th>
<th>符号</th>
<th>上限值</th>
</tr>
</thead>
<tbody id="rulesBody">
<tr class="text-muted">
<td colspan="6">尚未添加评价规则,点击右上角“增加评价”。</td>
</tr>
</tbody>
</table>
</div>
</div>
</div> <!-- row -->
</div>
</div>
<!-- 占位内容,方便滚动测试吸顶 -->
<div style="height: 40vh;"></div>
</div>
</div>
</div>
<!-- jQuery如已全局引入可删 -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
/** ===== 示例你的指标列表id + name ===== */
const indicators = [
{ id: 'A', name: '性能' },
{ id: 'B', name: '价格' },
{ id: 'C', name: '服务' },
{ id: 'D', name: '交付周期' }
];
// 切换指标时,给重点卡片一个轻微高亮动画
const card = document.getElementById('cardPriority');
const select = document.getElementById('metricSelect');
select.addEventListener('change', () => {
card.classList.add('flash');
setTimeout(() => card.classList.remove('flash'), 900);
});
/** ===== AHP 工具:几何平均法权重 + CI/CR ===== */
function geometricMeanWeights(A) {
const n = A.length;
const gm = new Array(n).fill(0);
for (let i=0;i<n;i++) {
let prod = 1;
for (let j=0;j<n;j++) prod *= A[i][j];
gm[i] = Math.pow(prod, 1/n);
}
const sum = gm.reduce((a,b)=>a+b,0);
return gm.map(x=>x/sum);
}
function lambdaMaxApprox(A, w) {
// 近似 λmax = 平均((A w)_i / w_i)
const n = A.length;
const Aw = new Array(n).fill(0);
for (let i=0;i<n;i++) {
let s=0; for (let j=0;j<n;j++) s += A[i][j]*w[j];
Aw[i] = s;
}
let acc = 0;
for (let i=0;i<n;i++) acc += Aw[i]/w[i];
return acc/n;
}
function ci(lambdaMax, n){ return (lambdaMax - n) / (n - 1); }
function cr(lambdaMax, n){
// Saaty RIn=1..10
const RI = [0.00,0.00,0.58,0.90,1.12,1.24,1.32,1.41,1.45,1.49];
if (n<1 || n>10) return NaN;
const v = ci(lambdaMax, n);
return RI[n-1]===0 ? 0 : v/RI[n-1];
}
/** ===== UI1..9 标度(上三角选择),自动倒数到下三角 ===== */
const scaleOptions = [
{v:1/9, t:'1/9 极端不如'},
{v:1/8, t:'1/8'},
{v:1/7, t:'1/7 很不如'},
{v:1/6, t:'1/6'},
{v:1/5, t:'1/5 明显不如'},
{v:1/4, t:'1/4'},
{v:1/3, t:'1/3 略不如'},
{v:1/2, t:'1/2'},
{v:1, t:'1 同等'},
{v:2, t:'2'},
{v:3, t:'3 略重要'},
{v:4, t:'4'},
{v:5, t:'5 明显重要'},
{v:6, t:'6'},
{v:7, t:'7 很重要'},
{v:8, t:'8'},
{v:9, t:'9 极端重要'}
];
function buildMatrix(list){
const n = list.length;
const $table = $("#matrixTable");
let thead = '<thead><tr><th style="min-width:200px;"></th>';
for (let j=0;j<n;j++) thead += `<th title="${list[j].id}">${list[j].name}</th>`;
thead += `<th class="w-col">权重</th></tr></thead>`;
let tbody = '<tbody>';
for (let i=0;i<n;i++){
tbody += `<tr data-row="${i}"><th>${list[i].name}</th>`;
for (let j=0;j<n;j++){
if (i===j) {
tbody += `<td class="diag">1</td>`;
} else if (i<j) {
// 上三角:选择器
tbody += `<td data-cell="${i}-${j}">
<select class="form-select form-select-sm select-judge" data-i="${i}" data-j="${j}">
${scaleOptions.map(o=>`<option value="${o.v}">${o.t}</option>`).join('')}
</select>
</td>`;
} else { // 下三角:只读,显示倒数
tbody += `<td data-cell="${i}-${j}" class="cell-readonly text-end"></td>`;
}
}
tbody += `<td class="w-col text-end" data-weight="${i}"></td>`;
tbody += `</tr>`;
}
tbody += '</tbody>';
$table.html(thead + tbody);
// 默认上三角全 1同等
$table.find('.select-judge').val('1');
// 绑定联动
$table.on('change', '.select-judge', function(){
const i = parseInt(this.dataset.i,10), j = parseInt(this.dataset.j,10);
const v = parseFloat(this.value);
// 设置下三角为倒数
const recip = 1 / v;
const $mirror = $table.find(`[data-cell="${j}-${i}"]`);
$mirror.text(recip.toFixed(6));
recalcWeights();
});
// 初次同步一次下三角并计算
$table.find('.select-judge').trigger('change');
}
/** 读取矩阵为二维数组 */
function readMatrix(){
const n = $("#matrixTable thead th").length - 2; // 去掉左上空格和“权重”列
const A = Array.from({length:n}, ()=>Array(n).fill(1));
for (let i=0;i<n;i++){
for (let j=0;j<n;j++){
if (i===j) { A[i][j]=1; continue; }
if (i<j){
const v = parseFloat($(`.select-judge[data-i="${i}"][data-j="${j}"]`).val());
A[i][j] = v;
A[j][i] = 1/v;
}
}
}
return A;
}
/** 计算权重/CR 并渲染 */
function recalcWeights(){
const A = readMatrix();
const w = geometricMeanWeights(A);
const lm = lambdaMaxApprox(A, w);
const CR = cr(lm, A.length);
// 渲染权重列
for (let i=0;i<w.length;i++){
$(`[data-weight="${i}"]`).text(w[i].toFixed(4));
}
// 渲染 CR
const $b = $("#crBadge");
$b.text(`CR: ${isNaN(CR) ? '--' : CR.toFixed(4)}`);
$b.removeClass('bg-green bg-red bg-blue');
if (!isNaN(CR)) {
$b.addClass(CR < 0.10 ? 'bg-green' : 'bg-red');
// 下一步按钮:如果未选择则聚焦并闪烁
document.getElementById('btnNext').addEventListener('click', () => {
if (!select.value) {
select.focus();
card.classList.add('flash');
setTimeout(() => card.classList.remove('flash'), 900);
} else {
$b.addClass('bg-blue');
// 这里写你的跳转或展开逻辑
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
}
});
/** 事件:置同等、重置、导出 */
$(function(){
buildMatrix(indicators);
$("#btnEven").on('click', function(){
$('.select-judge').val('1').trigger('change');
});
$("#btnReset").on('click', function(){
$("#matrixTable").off(); // 解绑旧事件
buildMatrix(indicators); // 重建
});
$("#btnExport").on('click', function(){
const A = readMatrix();
console.log('Matrix:', A);
alert('矩阵已输出到控制台。');
});
// 增加评价:简单追加一行示例
document.getElementById('btnAdd').addEventListener('click', () => {
const body = document.getElementById('rulesBody');
if (body.firstElementChild && body.firstElementChild.classList.contains('text-muted')) {
body.innerHTML = '';
}
const idx = body.children.length + 1;
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${idx}</td>
<td><input class="form-control form-control-sm" placeholder="名称"/></td>
<td>
<select class="form-select form-select-sm">
<option>&gt;=</option><option>&gt;</option><option>=</option>
<option>&lt;=</option><option>&lt;</option>
</select>
</td>
<td><input class="form-control form-control-sm" placeholder="下限"/></td>
<td>
<select class="form-select form-select-sm">
<option>&lt;=</option><option>&lt;</option><option>=</option>
<option>&gt;=</option><option>&gt;</option>
</select>
</td>
<td><input class="form-control form-control-sm" placeholder="上限"/></td>
`;
body.appendChild(tr);
});
</script>
</body>

View File

@ -0,0 +1,39 @@
package com.hshh.evaluation.bean;
import java.util.List;
import java.util.Map;
import lombok.Data;
/**
* 模板权重暂存数据.
*
* @author LiDongYU
* @since 2025/7/22
*/
@Data
public class DraftWeightData {
/**
* 页面定义的临时key.
*/
private String key;
/**
* 父指标ID.
*/
private Integer parentIndicationId;
/**
* 模板ID.
*/
private Integer templateId;
/**
* 表头map linkedMap. key是指标id,value是 {id,name}
*/
private Map<Integer, MetricTableHeaderBean> headerMap;
/**
* 页面中指标表的权重信息设置.key 为fromIndicatorId+"_"+toIndicatorId.
*/
private List<List<MetricMapperWeightBean>> weight;
}

View File

@ -0,0 +1,24 @@
package com.hshh.evaluation.bean;
import java.util.List;
import java.util.Map;
import lombok.Data;
/**
* 动态返回表格.
*
* @author LiDongYU
* @since 2025/7/22
*/
@Data
public class DynamicTable {
/**
* 表头map linkedMap.
*/
private Map<Integer, MetricTableHeaderBean> headerMap;
private List<List<MetricMapperWeightBean>> weight;
}

View File

@ -0,0 +1,18 @@
package com.hshh.evaluation.bean;
import java.util.List;
import lombok.Data;
/**
* 计算指标的请求对象.
*
* @author LiDongYU
* @since 2025/7/22
*/
@Data
public class MetricComputeRequest {
private List<String> metric;
private List<List<MetricMapperWeightBean>> weightList;
}

View File

@ -0,0 +1,16 @@
package com.hshh.evaluation.bean;
import lombok.Data;
/**
* 指标计算后的结果.
*
* @author LiDongYU
* @since 2025/7/22
*/
@Data
public class MetricComputerResponse {
private String id;
private String weight;
}

View File

@ -0,0 +1,34 @@
package com.hshh.evaluation.bean;
import lombok.Data;
/**
* 指标权重映射描述.
*
* @author LiDongYU
* @since 2025/7/22
*/
@Data
public class MetricMapperWeightBean {
/**
* 从那个指标开始.
*/
private int rowId;
/**
* 到那个指标.
*/
private int colId;
/**
* 权重.
*/
private String value;
/**
* 父指标ID.
*/
private int parentId;
/**
* 行号.
*/
private int rowNum;
}

View File

@ -0,0 +1,16 @@
package com.hshh.evaluation.bean;
import lombok.Data;
/**
* 指标动态表头.
*
* @author LiDongYU
* @since 2025/7/22
*/
@Data
public class MetricTableHeaderBean {
private int id;
private String name;
}

View File

@ -0,0 +1,124 @@
package com.hshh.evaluation.controller;
import com.hshh.evaluation.bean.DraftWeightData;
import com.hshh.evaluation.bean.MetricMapperWeightBean;
import com.hshh.evaluation.entity.EvaluationTemplate;
import com.hshh.evaluation.entity.EvaluationTemplateWeight;
import com.hshh.indicator.entity.Indicator;
import com.hshh.system.common.bean.BaseController;
import com.hshh.system.common.util.DraftStore;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 评估模板类辅助方法.
*
* @author LiDongYU
* @since 2025/7/22
*/
public class AssistantTemplateController extends BaseController {
/**
* 转化指标权重列表到double二维数组.
*
* @param weightList 指标权重列表
* @return list string数组
*/
protected List<String[]> convertMetricMapperWeightBeanListToStrArray(
List<List<MetricMapperWeightBean>> weightList) {
List<String[]> data = new ArrayList<>();
weightList.forEach(weight -> {
String[] strArr = new String[weight.size()];
data.add(strArr);
for (int i = 0; i < weight.size(); i++) {
strArr[i] = weight.get(i).getValue();
}
});
return data;
}
/**
* 组装历史权重数据.
*
* @param evaluationTemplate 当前模板数据
* @return 评估数据列表
*/
@SuppressWarnings("unchecked")
protected Map<Integer, DraftWeightData> unitWeightMap(
EvaluationTemplate evaluationTemplate) {
// 获取缓存中的数据
Map<Integer, DraftWeightData> cacheData = DraftStore.get(evaluationTemplate.getDraftKey(),
Map.class);
if (cacheData == null) {
if (evaluationTemplate.getCurrentPagePartData() != null) {
Map<Integer, DraftWeightData> defaultMap = new LinkedHashMap<>();
DraftWeightData weightData = createDefaultDraftWeightData(evaluationTemplate);
defaultMap.put(weightData.getParentIndicationId(), weightData);
return defaultMap;
}
}
//cacheData数据结构 父ID-->当前id下子指标的权重信息
//查看当前页面上传的最后一次指标权重信息;用当前页面的数据更新缓存或者增加缓存
if (evaluationTemplate.getCurrentPagePartData() != null) {
int parentId = evaluationTemplate.getCurrentPagePartData().get(0).get(0).getParentId();
cacheData.remove(parentId);
//构建一个简易的只包含实际当前表格中的数据的暂存对象类供后面合并
DraftWeightData draftWeightData = createDefaultDraftWeightData(evaluationTemplate);
cacheData.put(parentId, draftWeightData);
}
return cacheData;
}
private DraftWeightData createDefaultDraftWeightData(EvaluationTemplate evaluationTemplate) {
int pid = evaluationTemplate.getCurrentPagePartData().get(0).get(0).getParentId();
DraftWeightData draftWeightData = new DraftWeightData();
draftWeightData.setTemplateId(evaluationTemplate.getId());
draftWeightData.setWeight(evaluationTemplate.getCurrentPagePartData());
draftWeightData.setParentIndicationId(pid);
draftWeightData.setKey(evaluationTemplate.getDraftKey());
return draftWeightData;
}
/**
* 当一个指标下面只有一个孩子时填充默认权重1.
*
* @param lonelyChild 只有自己没有其他节点和他同级
* @param unitWeightDataMap 当前已经设置的权重信息.
*/
protected void paddingDefaultValueWhenParentHasOnlyLonelyChild(List<Indicator> lonelyChild,
Map<Integer, DraftWeightData> unitWeightDataMap) {
for (Indicator indicator : lonelyChild) {
if (!unitWeightDataMap.containsKey(indicator.getParentId())) {
List<List<MetricMapperWeightBean>> singleRowList = new ArrayList<>();
List<MetricMapperWeightBean> list = new ArrayList<>();
MetricMapperWeightBean weightBean = new MetricMapperWeightBean();
list.add(weightBean);
singleRowList.add(list);
weightBean.setParentId(indicator.getParentId());
weightBean.setValue("1");
weightBean.setRowId(indicator.getId());
weightBean.setColId(indicator.getId());
weightBean.setRowNum(1);
DraftWeightData draftWeightData = new DraftWeightData();
draftWeightData.setParentIndicationId(indicator.getParentId());
draftWeightData.setWeight(singleRowList);
;
unitWeightDataMap.put(indicator.getParentId(), draftWeightData);
}
}
}
}

View File

@ -1,11 +1,12 @@
package com.hshh.evaluation.controller;
import com.hshh.evaluation.entity.EvaluationProject;
import com.hshh.evaluation.entity.EvaluationTemplate;
import com.hshh.evaluation.service.EvaluationProjectService;
import com.hshh.indicator.entity.Indicator;
import com.hshh.indicator.service.IndicatorService;
import com.hshh.evaluation.service.EvaluationTemplateService;
import com.hshh.system.base.entity.TableRelations;
import com.hshh.system.base.service.TableRelationsService;
import com.hshh.system.common.bean.BaseController;
import com.hshh.system.common.bean.JsTree;
import com.hshh.system.common.bean.OperateResult;
import com.hshh.system.common.bean.PaginationBean;
import com.hshh.system.common.enums.ErrorCode;
@ -37,8 +38,16 @@ public class EvaluationProjectController extends BaseController {
@Resource
private EvaluationProjectService evaluationProjectService;
/**
* 模板服务类.
*/
@Resource
private IndicatorService indicatorService;
private EvaluationTemplateService evaluationTemplateService;
/**
* 数据库引用关系记录服务类.
*/
@Resource
private TableRelationsService tableRelationsService;
/**
* 默认页.
@ -53,7 +62,7 @@ public class EvaluationProjectController extends BaseController {
Long total = evaluationProjectService.count(request);
//设置分页信息
setPaginationInfo(request, list, total, model);
return "evaluation/list";
return "project/list";
}
/**
@ -64,9 +73,9 @@ public class EvaluationProjectController extends BaseController {
*/
@GetMapping("/add")
public String add(Model model) {
List<Indicator> rootList = indicatorService.queryRootList();
List<EvaluationTemplate> rootList = evaluationTemplateService.list();
model.addAttribute("rootList", rootList);
return "evaluation/add";
return "project/add";
}
/**
@ -83,58 +92,42 @@ public class EvaluationProjectController extends BaseController {
if (bindingResult.hasErrors()) {
return errorsInputHandle(bindingResult);
}
//查询是否有重名的
List<EvaluationProject> projectList = evaluationProjectService.queryListByName(
project.getProjectName());
if (project.getId() == null) {
if (!projectList.isEmpty()) {
if (!projectList.isEmpty()) {
if (!projectList.get(0).getId().equals(project.getId())) {
return OperateResult.error(null, ErrorMessage.NAME_OR_CODE_EXIT.getMessage(),
ErrorCode.BUSINESS_ERROR.getCode());
}
project.setCreateDate(LocalDateTime.now());
evaluationProjectService.save(project);
} else {
if (!projectList.isEmpty()) {
if (!project.getId().equals(projectList.get(0).getId())) {
return OperateResult.error(null, ErrorMessage.NAME_OR_CODE_EXIT.getMessage(),
ErrorCode.BUSINESS_ERROR.getCode());
}
evaluationProjectService.updateById(project);
}
}
//设置创建时间
if (project.getId() == null) {
project.setCreateDate(LocalDateTime.now());
}
evaluationProjectService.saveWhole(project);
return OperateResult.success();
}
/**
* 开始评估页面.
* 删除工程接口.
*
* @return 开始评估页面
* @param id 要删除的ID
* @return 操作结果
*/
@GetMapping("/startEvaluation/{id}")
public String startEvaluation(@PathVariable("id") Integer projectId, Model model) {
EvaluationProject project = evaluationProjectService.getById(projectId);
List<Indicator> children = indicatorService.queryChildren(project.getIndicatorTopId());
model.addAttribute("children", children);
return "evaluation/start_weight_evaluation";
}
/**
* 获取指标树.
*
* @param projectId 项目ID
* @return 指标树
*/
@GetMapping("/metricTree/{id}")
@ResponseBody
public OperateResult<List<JsTree>> metricTree(@PathVariable("id") Integer projectId) {
EvaluationProject project = evaluationProjectService.getById(projectId);
if (project == null) {
return OperateResult.error(null, ErrorMessage.ID_NOT_EXIT.getMessage(),
@GetMapping("/remove/{id}")
public OperateResult<Void> remove(@PathVariable("id") Integer id) {
List<TableRelations> reList = tableRelationsService.queryRel(id,
"m_data_evaluation_project");
if (!reList.isEmpty()) {
return OperateResult.error(null, ErrorMessage.OBJ_ALREADY_TAKEN.getMessage(),
ErrorCode.BUSINESS_ERROR.getCode());
}
return OperateResult.success(indicatorService.metricTree(project.getIndicatorTopId())
);
evaluationProjectService.removeById(id);
return OperateResult.success();
}
}

View File

@ -1,16 +1,48 @@
package com.hshh.evaluation.controller;
import com.hshh.evaluation.bean.DraftWeightData;
import com.hshh.evaluation.bean.DynamicTable;
import com.hshh.evaluation.bean.MetricComputeRequest;
import com.hshh.evaluation.bean.MetricComputerResponse;
import com.hshh.evaluation.bean.MetricMapperWeightBean;
import com.hshh.evaluation.bean.MetricTableHeaderBean;
import com.hshh.evaluation.entity.EvaluationTemplate;
import com.hshh.evaluation.entity.EvaluationTemplateWeight;
import com.hshh.evaluation.service.EvaluationTemplateService;
import com.hshh.system.common.bean.BaseController;
import com.hshh.evaluation.service.EvaluationTemplateWeightService;
import com.hshh.indicator.entity.Indicator;
import com.hshh.indicator.service.IndicatorService;
import com.hshh.system.base.entity.TableRelations;
import com.hshh.system.base.service.TableRelationsService;
import com.hshh.system.common.Strings.StringOperationUtil;
import com.hshh.system.common.algorithm.AhpNode;
import com.hshh.system.common.algorithm.AhpTreeCompute;
import com.hshh.system.common.bean.JsTree;
import com.hshh.system.common.bean.OperateResult;
import com.hshh.system.common.bean.PaginationBean;
import com.hshh.system.common.enums.ErrorCode;
import com.hshh.system.common.enums.ErrorMessage;
import com.hshh.system.common.util.DraftStore;
import io.swagger.v3.oas.annotations.Operation;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 评估工程模板表 前端控制器.
@ -20,11 +52,33 @@ import org.springframework.web.bind.annotation.RequestMapping;
*/
@Controller
@RequestMapping("/evaluation/evaluationTemplate")
public class EvaluationTemplateController extends BaseController {
@Slf4j
public class EvaluationTemplateController extends AssistantTemplateController {
/**
* 模板服务类.
*/
@Resource
private EvaluationTemplateService evaluationTemplateService;
/**
* 指标服务类.
*/
@Resource
private IndicatorService indicatorService;
/**
* 数据库引用关系记录服务类.
*/
@Resource
private TableRelationsService tableRelationsService;
/**
* 权重服务类.
*/
@Resource
private EvaluationTemplateWeightService evaluationTemplateWeightService;
/**
* 默认页.
*
@ -35,9 +89,393 @@ public class EvaluationTemplateController extends BaseController {
public String list(PaginationBean request, Model model) {
setNavigateTitle(model, "/evaluation/evaluationTemplate/");
List<EvaluationTemplate> list = evaluationTemplateService.list(request);
Long total = evaluationTemplateService.count(request);
//设置分页信息
setPaginationInfo(request, list, total, model);
return "evaluation_template/list";
return "project_template/list";
}
/**
* 导航到增加页面.
*
* @param model session容器
*/
@GetMapping("/add")
public String add(Model model) {
List<Indicator> rootList = indicatorService.queryRootList();
model.addAttribute("rootList", rootList);
return "project_template/add";
}
/**
* 获取指标树.
*
* @param id 项目ID
* @return 指标树
*/
@GetMapping("/metricTree/{id}")
@ResponseBody
public OperateResult<List<JsTree>> metricTree(@PathVariable("id") Integer id, Model model) {
return OperateResult.success(indicatorService.metricTree(id));
}
/**
* 保存模板. 必须设置指标设置完成权重才成成功提交
*
* @param evaluationTemplate 模板数据
* @param bindingResult 验证的错误信息
* @return 操作结果
*/
@ResponseBody
@PostMapping("/save")
public OperateResult<Object> save(@Valid @RequestBody EvaluationTemplate evaluationTemplate,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return errorsInputHandle(bindingResult);
}
//查询是否有重名
List<EvaluationTemplate> list = evaluationTemplateService.queryByName(
evaluationTemplate.getTemplateName());
if (!list.isEmpty()) {
if (!list.get(0).getId().equals(evaluationTemplate.getId())) {
return OperateResult.error(null, ErrorMessage.NAME_OR_CODE_EXIT.getMessage(),
ErrorCode.BUSINESS_ERROR.getCode());
}
}
//设置
if (evaluationTemplate.getId() == null) {
evaluationTemplate.setCreateTime(LocalDateTime.now());
}
//1.合并权重数据
Map<Integer, DraftWeightData> unitWeightDataMap = unitWeightMap(evaluationTemplate);
//2.填充数据;如果一个指标下面只有一个孩子则默认填充为1
List<Indicator> lonelyChild = indicatorService.queryLonelyChild(
evaluationTemplate.getIndicatorTopId());
paddingDefaultValueWhenParentHasOnlyLonelyChild(lonelyChild, unitWeightDataMap);
//3.开始验证完整性/一致性
String validateWholeMessage = validate(evaluationTemplate.getIndicatorTopId(),
unitWeightDataMap);
if (!validateWholeMessage.isEmpty()) {
return OperateResult.error(null, validateWholeMessage, ErrorCode.BUSINESS_ERROR.getCode());
}
//4. 提交
evaluationTemplateService.saveWhole(evaluationTemplate, unitWeightDataMap);
return OperateResult.success();
}
/**
* 根据id删除指标.
*
* @param id 要删除的ID
* @return 操作结果
*/
@GetMapping("/remove/{id}")
@ResponseBody
public OperateResult<Object> remove(@PathVariable("id") Integer id) {
List<TableRelations> reledList = tableRelationsService.queryRel(id,
"m_data_evaluation_template");
if (!reledList.isEmpty()) {
return OperateResult.error(null, ErrorMessage.OBJ_ALREADY_TAKEN.getMessage(),
ErrorCode.BUSINESS_ERROR.getCode());
}
evaluationTemplateService.deleteTemplate(evaluationTemplateService.getById(id));
return OperateResult.success();
}
/**
* 获取模板基础信息.
*
* @param id 模板ID
* @return 模板数据
*/
@GetMapping("/{id}")
@ResponseBody
public OperateResult<EvaluationTemplate> view(@PathVariable("id") Integer id) {
EvaluationTemplate template = evaluationTemplateService.getById(id);
if (template == null) {
return OperateResult.error(null, ErrorMessage.ID_NOT_EXIT.getMessage(),
ErrorCode.BUSINESS_ERROR.getCode());
}
return OperateResult.success(template);
}
/**
* 暂存权重数据.
*
* @param data 权重数据 页面传入的权重map数据key为rowId_colId(from指标id_to指标ID
* value为{rowId:a,colId:b,value:0.1}类似
* @return 操作结果
*/
@PostMapping("/receiveDraft")
@ResponseBody
@SuppressWarnings("unchecked")
public synchronized OperateResult<Void> receiveDraft(@RequestBody DraftWeightData data) {
log.info("receive draft data: pId= {}", data.getParentIndicationId());
//根据key获取已经缓存的信息
Map<Integer, DraftWeightData> dataMap = DraftStore.get(data.getKey(), Map.class);
//如果缓存不存在
if (dataMap == null) {
dataMap = new HashMap<>();
}
//key为上级指标
dataMap.put(data.getParentIndicationId(), data);
//更新缓存
DraftStore.put(data.getKey(), dataMap);
return OperateResult.success();
}
/**
* 获取暂存数据.
*
* @param requestData 请求数据.
* @return 表格和数据
*/
@SuppressWarnings("unchecked")
@PostMapping("/getDraftData")
public synchronized String getDraftData(@RequestBody DraftWeightData requestData, Model model) {
//查看key 是否有缓存记录
Map<Integer, Object> cacheData = DraftStore.get(requestData.getKey(), Map.class);
//如果没有记录
if (cacheData == null) { //从数据库中获取
model.addAttribute("data", createNewDynamicFromDatabase(requestData));
return "project_template/table";
}
//如果有记录查看对应的指标是否有记录
Object cacheWeightObj = cacheData.get(requestData.getParentIndicationId());
//如果对应指标没有记录
if (cacheWeightObj == null) { //从数据库读取
model.addAttribute("data", createNewDynamicFromDatabase(requestData));
return "project_template/table";
}
DraftWeightData data = (DraftWeightData) cacheWeightObj;
model.addAttribute("data", createDynamicFromCache(data));
return "project_template/table";
}
/**
* 从数据库创建动态表.
*
* @param requestData 用户请求数据
* @return 动态表
*/
private DynamicTable createNewDynamicFromDatabase(DraftWeightData requestData) {
DynamicTable dynamicTable = new DynamicTable();
//查询当前指标的子指标
List<Indicator> children = indicatorService.queryChildren(requestData.getParentIndicationId());
//获取指标权重信息
List<EvaluationTemplateWeight> weightInDbList = evaluationTemplateWeightService.queryListByIndicatorParentIdAndTemplateId(
requestData.getParentIndicationId(), requestData.getTemplateId());
//转化为key=fromId+"_"+toId,value=self
Map<String, EvaluationTemplateWeight> weightInMap = weightInDbList.stream()
.collect(
Collectors.toMap(a -> a.getFromIndicatorId() + "_" + a.getToIndicatorId(), a -> a));
//设置头信息
dynamicTable.setHeaderMap(createTableHeaderMap(children));
//设置权重信息
List<List<MetricMapperWeightBean>> weight = new ArrayList<>();
for (int i = 0; i < children.size(); i++) {
List<MetricMapperWeightBean> innerList = createEmptyMapperList(requestData, children, i,
weightInMap);
weight.add(innerList);
}
dynamicTable.setWeight(weight);
return dynamicTable;
}
/**
* 创建表格头.
*
* @param children 子指标
* @return 表格头信息
*/
private Map<Integer, MetricTableHeaderBean> createTableHeaderMap(List<Indicator> children) {
List<MetricTableHeaderBean> headerBeans = new ArrayList<>();
children.forEach(a -> {
MetricTableHeaderBean bean = new MetricTableHeaderBean();
bean.setId(a.getId());
bean.setName(a.getName());
headerBeans.add(bean);
});
Map<Integer, MetricTableHeaderBean> headerMap = new LinkedHashMap<>();
headerBeans.forEach(header -> {
headerMap.put(header.getId(), header);
});
return headerMap;
}
/**
* 从数据库创建一个空的默认映射列表.
*
* @param requestData 请求数据
* @param children 子指标
* @param i 行号
* @return 默认映射列表
*/
private static List<MetricMapperWeightBean> createEmptyMapperList(DraftWeightData requestData,
List<Indicator> children, int i, Map<String, EvaluationTemplateWeight> weightInMap) {
List<MetricMapperWeightBean> innerList = new ArrayList<>();
for (Indicator child : children) {
MetricMapperWeightBean weightBean = new MetricMapperWeightBean();
//行ID
weightBean.setRowId(children.get(i).getId());
//列Id
weightBean.setColId(child.getId());
EvaluationTemplateWeight evaluationTemplateWeight = weightInMap.get(
weightBean.getRowId() + "_" + weightBean.getColId());
//初始默认为1
weightBean.setValue(
evaluationTemplateWeight == null ? "1" : evaluationTemplateWeight.getWeight() + "");
weightBean.setRowNum(evaluationTemplateWeight == null ? (i + 1)
: evaluationTemplateWeight.getRowNum());
weightBean.setParentId(requestData.getParentIndicationId());
innerList.add(weightBean);
}
return innerList;
}
/**
* 从缓存中创建动态表.
*
* @param data 缓存数据
* @return 动态表
*/
protected DynamicTable createDynamicFromCache(DraftWeightData data) {
DynamicTable dynamicTable = new DynamicTable();
dynamicTable.setHeaderMap(data.getHeaderMap());
dynamicTable.setWeight(data.getWeight());
return dynamicTable;
}
/**
* 计算指标当设置变化时触发.
*
* @param requests 计算请求包含 指标id,当前表格数据
* @return 计算结果
*/
@PostMapping("/computer")
@ResponseBody
public synchronized OperateResult<List<MetricComputerResponse>> computerWeight(
@RequestBody MetricComputeRequest requests) {
//如果没有值直接静默返回
if (requests.getWeightList() == null || requests.getWeightList().isEmpty()) {
return OperateResult.success();
}
final List<MetricComputerResponse> responseList = new ArrayList<>();
List<String> metricList = requests.getMetric();
final AhpNode H = new AhpNode("H");
metricList.forEach(metric -> {
AhpNode m = new AhpNode(metric);
H.add(m);
});
List<List<MetricMapperWeightBean>> weightList = requests.getWeightList();
double[][] data = StringOperationUtil.toDoubleArray(
convertMetricMapperWeightBeanListToStrArray(weightList));
H.setLocalMatrix(data);
H.globalWeight = 1; // 根节点全局权重=1
AhpTreeCompute.computeTree(H);
H.children.forEach(child -> {
MetricComputerResponse response = new MetricComputerResponse();
response.setId(child.name);
response.setWeight(String.format("%.2f", child.globalWeight));
responseList.add(response);
});
return OperateResult.success(responseList);
}
/**
* 验证指标权重完整性/一致性.
*
* @param indicatorTopId 指标顶级ID
* @param weightByParentIdMap 已经设置的权重Map
* @return 验证提示结果
*/
private String validate(Integer indicatorTopId,
Map<Integer, DraftWeightData> weightByParentIdMap) {
//所有topId=indicatorTopId列表
List<Indicator> list = indicatorService.queryByTopId(indicatorTopId);
// key=指标ID value=指标数据
Map<Integer, Indicator> indicatorMap = list.stream()
.collect(Collectors.toMap(Indicator::getId, a -> a));
Map<Integer, List<Indicator>> parentMap = list.stream().filter(a -> a.getParentId() != null)
.collect(Collectors.groupingBy(Indicator::getParentId));
StringBuffer sb = new StringBuffer();
parentMap.forEach((indicatorId, children) -> {
//完整性
if (!weightByParentIdMap.containsKey(indicatorId)) {
String indicatorName =
indicatorMap.get(indicatorId) == null ? "" : indicatorMap.get(indicatorId).getName();
sb.append(indicatorName).append("没有设置权重<br>\n");
}
if (weightByParentIdMap.get(indicatorId) != null) {
//一致性
List<List<MetricMapperWeightBean>> childWeightList = weightByParentIdMap.get(indicatorId)
.getWeight();
consistencyValidate(sb, childWeightList, indicatorMap);
}
});
return sb.toString();
}
/**
* 验证一致性. 假设有三个指标 AB,C A--AA AB,AC |B--BB BA BC| C--CC CA CB.
*
* @param sb 提示消息
* @param childWeightList 要验证的数据
* @param indicatorMap 指标map key=id value=指标
*/
private void consistencyValidate(StringBuffer sb,
List<List<MetricMapperWeightBean>> childWeightList, Map<Integer, Indicator> indicatorMap) {
if (childWeightList != null && childWeightList.size() > 1) {
//获取第一行记录作为标准
List<MetricMapperWeightBean> firstRow = childWeightList.get(0);
//获取A指标ID
Integer firstFormIndicatorId = firstRow.get(0).getRowId();
//把第一行的数据用rowId+"_+colId作为键value为值
Map<String, Double> firstRowMap = new HashMap<>();
firstRow.forEach(child -> {
firstRowMap.put(child.getRowId() + "_" + child.getColId(),
Double.parseDouble(child.getValue()));
});
for (int i = 1; i < childWeightList.size(); i++) {
List<MetricMapperWeightBean> weightBeanList = childWeightList.get(i);
for (int j = 1; j < weightBeanList.size(); j++) {
//获取第一行她的from指标值 AB
Double abValue = firstRowMap.get(
firstFormIndicatorId + "_" + weightBeanList.get(j).getRowId());
//获取AC
Double acValue = firstRowMap.get(
firstFormIndicatorId + "_" + weightBeanList.get(j).getColId());
//获取BC
double bcValue = Double.parseDouble(weightBeanList.get(j).getValue());
if (bcValue > 1 && acValue.compareTo(abValue) > 0) {
sb.append(indicatorMap.get(weightBeanList.get(j).getRowId())).append("必须小于1")
.append("<br>\n");
}
if (bcValue < 1 && acValue.compareTo(abValue) < 0) {
sb.append(indicatorMap.get(weightBeanList.get(j).getRowId())).append("必须大于1")
.append("<br>\n");
}
}
}
}
}
}

View File

@ -35,10 +35,10 @@ public class EvaluationProject extends BaseBean {
private String projectMemo;
private Integer indicatorTopId;
private Integer templateId;
@TableField(exist = false)
private String indicatorTopName;
private String templateName;
private LocalDateTime createDate;
}

View File

@ -4,8 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.hshh.evaluation.bean.MetricMapperWeightBean;
import com.hshh.system.common.bean.BaseBean;
import java.time.LocalDateTime;
import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
/**
@ -22,19 +27,19 @@ public class EvaluationTemplate extends BaseBean {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@NotBlank(message = "模板名称不能为空")
@Size(max = 50, message = "不能超过50字符")
private String templateName;
//顶级指标ID
@NotNull(message = "必须选择指标")
private Integer indicatorTopId;
private Integer indicatorWeightId;
//模板状态
private Integer templateStatus;
@Size(max = 255, message = "不能超过255字符")
private String templateMemo;
private LocalDateTime createTime;
@ -42,4 +47,12 @@ public class EvaluationTemplate extends BaseBean {
private String indicatorTopName;
@TableField(exist = false)
private String templateStatusName;
//提交时当前页面对应的指标id,其余指标从缓存取
@TableField(exist = false)
private List<List<MetricMapperWeightBean>> currentPagePartData;
//暂存指标权重的key
@TableField(exist = false)
private String draftKey;
}

View File

@ -36,4 +36,11 @@ public interface EvaluationProjectService extends IService<EvaluationProject> {
* @return 查询结果
*/
List<EvaluationProject> queryListByName(String name);
/**
* 保存工程信息.
*
* @param evaluationProject 工程数据
*/
void saveWhole(EvaluationProject evaluationProject);
}

View File

@ -1,9 +1,12 @@
package com.hshh.evaluation.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hshh.evaluation.bean.DraftWeightData;
import com.hshh.evaluation.bean.MetricMapperWeightBean;
import com.hshh.evaluation.entity.EvaluationTemplate;
import com.hshh.system.common.bean.PaginationBean;
import java.util.List;
import java.util.Map;
/**
* 评估工程模板表 服务类.
@ -28,4 +31,22 @@ public interface EvaluationTemplateService extends IService<EvaluationTemplate>
* @return 总数
*/
Long count(PaginationBean search);
/**
* 保存评估模板数据.
*
* @param evaluationTemplate 评估模板
* @param weightData 权重
*/
void saveWhole(EvaluationTemplate evaluationTemplate,
Map<Integer, DraftWeightData> weightData);
/**
* 根据名称查询列表.
*
* @param name 名称
* @return 模板列表
*/
List<EvaluationTemplate> queryByName(String name);
void deleteTemplate(EvaluationTemplate evaluationTemplate);
}

View File

@ -5,9 +5,13 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hshh.evaluation.entity.EvaluationProject;
import com.hshh.evaluation.mapper.EvaluationProjectMapper;
import com.hshh.evaluation.service.EvaluationProjectService;
import com.hshh.system.base.service.TableRelationsService;
import com.hshh.system.common.bean.PaginationBean;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 服务实现类.
@ -20,6 +24,12 @@ public class EvaluationProjectServiceImpl extends
ServiceImpl<EvaluationProjectMapper, EvaluationProject> implements
EvaluationProjectService {
/**
* 数据库引用关系记录服务类.
*/
@Resource
private TableRelationsService tableRelationsService;
@Override
public List<EvaluationProject> list(PaginationBean paginationBean) {
return this.baseMapper.list(paginationBean);
@ -37,4 +47,15 @@ public class EvaluationProjectServiceImpl extends
return this.list(queryWrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveWhole(EvaluationProject evaluationProject) {
this.saveOrUpdate(evaluationProject);
tableRelationsService.removeRel(evaluationProject.getId(), "m_data_evaluation_project",
"m_data_evaluation_template");
List<Integer> relList = new ArrayList<>();
relList.add(evaluationProject.getTemplateId());
tableRelationsService.addRel(evaluationProject.getId(), "m_data_evaluation_project", relList,
"m_data_evaluation_template");
}
}

View File

@ -1,12 +1,19 @@
package com.hshh.evaluation.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hshh.evaluation.bean.DraftWeightData;
import com.hshh.evaluation.entity.EvaluationTemplate;
import com.hshh.evaluation.entity.EvaluationTemplateWeight;
import com.hshh.evaluation.mapper.EvaluationTemplateMapper;
import com.hshh.evaluation.service.EvaluationTemplateService;
import com.hshh.evaluation.service.EvaluationTemplateWeightService;
import com.hshh.system.common.bean.PaginationBean;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 评估工程模板表 服务实现类.
@ -16,8 +23,10 @@ import org.springframework.stereotype.Service;
*/
@Service
public class EvaluationTemplateServiceImpl extends
ServiceImpl<EvaluationTemplateMapper, EvaluationTemplate> implements
EvaluationTemplateService {
ServiceImpl<EvaluationTemplateMapper, EvaluationTemplate> implements EvaluationTemplateService {
@Resource
private EvaluationTemplateWeightService evaluationTemplateWeightService;
@Override
public List<EvaluationTemplate> list(PaginationBean search) {
@ -28,4 +37,59 @@ public class EvaluationTemplateServiceImpl extends
public Long count(PaginationBean search) {
return this.baseMapper.count(search);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void saveWhole(EvaluationTemplate evaluationTemplate,
Map<Integer, DraftWeightData> weightDataMap) {
saveOrUpdate(evaluationTemplate);
if (weightDataMap != null && !weightDataMap.isEmpty()) {
weightDataMap.forEach((k, v) -> {
//先按模板ID+上级指标ID
evaluationTemplateWeightService.deleteEvaluationTemplateWeightWithTemplateIdAndIndicatorId(
evaluationTemplate.getId(), evaluationTemplate.getIndicatorTopId(), k);
v.getWeight().forEach(weightBeanList -> {
weightBeanList.forEach(weightBean -> {
EvaluationTemplateWeight evaluationTemplateWeight = new EvaluationTemplateWeight();
//设置模板ID
evaluationTemplateWeight.setTemplateId(evaluationTemplate.getId());
//设置权重
evaluationTemplateWeight.setWeight(Double.valueOf(weightBean.getValue()));
//设置顶级指标ID
evaluationTemplateWeight.setIndicatorTopId(evaluationTemplate.getIndicatorTopId());
//fromIndicatorId
evaluationTemplateWeight.setFromIndicatorId(weightBean.getRowId());
//toIndicatorId
evaluationTemplateWeight.setToIndicatorId(weightBean.getColId());
//设置上级指标ID
evaluationTemplateWeight.setIndicatorParentId(weightBean.getParentId());
//设置行号
evaluationTemplateWeight.setRowNum(weightBean.getRowNum());
//保存
evaluationTemplateWeightService.save(evaluationTemplateWeight);
});
});
});
}
}
@Override
public List<EvaluationTemplate> queryByName(String name) {
QueryWrapper<EvaluationTemplate> wrapper = new QueryWrapper<>();
wrapper.eq("template_name", name);
return this.list(wrapper);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void deleteTemplate(EvaluationTemplate evaluationTemplate) {
evaluationTemplateWeightService.deleteEvaluationTemplateWeightWithTemplateId(
evaluationTemplate.getId());
this.removeById(evaluationTemplate.getId());
}
}

View File

@ -50,7 +50,7 @@ public class EvaluationController extends BaseController {
*/
@GetMapping("/evaluationList")
public String evaluationList(Integer topIndicatorId, Integer indicatorId, Model model) {
setNavigateTitle(model, "/indicator/evaluationList");
setNavigateTitle(model, "/evaluation/evaluationList");
List<Indicator> rootList = indicatorService.queryRootList();
if (rootList != null && !rootList.isEmpty()) {
if (topIndicatorId != null) {

View File

@ -83,4 +83,20 @@ public interface IndicatorService extends IService<Indicator> {
* @return 树数据
*/
List<JsTree> metricTree(Integer topId);
/**
* 查询只有一个孩子的指标列表.
*
* @param topId 指标ID
* @return 指标列表
*/
List<Indicator> queryLonelyChild(Integer topId);
/**
* 按照顶级指标查询所有指标.
*
* @param topId 顶级ID
* @return 指标列表topId一致
*/
List<Indicator> queryByTopId(Integer topId);
}

View File

@ -165,4 +165,26 @@ public class IndicatorServiceImpl extends ServiceImpl<IndicatorMapper, Indicator
return JsTree.getJsTree(rootList);
}
@Override
public List<Indicator> queryLonelyChild(Integer topId) {
Map<Integer, List<Indicator>> groupByPidMap = this.list()
.stream().filter(a -> a.getParentId() != null)
.collect(Collectors.groupingBy(Indicator::getParentId));
List<Indicator> list = new ArrayList<>();
groupByPidMap.forEach((k, v) -> {
if (v.size() == 1) {
list.add(v.get(0));
}
});
return list;
}
@Override
public List<Indicator> queryByTopId(Integer topId) {
QueryWrapper<Indicator> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("top_id", topId);
return this.list(queryWrapper);
}
}

View File

@ -27,7 +27,7 @@ mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
database-id: mysql
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:

View File

@ -7,7 +7,7 @@
@rownum := @rownum + 1 AS seq,
t.*
FROM (
SELECT T1.*,T2.name as indicatorTopName FROM m_data_evaluation_project T1 left join m_data_indicator T2 on T1.indicator_top_id=T2.id
SELECT T1.*,T2.template_name as templateName FROM m_data_evaluation_project T1 left join m_data_evaluation_template T2 on T1.template_id=T2.id
<where>
<if test="search != null and search !='' ">
T1.project_name LIKE CONCAT('%',#{search},'%')
@ -24,8 +24,8 @@
SELECT
ROW_NUMBER() OVER (ORDER BY id ASC) AS seq,
a.*,
a1.name as indicatorTopName
FROM m_data_evaluation_project a left join m_data_indicator a1 on a.indicator_top_id=a1.name
a1.template_name as templateName
FROM m_data_evaluation_project a left join m_data_evaluation_template a1 on a.template_id=a1.name
<where>
<if test="search != null and search !='' ">
a.project_name LIKE '%'||#{search}||'%'

View File

@ -13,7 +13,7 @@
T1.template_name LIKE CONCAT('%',#{search},'%')
</if>
</where>
order by T1.id asc ) t, ( SELECT @rownum := #{start} ) r limit
order by T1.id desc ) t, ( SELECT @rownum := #{start} ) r limit
#{start},#{pageSize}
</select>
<select id="list" resultType="com.hshh.evaluation.entity.EvaluationTemplate"
@ -22,7 +22,7 @@
t.*
FROM (
SELECT
ROW_NUMBER() OVER (ORDER BY id ASC) AS seq,
ROW_NUMBER() OVER (ORDER BY id desc) AS seq,
a.*,
a1.name as indicatorTopName
FROM m_data_evaluation_template a left join m_data_indicator a1 on a.indicator_top_id=a1.name

View File

@ -47,6 +47,7 @@ class HttpClient {
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
let response = xhr.responseText;
let contentType = xhr.getResponseHeader('Content-Type');
if (xhr.status >= 200 && xhr.status < 300) {
@ -54,24 +55,24 @@ class HttpClient {
if (contentType && contentType.indexOf('application/json') !== -1) {
try {
response = JSON.parse(response);
if (response.code === 0) {
callback(null, response, xhr);
} else {
if (formId) {
removeValidCss(formId);
}
//字段验证提示错误
if (formId ) {
errorsHandler(response, formId, dialogId);
}
}
} catch (e) {
// JSON 解析失败,保持原始文本
}
}
if (response.code === 0) {
if (contentType && contentType.indexOf('text/html') !== -1) {
callback(null, response, xhr);
} else {
if (formId) {
removeValidCss(formId);
}
//字段验证提示错误
if (formId && dialogId) {
errorsHandler(response, formId, dialogId);
}
}
} else {
callback(new Error(`HTTP POST Error: ${xhr.status}`), null, xhr);
}
@ -79,6 +80,7 @@ class HttpClient {
};
xhr.send(JSON.stringify(data));
}
/**
* 提交 new formData数据
* @param url
@ -86,6 +88,7 @@ class HttpClient {
* @param callback
* @param formId
* @param dialogId
* @param offFlag
*/
postFormData(url, data, callback, formId, dialogId) {
$.ajax({
@ -107,7 +110,7 @@ class HttpClient {
if (res.code === 10001) {
errorsHandler(res, formId, dialogId);
}
if(res.code===0){
if (res.code === 0) {
callback(null, res, null);
}
},
@ -119,6 +122,7 @@ class HttpClient {
function errorsHandler(response, formId, dialogId) {
if (response.code === -1) {//输入字段提示
let errors = response.errors;
let formElement = document.getElementById(formId);
@ -135,7 +139,7 @@ function errorsHandler(response, formId, dialogId) {
}
//业务错误
if (response.code === 10001) {
if (dialogId) {
if (dialogId ) {
closeDialog(dialogId);
}

View File

@ -3,9 +3,7 @@
* @param formId 所属formId
*/
function removeValidCss(formId) {
if (document.getElementById("error_message")) {
document.getElementById("error_message").style.display = "none";
}
let form = document.getElementById(formId);
let elements = form.elements;
@ -354,5 +352,26 @@ function hideContextMenu() {
}
}
function generateDraftKey() {
// 优先用 crypto.randomUUID 生成
if (window.crypto && window.crypto.randomUUID) {
return 'tpl:new:' + window.crypto.randomUUID();
}
// 兼容老浏览器
return 'tpl:new:' + Date.now().toString(36) + Math.random().toString(36).substring(2, 10);
}
function mapToObject(map) {
const obj = {};
for (const [k, v] of map) {
obj[k] = v;
}
return obj;
}
function showFieldErrorTip(formId,fieldId,message){
let formElement = document.getElementById(formId);
formElement.elements[fieldId].classList.add(
"is-invalid");
document.getElementById(
fieldId + "_error_tip").innerHTML =message;
}

View File

@ -1,81 +0,0 @@
<div class="row g-4" data-page="start_evaluation">
<div class="card-body">
<ul class="steps steps-green my-4">
<li class="step-item active">设置/加载 权重</li>
<li class="step-item ">选择/上传 数据集</li>
<li class="step-item">执行评估</li>
<li class="step-item">本次结果</li>
</ul>
</div>
</div>
<div class="row g-4" id="evaluation-start-page">
<div class="col-4" id="project_metric_tree" style="overflow-y: scroll; min-height: 600px;">
</div>
<div class="col-8">
<div class="card" style="overflow-y: scroll; min-height: 600px;">
<div class="card-header">
<div class="card-title">权重设置区</div>
<div class="card-actions">
<a href="#" class="btn btn-primary">
<!-- Download SVG icon from http://tabler-icons.io/i/plus -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 5l0 14"></path>
<path d="M5 12l14 0"></path>
</svg>
保存
</a>
</div>
</div>
<div class="card-body">
<div class="map-scroller">
<table class="table table-vcenter table-striped table-hover align-middle map-table">
<thead>
<tr>
<th></th>
<th th:each="item:${children}" th:text="${item.name}" style="font-weight: bold;"></th>
</tr>
</thead>
<tbody class="autonum">
<tr th:each="item:${children}">
<td th:text="${item.name}" style="font-weight: bold;"></td>
<td th:each="innerItem:${children}" >
<select class="form-select" >
<option value="9" >极端重要(9)</option>
<option value="8">更强烈重要(8)</option>
<option value="7">强烈重要(7)</option>
<option value="6">十分重要(6)</option>
<option value="5">明显重要(5)</option>
<option value="4">更为重要(4)</option>
<option value="3">稍微重要(3)</option>
<option value="2">微小重要(2)</option>
<option value="1" th:selected="${item.id==innerItem.id}">同样重要(1)</option>
<option value="0.5">微小次要(0.5)</option>
<option value="0.3333">稍微次要(0.3333)</option>
<option value="0.25">更为次要(0.25)</option>
<option value="0.2">明显次要(0.2)</option>
<option value="0.16667">十分次要(0.16667)</option>
<option value="0.14286">强烈次要(0.14286)</option>
<option value="0.125">更强烈次要(0.125)</option>
<option value="0.11111">极端次要(0.11111)</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,87 +0,0 @@
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<!-- 面包屑导航 -->
<div th:replace="fragments/dialog::navigateDialog(${chainMenuList})"></div>
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">评估模板列表</h3>
<div class="card-actions">
<a href="javascript:void(0)" class="btn btn-primary" onclick="_toAdd()">
<!-- Download SVG icon from http://tabler-icons.io/i/plus -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 5l0 14"></path>
<path d="M5 12l14 0"></path>
</svg>
新增
</a>
</div>
</div>
<div th:replace="fragments/dialog::searchConditionDialog(${condition})"></div>
<div class="table-responsive">
<table class="table card-table table-vcenter text-nowrap datatable">
<thead>
<tr>
<th class="w-1">No.
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-up -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-sm icon-thick" width="24"
height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 15l6 -6l6 6"></path>
</svg>
</th>
<th>模板名称</th>
<th>对应指标</th>
<th>模板状态</th>
<th>模板描述</th>
<th>创建时间</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:if="${result.list!=null}" th:each="project : ${result.list}">
<td th:text="${project.seq}"></td>
<td th:text="${project.templateName}"></td>
<td th:text="${project.indicatorTopName}"></td>
<td th:text="${project.templateStatusName}"></td>
<td th:text="${project.templateMemo}"></td>
<td th:text="${#temporals.format(project.createDate, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
<div class="btn-list flex-nowrap">
<a href="javascript:void(0)" class="btn btn-primary"
th:onclick="|_startEvaluation('${project.id}')|">
编辑
</a>
<a href="javascript:void(0)" class=" btn btn-danger"
th:onclick="|_projectDelete('${project.id}')|">
删除
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/dialog::paginationDialog(${result})"></div>
</div>
</div>
</div>
</div>
</div>
<div th:replace="fragments/dialog::confirmationDialog"></div>
<script>
</script>

View File

@ -24,163 +24,203 @@
<style>
@keyframes notif-in {
from {
opacity: 0;
transform: translateY(-16px);
}
to {
opacity: 1;
transform: translateY(0);
}
/* =========================
基础变量 & 全局
========================= */
:root{
--accent: #206bc4; /* 主题主色 */
--border: #e0e6ed;
--muted: #f6f8fb;
--card-bg: #fff;
--head-bg: #f3f6fa;
--sticky-bg:#ffffff; /* 粘列背景,防止覆盖时变暗 */
--col-index-w: 48px; /* 表格首列粘列1宽度 */
}
@keyframes pop-in {
0% {
transform: scale(0.8);
opacity: 0.4;
}
100% {
transform: scale(1);
opacity: 1;
}
body, .page-body{
background: #f5f7fb;
}
#contextMenu ul {
list-style: none;
margin: 0;
padding: 0;
/* =========================
卡片hero / section
========================= */
.card.card-hero{
border: 1px solid var(--border);
border-radius: 14px;
background: var(--card-bg);
box-shadow: 0 14px 36px rgba(0,0,0,.08);
}
.card.card-hero > .card-header{
background: linear-gradient(90deg, rgba(32,107,196,.10), rgba(32,107,196,0));
border-bottom: 1px solid rgba(32,107,196,.25);
border-top-left-radius: 14px;
border-top-right-radius: 14px;
}
.card.card-hero .card-title{
font-weight: 800;
border-left: 6px solid var(--accent);
padding-left: .5rem;
}
#contextMenu li {
.card.section{
border: 1px solid var(--border);
border-radius: 12px;
background: var(--card-bg);
box-shadow: 0 6px 18px rgba(0,0,0,.06);
margin-bottom: 1rem;
}
.card.section .card-header{
background: var(--head-bg);
border-bottom: 1px solid #e9edf3;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.card.section .card-title{
font-weight: 700;
}
/* =========================
表单(输入/下拉/勾选)
========================= */
.form-select:focus, .form-control:focus{
border-color: var(--accent);
box-shadow: 0 0 0 .2rem rgba(32,107,196,.15);
}
.form-check-input[type="radio"],
.form-check-input[type="checkbox"]{
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent) inset;
background-color: #fff;
transition: all .15s ease-in-out;
}
.form-check-input[type="radio"]:hover,
.form-check-input[type="checkbox"]:hover{
background-color: rgba(32,107,196,.10);
box-shadow: 0 0 0 2px rgba(32,107,196,.25);
}
.form-check-input:checked{
background-color: var(--accent);
border-color: var(--accent);
box-shadow: none;
}
/* =========================
CSV 列表
========================= */
.csv-heads .list-group-item{
background: #fff;
border: 1px solid #eef2f7;
margin-bottom: .25rem;
border-radius: 8px;
padding: .5rem .75rem;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
}
/* =========================
AHP 表:容器与表格
========================= */
.map-scroller{
overflow: auto;
border: 1px solid #e9edf3;
border-radius: 10px;
box-shadow: inset 0 1px 0 rgba(0,0,0,.02);
background: #fff;
}
.map-table{
min-width: 980px; /* 横向滚动更友好 */
border-collapse: separate;
border-spacing: 0;
/* 确保粗体切片可用(有些字体 700 看不粗) */
font-family: system-ui, -apple-system, "Segoe UI", Arial, "Microsoft YaHei", sans-serif;
}
/* ---- 表头吸顶 ---- */
.map-table thead th.sticky-header{
position: sticky; top: 0; z-index: 4;
background: var(--head-bg);
border-bottom: 1px solid #dee2e6;
}
/* ---- 粘左两列(表头 + 表体) ---- */
.map-table thead th.sticky-col-1,
.map-table tbody td.sticky-body-col-1{
position: sticky; left: 0; z-index: 3;
background: var(--sticky-bg);
box-shadow: 1px 0 0 #e9edf3;
}
.map-table thead th.sticky-col-2,
.map-table tbody td.sticky-body-col-2{
position: sticky; left: var(--col-index-w); z-index: 3; /* 用变量替代 48px */
background: var(--sticky-bg);
box-shadow: 1px 0 0 #e9edf3;
}
/* 粘列在表头处层级更高,避免被后列覆盖 */
.map-table thead th.sticky-col-1,
.map-table thead th.sticky-col-2{
z-index: 5;
}
/* ---- 条纹 & 悬停 ---- */
.map-table tbody tr:nth-child(odd){ background: #fcfdfe; }
.map-table tbody tr:hover{ background: #f3f8ff; }
/* ---- 紧凑列 ---- */
.map-table th.w-compact,
.map-table td.w-compact{
width: 1%; white-space: nowrap;
}
/* ---- 加粗规则(表头 + 第一列标签)---- */
/* Tabler/Bootstrap 有时会把表头设为 400这里强制 700 */
.map-table thead th{
font-weight: 700 !important;
font-size: 16px; /* 可按需调整 */
color: inherit !important; /* 继承父级颜色 */
}
.map-table tbody td:first-child{
font-weight: 700 !important;
font-size: 16px; /* 可选,同步视觉 */
}
/* =========================
动画/菜单/通用
========================= */
@keyframes flashHighlight{
0%{ box-shadow:0 0 0 0 rgba(32,107,196,0); }
20%{ box-shadow:0 0 0 8px rgba(32,107,196,.22); }
100%{ box-shadow:0 14px 36px rgba(0,0,0,.08); }
}
.flash{ animation: flashHighlight .9s ease-out; }
@keyframes pop-in{
0%{ transform: scale(0.8); opacity: .4; }
100%{ transform: scale(1); opacity: 1; }
}
#contextMenu ul{ list-style:none; margin:0; padding:0; }
#contextMenu li{
padding: 9px 18px;
cursor: pointer;
border-bottom: 1px solid #eee;
transition: background 0.18s, color 0.18s;
display: flex;
align-items: center;
transition: background .18s, color .18s;
display: flex; align-items: center;
font-size: 15px;
}
#contextMenu li:last-child{ border-bottom: none; }
#contextMenu li:hover{ background: #ffcd38; color: #fff; }
#contextMenu li i{ margin-right: 8px; font-size: 15px; opacity: .85; }
#contextMenu li:last-child {
border-bottom: none;
}
#contextMenu li:hover {
background: #ffcd38;
color: #fff;
}
#contextMenu li i {
margin-right: 8px;
font-size: 15px;
opacity: 0.85;
}
.dropdown-item.active {
background-color: #206bc4 !important; /* Tabler 蓝色 */
.dropdown-item.active{
background-color: var(--accent) !important;
color: #fff !important;
}
.alert{ margin-bottom: 1rem; }
.alert {
margin-bottom: 1rem;
}
:root {
--col-index-w: 48px; /* 首列宽度(与 th/td 一致) */
}
/* CSV 预览区域 */
.csv-heads {
max-height: 300px;
overflow: auto;
}
/* 滚动容器 */
.map-scroller {
max-height: 65vh;
overflow: auto;
border-radius: .5rem;
background: #fff;
}
/* 表格与单元格 */
.map-table {
table-layout: fixed;
width: max-content;
min-width: 1200px;
border-collapse: separate;
border-spacing: 0;
}
.map-table th, .map-table td {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-top: .55rem;
padding-bottom: .55rem;
}
/* ===== 表头:粘顶 + 粘左(前两列) —— 层级最高 ===== */
.sticky-header {
position: sticky;
top: 0;
z-index: 10;
background: #fff;
border-bottom: 1px solid #e9ecef;
box-shadow: 0 2px 0 rgba(0, 0, 0, .04);
font-weight: bold;
}
.sticky-header.sticky-col-1 {
left: 0;
z-index: 11;
}
.sticky-header.sticky-col-2 {
left: var(--col-index-w);
z-index: 11;
}
/* ===== 表体:粘左(前两列) —— 层级低于表头 ===== */
.sticky-body-col-1 {
position: sticky;
left: 0;
z-index: 4;
background: #fff;
}
.sticky-body-col-2 {
position: sticky;
left: var(--col-index-w);
z-index: 4;
background: #fff;
box-shadow: 2px 0 0 rgba(0, 0, 0, .06); /* 分隔线效果 */
}
/* 自动编号tbody 开始计数tr 自增;序号/名称自动输出数值 */
.autonum {
counter-reset: row;
}
.autonum tr {
counter-increment: row;
}
.autonum .col-index::before {
content: counter(row);
}
.autonum .col-name::after {
}
.map-table th {
font-weight: bold !important;
}
</style>
</head>
<body>
@ -228,6 +268,7 @@
<script th:src="@{/js/HttpClient.js}"></script>
<script th:src="@{/js/d3.v7.min.js}"></script>
<script>
let d3TreeData = null;
document.addEventListener('DOMContentLoaded', function () {
// 1. 支持多种菜单项,逗号分隔多个选择器
@ -296,6 +337,11 @@
}
}
});
const RECIP = {
"9":"0.11111","8":"0.125","7":"0.14286","6":"0.16667","5":"0.2","4":"0.25","3":"0.3333","2":"0.5","1":"1",
"0.5":"2","0.3333":"3","0.25":"4","0.2":"5","0.16667":"6","0.14286":"7","0.125":"8","0.11111":"9"
};
</script>
</body>

View File

@ -1,81 +1,73 @@
<style>
.radios-scroll-x {
overflow-x: auto;
white-space: nowrap;
padding-bottom: 4px; /* 防止滚动条遮住内容 */
}
.radios-scroll-x .form-check-inline {
white-space: nowrap;
}
</style>
<div class="page-body" data-page="evaluation_list">
<div class="container-xl">
<div th:replace="fragments/dialog::navigateDialog(${chainMenuList})"></div>
<div class="row row-cards">
<div class="card">
<!-- 顶部重点卡片 -->
<div class="card card-priority">
<div class="card-header">
<div class="card-title">指标数据评价集设置</div>
<h3 class="card-title title-accent">选择评价指标
<span class="badge bg-primary ms-2">必填</span>
</h3>
</div>
<div class="card-body">
<div class="mb-3">
<div class="radios-scroll-x">
<label class="form-check form-check-inline" th:each="item:${rootList}">
<input class="form-check-input" type="radio" name="radios-inline"
th:checked="${item.checked}" th:value="${item.id}">
<span class="form-check-label" th:text="${item.getName()}"></span>
</label>
</div>
<!-- 新增:上次上传文件 -->
<div class="text-muted small mt-2">说明: 切换不同的指标可以设置当前指标子项的评价标准
<div class="mb-3 row align-items-center">
<label class="col-3 col-form-label fw-semibold" for="indicationTopId">指标列表</label>
<div class="col">
<select class="form-select" name="indicationTopId" id="indicationTopId">
<option th:each="item:${rootList}" th:value="${item.id}" th:text="${item.name}"></option>
</select>
</div>
<div class="col-auto text-muted small">选定后,下方“评价集设置”将随之变化</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title" th:each="item : ${rootList}"
th:if="${item.checked == true}">[[${item.name}]]子集</h3>
</div>
<div class="list-group list-group-flush overflow-auto" style="max-height: 35rem">
<div class="list-group-item" th:each="item:${indicatorListWithoutChildren}">
<div class="row">
<label class="form-check form-check-inline">
<input class="form-check-input" type="radio"
name="radios-inline-indicator-no-child" th:value="${item.getId()}"
th:checked="${item.checked}" onclick="changeIndicator()">
<span class="form-check-label" th:text="${item.getName()}"></span>
</label>
</div>
</div>
</div>
</div>
<!-- 次级卡片 -->
<div class="card">
<div class="card-header">
<div class="card-title fw-bold">评价集设置</div>
<div class="ms-auto">
<a href="#" class="btn btn-primary" onclick="evaluation_add()">增加评价</a>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<!-- 左:子集列表 -->
<div class="col-md-3">
<div class="subset-panel">
<div class="subset-scroll list-group list-group-flush">
<div class="list-group-item bg-transparent border-0 py-2"
th:each="item:${indicatorListWithoutChildren}">
<label class="form-check form-check-inline m-0">
<input class="form-check-input" type="radio"
name="radios-inline-indicator-no-child" th:value="${item.getId()}"
th:checked="${item.checked}" onclick="changeIndicator()">
<span class="form-check-label ms-2" th:text="${item.getName()}"></span>
</label>
</div>
</div>
</div>
</div>
<div class="col-md-9">
<div class="row">
<div class="text-end">
<a href="#" class="btn btn-primary active w-10" onclick="evaluation_add()">
增加评价
</a>
</div>
</div>
<!-- 右:表格 -->
<div class="col-md-9">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<thead>
<tr>
<th>序号</th>
<th class="w-1">序号</th>
<th>名称</th>
<th>符号</th>
<th>下限值</th>
<th>符号</th>
<th>上限值</th>
<th ></th>
<th class="w-1">符号</th>
<th class="w-1">下限值</th>
<th class="w-1">符号</th>
<th class="w-1">上限值</th>
<th class="text-end w-10"></th>
</tr>
</thead>
<tbody>
@ -86,8 +78,8 @@
<td th:text="${item.getMinValue()}"></td>
<td th:text="${item.getMaxSymbol()}"></td>
<td th:text="${item.getMaxValue()}"></td>
<td class="w-15">
<a href="javascript:void(0)"
<td class="text-end">
<a href="javascript:void(0)" class="me-2"
th:onclick="|editEvaluation('${item.id}')|">编辑</a>
<a href="javascript:void(0)"
th:onclick="|removeEvaluation('${item.id}')|">删除</a>
@ -97,49 +89,52 @@
</table>
</div>
</div>
</div>
</div> <!-- row -->
</div>
</div>
</div>
</div>
</div>
<form id="evaluationForm">
<div th:replace="fragments/dialog::addSimpleFormDialog"></div>
</form>
<div th:replace="fragments/dialog::confirmationDialog"></div>
<script>
// 交互时给重点卡片一个轻微高亮
(function(){
const card = document.querySelector('.card-priority');
const topSel = document.getElementById('indicationTopId');
topSel?.addEventListener('change', ()=>{
card.classList.add('flash');
setTimeout(()=>card.classList.remove('flash'), 900);
});
})();
function evaluation_add(data) {
let url = document.getElementById("_rootPath").value + "evaluation/evaluationAdd";
let http = new HttpClient();
http.get(url, function (err, res, xhr) {
document.getElementById("simpleFormBody").innerHTML = res;
openDialog("simple-form-model");
if (data) {
fillData(data);
} else {
//设置指标祖先ID,指标ID
let topId = $(
'input[name="radios-inline"]:checked').val();
let indicationId = $(
'input[name="radios-inline-indicator-no-child"]:checked').val();
let topId = $('input[name="radios-inline"]:checked').val();
let indicationId = $('input[name="radios-inline-indicator-no-child"]:checked').val();
document.getElementById("evaluationForm")['indicatorTopId'].value = topId;
document.getElementById("evaluationForm")['indicatorId'].value = indicationId;
}
document.getElementById("SimpleFormDialog_save").onclick = function () {
evaluation_save();
}
}, "evaluationForm")
}
function fillData(data) {
fillForm('evaluationForm', data);
}
function fillData(data) { fillForm('evaluationForm', data); }
function evaluation_save() {
let url = document.getElementById("_rootPath").value + "evaluation/save";
@ -147,48 +142,37 @@
let form = document.getElementById("evaluationForm");
http.post(url, formObjToObject(form), function (err, res, xhr) {
closeDialog("simple-form-model");
//重新刷新页面
let topId = $('input[type=radio][name="radios-inline"]:checked').val();
let subId = $(
'input[name="radios-inline-indicator-no-child"]:checked').val();
document.getElementById("_evaluation_evaluationList").setAttribute("hx-vals",
JSON.stringify({topIndicatorId: topId, indicatorId: subId}))
let subId = $('input[name="radios-inline-indicator-no-child"]:checked').val();
document.getElementById("_evaluation_evaluationList")
.setAttribute("hx-vals", JSON.stringify({topIndicatorId: topId, indicatorId: subId}));
document.getElementById("_evaluation_evaluationList").click();
}, "evaluationForm", "simple-form-model");
}
//编辑
function editEvaluation(id) {
let url = document.getElementById("_rootPath").value + "evaluation/" + id;
let http = new HttpClient();
http.get(url, function (error, res, xhr) {
evaluation_add(res.result);
}, "evaluationForm", "simple-form-model");
http.get(url, function (error, res, xhr) { evaluation_add(res.result); },
"evaluationForm", "simple-form-model");
}
//删除
function removeEvaluation(id) {
function removeEvaluation(id) {
openDialog("modal-danger");
document.getElementById('delete-confirm-9999').onclick = function () {
let url = document.getElementById("_rootPath").value + "evaluation/remove/" + id;
let http = new HttpClient();
http.get(url, (err, res, xhr) => {
if (!err) {
//模拟点击菜单
document.getElementById("_evaluation_evaluationList").click();
}
if (!err) document.getElementById("_evaluation_evaluationList").click();
})
};
}
function changeIndicator(){
let topId = $('input[type=radio][name="radios-inline"]:checked').val();
let subId = $(
'input[name="radios-inline-indicator-no-child"]:checked').val();
document.getElementById("_evaluation_evaluationList").setAttribute("hx-vals",
JSON.stringify({topIndicatorId: topId, indicatorId: subId}))
let topId = $('input[name="radios-inline"]:checked').val();
let subId = $('input[name="radios-inline-indicator-no-child"]:checked').val();
document.getElementById("_evaluation_evaluationList")
.setAttribute("hx-vals", JSON.stringify({topIndicatorId: topId, indicatorId: subId}));
document.getElementById("_evaluation_evaluationList").click();
}
</script>

View File

@ -1,162 +1,164 @@
<!-- ====== 页面主体(纯静态,无 JS/Thymeleaf ====== -->
<div class="page-body" data-page="indicator_mapper">
<div class="container-xl">
<style>
<!-- 面包屑(静态示例) -->
<div th:replace="fragments/dialog::navigateDialog(${chainMenuList})"></div>
</style>
</head>
<div class="row row-cards">
<div class="card">
<div class="card-header">
<div class="card-title">指标数据映射集设置</div>
<div class="card-actions">
<a href="javascript:void(0)" class="btn btn-primary" onclick="mapperSave()">
<!-- Download SVG icon from http://tabler-icons.io/i/plus -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 5l0 14"></path>
<path d="M5 12l14 0"></path>
</svg>
保存映射关系
</a>
<div class="page">
<div class="page-body" data-page="indicator_mapper">
<div class="container-xl">
<!-- 面包屑(静态示例/Thymeleaf -->
<div th:replace="fragments/dialog::navigateDialog(${chainMenuList})"></div>
<div class="row row-cards">
<!-- 顶层主卡片:加上 card-hero -->
<div class="card card-hero">
<div class="card-header">
<div class="card-title">指标数据映射集设置</div>
<div class="card-actions">
<a href="javascript:void(0)" class="btn btn-primary" onclick="mapperSave()">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 5l0 14"></path>
<path d="M5 12l14 0"></path>
</svg>
保存映射关系
</a>
</div>
</div>
<div class="card-body">
<!-- 1. 根指标 -->
<div class="card section mb-3">
<div class="card-header">
<div class="card-title">1. 选择指标(根指标)</div>
</div>
<div class="card-body">
<div class="row g-2 align-items-center">
<div class="col-auto">
<label for="indicationId" class="form-label m-0">根指标</label>
</div>
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
<select id="indicationId" class="form-select" name="indicationId">
<option value="0">-选择根指标-</option>
<option th:each="item:${rootList}" th:value="${item.id}"
th:text="${item.getName()}" th:selected="${item.checked}"></option>
</select>
</div>
<div class="col-auto text-muted small">选择根指标后,再选择设施与 CSV 以建立映射。</div>
</div>
</div>
</div>
<!-- 2. 选择设施 -->
<div class="card section mb-3">
<div class="card-header">
<div class="card-title">2. 选择设施</div>
</div>
<div class="card-body">
<div class="row g-2 align-items-center">
<div class="col-auto">
<label for="modelId" class="form-label m-0">设施名称</label>
</div>
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
<select id="modelId" class="form-select" onchange="formModelChange(this)">
<option value="0">-选择form表单-</option>
<option th:each="modelItem:${modelList}" th:value="${modelItem.id}"
th:text="${modelItem.getModelName()}"
th:selected="${modelItem.checked}"></option>
</select>
</div>
</div>
</div>
</div>
<!-- 3. 上传 CSV -->
<div class="card section mb-3">
<div class="card-header">
<div class="card-title">3. 上传 CSV 文件</div>
</div>
<div class="card-body">
<input type="file" id="csvFile" class="form-control mb-3" accept=".csv">
<!-- 上次上传文件 -->
<div class="mb-2" th:if="${csvMapper!=null}">
<span class="fw-bold">上次上传文件:</span>
<a th:href="@{/indicator/download/{id}(id=${csvMapper.id})}" target="_blank"
th:text="${csvMapper.getCsvName()}"></a>
</div>
<div class="fw-bold mb-2" id="csvHeader-total-num"></div>
<div class="list-group list-group-flush csv-heads" id="csvHeaders">
<!-- 解析后在这里填充 CSV 列名 -->
</div>
</div>
</div>
<!-- 4. 子指标映射 -->
<div class="card section">
<div class="card-header">
<div class="card-title">4. 子指标映射设置</div>
</div>
<div class="card-body">
<div class="map-scroller">
<table class="table table-vcenter table-striped table-hover align-middle map-table">
<thead>
<tr>
<th class="sticky-header sticky-col-1 w-compact" style="width:48px;">#</th>
<th class="sticky-header sticky-col-2" style="width:220px;">子指标名称</th>
<th class="sticky-header" style="width:320px;">设施字段</th>
<th class="sticky-header" style="width:320px;">CSV 表头</th>
</tr>
</thead>
<tbody class="autonum">
<tr th:each="item:${childrenIndicator}">
<input type="hidden" name="indicatorId" th:value="${item.getId()}">
<td class="sticky-body-col-1 w-compact col-index"></td>
<td class="sticky-body-col-2 col-name" th:text="${item.getName()}"></td>
<td>
<select class="form-select form-select-sm" name="formField">
<option th:if="${formFieldList.size()>0}"
th:each="field:${formFieldList}" th:value="${field.id}"
th:text="${field.fieldLabel}"
th:selected="${field.id == (bottomFormMap != null ? bottomFormMap[item.id] : '')}">
</option>
</select>
</td>
<td>
<select class="form-select form-select-sm" name="csvField">
<option th:if="${csvColumns.size()>0}" th:each="column:${csvColumns}"
th:value="${column.id}"
th:text="${column.getCsvColumnName()}"
th:selected="${column.id == (bottomCsvColumnMap != null ? bottomCsvColumnMap[item.id] : '')}">
</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div class="text-muted small mt-2">
说明:为每个子指标选择表单字段或 CSV 列,系统会保存映射关系用于后续处理。
</div>
</div>
</div>
</div><!-- /card-body -->
</div>
<div class="card-body">
<!-- 1. 根指标(静态下拉) -->
<div class="card mb-3">
<div class="card-header">
<div class="card-title">1. 选择指标(根指标)</div>
</div>
<div class="card-body">
<div class="row g-2 align-items-center">
<div class="col-auto">
<label for="indicationId" class="form-label m-0">根指标</label>
</div>
<div class="col-3">
<select id="indicationId" class="form-select" name="indicationId">
<option value="0">-选择根指标-</option>
<option th:each="item:${rootList}" th:value="${item.id}"
th:text="${item.getName()}" th:selected="${item.checked}"></option>
</select>
</div>
</div>
</div>
</div>
<!-- 1. 根指标(静态下拉) -->
<div class="card mb-3">
<div class="card-header">
<div class="card-title">2. 选择设施</div>
</div>
<div class="card-body">
<div class="row g-2 align-items-center">
<div class="col-auto">
<label for="modelId" class="form-label m-0">设施名称</label>
</div>
<div class="col-3">
<select id="modelId" class="form-select" onchange="formModelChange(this)">
<option value="0">-选择form表单-</option>
<option th:each="modelItem:${modelList}" th:value="${modelItem.id}"
th:text="${modelItem.getModelName()}"
th:selected="${modelItem.checked}"></option>
</select>
</div>
</div>
</div>
</div>
<!-- 2. CSV 表头(静态 50 列) -->
<div class="card mb-3">
<div class="card-header">
<div class="card-title">3. 上传 CSV 文件</div>
</div>
<div class="card-body">
<input type="file" id="csvFile" class="form-control mb-3" accept=".csv">
<!-- 新增:上次上传文件 -->
<div class="mb-2" th:if="${csvMapper!=null}">
<span class="fw-bold">上次上传文件:</span>
<a th:href="@{/indicator/download/{id}(id=${csvMapper.id})}" target="_blank"
th:text="${csvMapper.getCsvName()}"></a>
</div>
<div class="fw-bold mb-2" id="csvHeader-total-num"></div>
<div class="list-group list-group-flush csv-heads" id="csvHeaders">
</div>
</div>
</div>
<!-- 3. 子指标映射(静态 20 行;首两列粘左;自动编号) -->
<div class="card">
<div class="card-header">
<div class="card-title">4. 子指标映射设置</div>
</div>
<div class="card-body">
<div class="map-scroller">
<table class="table table-vcenter table-striped table-hover align-middle map-table">
<thead>
<tr>
<th class="sticky-header sticky-col-1" style="width:48px;">#</th>
<th class="sticky-header sticky-col-2" style="width:200px;">子指标名称</th>
<th class="sticky-header" style="width:320px;">设施字段</th>
<th class="sticky-header" style="width:320px;">CSV 表头</th>
</tr>
</thead>
<tbody class="autonum">
<tr th:each="item:${childrenIndicator}">
<input type="hidden" name="indicatorId" th:value="${item.getId()}">
<td class="sticky-body-col-1 col-index"></td>
<td class="sticky-body-col-2 col-name" th:text="${item.getName()}"></td>
<td><select class="form-select form-select-sm" name="formField">
<option th:if="${formFieldList.size()>0}"
th:each="field:${formFieldList}" th:value="${field.id}"
th:text="${field.fieldLabel}" th:selected="${field.id == (bottomFormMap != null ? bottomFormMap[item.id] : '')}"></option>
>
</option>
</select></td>
<td><select class="form-select form-select-sm" name="csvField">
<option th:if="${csvColumns.size()>0}" th:each="column:${csvColumns}"
th:value="${column.id}"
th:text="${column.getCsvColumnName()}" th:selected="${column.id == (bottomCsvColumnMap != null ? bottomCsvColumnMap[item.id] : '')}"></option>
>
</option>
</select></td>
</tr>
</tbody>
</table>
</div>
<div class="text-muted small mt-2">说明选择子指标对应表单字段或者CSV某列</div>
</div>
</div>
</div><!-- /card-body -->
</div>
</div>
</div>
</div>
<!-- ====== 样式(关键:表头与表体分层;首两列分别粘左) ====== -->
<style>
</style>
<!-- ====== JS保持你的原有逻辑 ====== -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script>
function formModelChange(object) {
let postUrl = document.getElementById('_rootPath').value + "indicator/indicatorFormMapper";
@ -171,19 +173,15 @@
$.each(data, function (_, opt) {
$sel.append('<option value="' + opt.id + '">' + opt.fieldLabel + '</option>');
});
// 交互高亮一下主卡片
flashHero();
}, null, null)
}
function csvListen() {
$('#csvFile').on('change', function () {
const f = this.files[0];
if (!f) {
return;
}
if (!f) return;
let indicatorTopId = $('#indicationId').val();
const fd = new FormData();
fd.append('file', f);
@ -191,61 +189,76 @@
let postUrl = document.getElementById('_rootPath').value + "indicator/uploadCsv";
let http = new HttpClient();
http.postFormData(postUrl, fd, function (error, response, xhr) {
let data = response.result;
let data = response.result || [];
let len = data.length;
document.getElementById("csvHeader-total-num").innerHTML = "已解析CSV文件" + len + "列";
document.getElementById("csvHeaders").innerHTML = "";
let html = "";
document.getElementById("csvHeader-total-num").innerHTML = "已解析 CSV 文件:" + len + " 列";
const list = document.getElementById("csvHeaders");
list.innerHTML = "";
if (len > 0) {
const frag = document.createDocumentFragment();
data.forEach(item => {
html = html + " <div class=\"list-group-item\">" + item.csvColumnName + "</div>";
})
document.getElementById("csvHeaders").innerHTML = html;
//处理指标映射那里的selected选项
const div = document.createElement("div");
div.className = "list-group-item";
div.textContent = item.csvColumnName;
frag.appendChild(div);
});
list.appendChild(frag);
// 填充右侧下拉
let $sel = $('select[name="csvField"]');
$sel.empty();
$.each(data, function (_, opt) {
$sel.append('<option value="' + opt.id + '">' + opt.csvColumnName + '</option>');
});
}
flashHero();
}, null, null)
});
}
function mapperSave() {
let obj = {};
obj.indicatorTopId = $('#indicationId').val();
//指标ID集合
let indicatorElements = document.getElementsByName('indicatorId');
let formMapperElements = document.getElementsByName('formField');
let csvElements = document.getElementsByName('csvField');
let formList = [];
let csvList = [];
let formList = [], csvList = [];
for (let i = 0; i < indicatorElements.length; i++) {
//form映射
let formOneMapper = {};
formOneMapper.indicatorId = parseInt(indicatorElements[i].value);
formOneMapper.formFieldId = parseInt(formMapperElements[i].value);
formOneMapper.indicatorTopId = obj.indicatorTopId;
let formOneMapper = {
indicatorId: parseInt(indicatorElements[i].value),
formFieldId: parseInt(formMapperElements[i].value),
indicatorTopId: obj.indicatorTopId
};
formList.push(formOneMapper);
//csv映射
let csvOneMapper = {};
csvOneMapper.indicatorTopId = obj.indicatorTopId;
csvOneMapper.indicatorId = parseInt(indicatorElements[i].value);
csvOneMapper.csvColumnId = parseInt(csvElements[i].value);
csvList.push(csvOneMapper);
let csvOneMapper = {
indicatorTopId: obj.indicatorTopId,
indicatorId: parseInt(indicatorElements[i].value),
csvColumnId: parseInt(csvElements[i].value)
};
csvList.push(csvOneMapper);
}
obj.csvList = csvList;
obj.formList = formList;
//开始提交
let http = new HttpClient();
let url = document.getElementById('_rootPath').value + "indicator/saveMapper";
http.post(url,obj, function (error, response, xhr) {
showAlert("success","保存成功")
},null,null);
http.post(url, obj, function (error, response, xhr) {
showAlert("success","保存成功");
flashHero();
}, null, null);
}
</script>
// 根指标变化时也高亮主卡
$('#indicationId').on('change', flashHero);
// 初始化 CSV 监听
csvListen();
// 轻微高亮动画(与 CSS 中 .flash 对应)
function flashHero(){
const hero = document.querySelector('.card-hero');
if(!hero) return;
hero.classList.add('flash');
setTimeout(()=>hero.classList.remove('flash'), 900);
}
</script>

View File

@ -9,9 +9,9 @@
<div class="invalid-feedback" id="projectName_error_tip"></div>
</div>
<div class="mb-3">
<label class="form-label required" for="indicatorTopId">指标</label>
<select name="indicatorTopId" id="indicatorTopId" class="form-control">
<option th:each="item:${rootList}" th:value="${item.id}" th:text="${item.name}"></option>
<label class="form-label required" for="templateId">评估模板</label>
<select name="templateId" id="templateId" class="form-control">
<option th:each="item:${rootList}" th:value="${item.id}" th:text="${item.templateName}"></option>
</select>
</div>
<div class="mb-3">

View File

@ -39,7 +39,7 @@
</svg>
</th>
<th>工程名称</th>
<th>指标名称</th>
<th>模板名称</th>
<th>创建时间</th>
<th></th>
@ -49,7 +49,7 @@
<tr th:if="${result.list!=null}" th:each="project : ${result.list}">
<td th:text="${project.seq}"></td>
<td th:text="${project.projectName}"></td>
<td th:text="${project.indicatorTopName}"></td>
<td th:text="${project.templateName}"></td>
<td th:text="${#temporals.format(project.createDate, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
@ -60,7 +60,7 @@
</a>
<a href="javascript:void(0)" class="btn btn-info"
th:onclick="|_projectHistory('${project.id}')|">
评估结果
评估历史
</a>
<a href="javascript:void(0)" class=" btn btn-danger"
th:onclick="|_projectDelete('${project.id}')|">
@ -96,17 +96,14 @@
document.getElementById("_evaluation_project_").click();
}
function _toAdd(data) {
function _toAdd() {
let url = document.getElementById("_rootPath").value + "evaluation/project/add";
let http = new HttpClient();
http.get(url, function (error, res, xhr) {
document.getElementById("simpleFormBody").innerHTML = res;
openDialog("simple-form-model");
if (data) {
//给窗体赋值
fillData(data);
}
document.getElementById("SimpleFormDialog_save").onclick = function () {
saveProject();
}
@ -123,51 +120,28 @@
}, "projectForm", "simple-form-model")
}
//第一步加载指标树
function _startEvaluation(projectId) {
let url = document.getElementById("_rootPath").value + "evaluation/project/startEvaluation/"+projectId;
let http = new HttpClient();
document.getElementById("stepForm")["projectId"].value = projectId;
http.get(url, function (error, response, xhr) {
document.getElementById("add-full-screen-form-modal-body").innerHTML = response;
openDialog("modal-full-width");
initTreeMetric();
}, "stepForm")
}
function _projectDelete(id) {
openDialog("modal-danger");
document.getElementById('delete-confirm-9999').onclick = function () {
let url = document.getElementById("_rootPath").value + "evaluation/project/remove/"
+ id;
let http = new HttpClient();
http.get(url, function (error, res, xhr) {
_pageSearch();
})
};
}
function _projectEdit(id) {
}
function _projectHistory(id) {
}
//初始化指标树
function initTreeMetric() {
let projectId = document.getElementById("stepForm")['projectId'].value;
let url = document.getElementById("_rootPath").value + "evaluation/project/metricTree/"
+ projectId;
let http = new HttpClient();
http.get(url, function (error, response, body) {
function _startEvaluation(id){
$('#project_metric_tree').jstree({
'core': {
'data': response.result
}
}).on("select_node.jstree", function (e, data) {
});
}, null);
}
</script>

View File

@ -0,0 +1,58 @@
<div class="row row-cards">
<div class="col-12">
<div class="card card-hero">
<input type="hidden" id="draftKey" name="draftKey" value="">
<div class="card-header">
<h3 class="card-title">1.基本信息</h3>
</div>
<div class="card-body">
<input type="hidden" name="id" id="id" value="">
<div class="mb-3">
<label class="form-label required" for="templateName">模板名称</label>
<input type="text" id="templateName" class="form-control" name="templateName"
placeholder="模板名称">
<div class="invalid-feedback" id="templateName_error_tip"></div>
</div>
<div class="mb-3">
<label class="form-label " for="templateMemo">备注</label>
<textarea id="templateMemo" class="form-control" name="templateMemo"
placeholder="说明信息" rows="5"></textarea>
<div class="invalid-feedback" id="templateMemo_error_tip"></div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card card-hero">
<div class="card-header">
<h3 class="card-title">2.指标权重</h3>
</div>
<div class="card-body" style="min-height: 40em;">
<div class="row">
<div class="col-3">
<div class="mb-3 row">
<label class="col-3 col-form-label" for="indicatorTopId">指标</label>
<div class="col">
<select class="form-select" name="indicatorTopId" id="indicatorTopId" onchange="metricTree(this.value)">
<option value="">--请选择--</option>
<option th:each="item:${rootList}" th:value="${item.id}" th:text="${item.name}"></option>
</select>
</div>
</div>
<div class="mb-3 row" id="metric_tree">
</div>
</div>
<div class="col-9" id="table_area">
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,416 @@
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<!-- 面包屑导航 -->
<div th:replace="fragments/dialog::navigateDialog(${chainMenuList})"></div>
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">评估模板列表</h3>
<div class="card-actions">
<a href="javascript:void(0)" class="btn btn-primary" onclick="_toAdd()">
<!-- Download SVG icon from http://tabler-icons.io/i/plus -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 5l0 14"></path>
<path d="M5 12l14 0"></path>
</svg>
新增
</a>
</div>
</div>
<div th:replace="fragments/dialog::searchConditionDialog(${condition})"></div>
<div class="table-responsive">
<table class="table card-table table-vcenter text-nowrap datatable">
<thead>
<tr>
<th class="w-1">No.
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-up -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-sm icon-thick" width="24"
height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 15l6 -6l6 6"></path>
</svg>
</th>
<th>模板名称</th>
<th>对应指标</th>
<th>模板描述</th>
<th>创建时间</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:if="${result.list!=null}" th:each="project : ${result.list}">
<td th:text="${project.seq}"></td>
<td th:text="${project.templateName}"></td>
<td th:text="${project.indicatorTopName}"></td>
<td th:text="${project.templateMemo}"></td>
<td th:text="${#temporals.format(project.createTime, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
<div class="btn-list flex-nowrap">
<a href="javascript:void(0)" class="btn btn-primary"
th:onclick="|_templateEdit('${project.id}')|">
编辑
</a>
<a href="javascript:void(0)" class=" btn btn-danger"
th:onclick="|_templateDelete('${project.id}')|">
删除
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/dialog::paginationDialog(${result})"></div>
</div>
</div>
</div>
</div>
</div>
<div th:replace="fragments/dialog::confirmationDialog"></div>
<form id="templateForm">
<input type="hidden" id="projectId" name="projectId">
<div th:replace="fragments/dialog::addFullScreenFormDialog"></div>
</form>
<script>
function _toAdd(data) {
const url = document.getElementById("_rootPath").value + "evaluation/evaluationTemplate/add";
const http = new HttpClient();
http.get(url, function (error, res, xhr) {
document.getElementById("add-full-screen-form-modal-body").innerHTML = res;
openDialog("modal-full-width");
document.getElementById("templateForm")["draftKey"].value = generateDraftKey();
if (data) {
fillData(data);
}
document.getElementById("addFormDialog-btn").onclick = function () {
saveTemplate();
};
}, "templateForm");
}
function fillData(data) {
fillForm('templateForm', data);
metricTree(data.indicatorTopId);
}
function _templateDelete(id) {
openDialog("modal-danger");
document.getElementById('delete-confirm-9999').onclick = function () {
let url = document.getElementById("_rootPath").value + "evaluation/evaluationTemplate/remove/"
+ id;
let http = new HttpClient();
http.get(url, function (error, res, xhr) {
_pageSearch();
})
};
}
function _pageSearch() {
let query = formObjToObject(document.getElementById("searchForm"));
document.getElementById("_evaluation_evaluationTemplate_").setAttribute("hx-vals",
JSON.stringify(query))
document.getElementById("_evaluation_evaluationTemplate_").click();
}
function _templateEdit(id) {
let url = document.getElementById("_rootPath").value + "evaluation/evaluationTemplate/" + id;
let http = new HttpClient();
http.get(url, function (error, res, xhr) {
_toAdd(res.result);
}, 'templateForm')
}
function saveTemplate() {
let obj = {};
obj.id = document.getElementById("templateForm")["id"].value;
obj.templateName = document.getElementById("templateForm")["templateName"].value;
obj.templateMemo = document.getElementById("templateForm")["templateMemo"].value;
obj.indicatorTopId = document.getElementById("templateForm")["indicatorTopId"].value;
obj.draftKey = document.getElementById("templateForm")["draftKey"].value;
let pageMapData = getRowsTableData();
if (pageMapData && pageMapData.length > 0) {
obj.currentPagePartData = pageMapData;
}
let url = document.getElementById("_rootPath").value + "evaluation/evaluationTemplate/save";
let http = new HttpClient();
http.post(url, obj, function (error, res, xhr) {
closeDialog("modal-full-width");
document.getElementById("_evaluation_evaluationTemplate_").click();
}, "templateForm", null)
}
// 根据指标ID获取指标树
function metricTree(id) {
const url = document.getElementById("_rootPath").value
+ "evaluation/evaluationTemplate/metricTree/" + id;
const http = new HttpClient();
http.get(url, function (error, res, xhr) {
const $tree = $('#metric_tree');
// 如已有实例,先销毁并清空
if ($.jstree.reference($tree)) {
$tree.jstree(true).destroy();
$tree.empty();
}
// 重新初始化
$tree.jstree({
core: {data: res.result, check_callback: true}
});
// 树加载完 → 自动选中第一个根节点(触发 select 事件,从而 drawTable 一次)
$tree.off("loaded.jstree").on("loaded.jstree", function (e, data) {
const roots = data.instance.get_node('#').children || [];
if (roots.length > 0) {
data.instance.select_node(roots[0]); // 会触发 select_node.jstree
}
});
// 绑定选择事件(避免重复绑定)
$tree.off("select_node.jstree").on("select_node.jstree", function (e, data) {
//暂存数据
let id = data.node.id || "";
id = id.replace(/^tree_/, "");
draftDataAddAndGetData(id);
});
}, "templateForm");
}
//暂存旧数据+获取表数据
function draftDataAddAndGetData(id) {
//首先判断是否存在历史表格.
let headerData = getTableHeadData();
if (!headerData || headerData.size === 0) {
//去数据
getTablePage(id);
} else {
let obj = {};
obj.templateId = document.getElementById("templateForm")["id"].value;
obj.key = document.getElementById("templateForm")["draftKey"].value;
obj.headerMap = mapToObject(headerData);
obj.weight = getRowsTableData();
if (obj.weight.length > 0) {
//获取父ID
obj.parentIndicationId = obj.weight[0][0].parentId;
let url = document.getElementById("_rootPath").value
+ "evaluation/evaluationTemplate/receiveDraft";
let http = new HttpClient();
//暂存数据
http.post(url, obj, function (error, res, xhr) {
getTablePage(id);
}, null, null)
}
}
}
//获取表头元素
function getTableHeadData() {
let tr = document.getElementById("dynamic_header_tr");
if (!tr) {
return null;
}
let thList = tr.querySelectorAll('th');
let map = new Map();
for (let i = 1; i < thList.length; i++) {
let th = thList[i];
let obj = {};
obj.id = parseInt(th.getAttribute('data-value'));
obj.name = th.getAttribute('data-name');
map.set(obj.id, obj);
}
return map;
}
//绘制表格
function getTablePage(indicatorId) {
const templateId = document.getElementById("templateForm")["id"].value;
const url = document.getElementById("_rootPath").value
+ "evaluation/evaluationTemplate/getDraftData";
let obj = {};
//页面key
obj.key = document.getElementById("templateForm")["draftKey"].value;
//父ID
obj.parentIndicationId = indicatorId;
//模板ID
obj.templateId = templateId;
let http = new HttpClient();
http.post(url, obj, function (error, res, xhr) {
document.getElementById("table_area").innerHTML = res;
}, null, null)
}
// 前端验证是否符合一致性
// 如果符合一致性,算出权重值,修改树节点名称
function mapperChange(obj) {
removeValidCss('templateForm')
//此处验证提示
if (validateConsistency(obj.id, obj.value)) {
//实时计算指标值
let url = document.getElementById("_rootPath").value
+ "evaluation/evaluationTemplate/computer";
let http = new HttpClient();
let id = obj.id;
let value = obj.value;
//分割id 用_
let ids = id.split("_");
let reversId = ids[1] + "_" + ids[0];
//获取当前value对应的1/x值
document.getElementById("templateForm")[reversId].value = RECIP[value];
let postObj = {};
let metric = [];
let header = getTableHeadData();
header.forEach(element => {
metric.push(element.id + "");
})
postObj.metric = metric;
postObj.weightList = getRowsTableData();
http.post(url, postObj, function (error, res, xhr) {
const $tree = $('#metric_tree');
res.result.forEach(element => {
let id = "tree_" + element.id;
let node = $tree.jstree(true).get_node(id, false);
let name = node.text.replace(/\([^)]*\)/g, "");
name = name + "(" + element.weight + ")";
$tree.jstree(true).rename_node(node, name);
})
}, null, null)
}
}
//获取树当前选中节点的权重数据
function getRowsTableData() {
let weightList = [];
if (!document.getElementById("dynamic_tbody_tr")) {
return weightList;
}
if (!document.getElementById("dynamic_tbody_tr").querySelectorAll('tr')) {
return weightList;
}
let trList = document.getElementById("dynamic_tbody_tr").querySelectorAll('tr');
for (let i = 0; i < trList.length; i++) {
let tr = trList[i];
let tdList = tr.querySelectorAll('td');
let row = [];
for (let j = 1; j < tdList.length; j++) {
let td = tdList[j];
let select = td.querySelector('select');
let selectIdInfo = select.id.split('_');
let obj = {};
obj.rowId = selectIdInfo[0];
obj.colId = selectIdInfo[1];
obj.value = select.value;
//行号
obj.rowNum = i + 1;
//父指标ID
obj.parentId = select.getAttribute("data-pid");
row.push(obj);
}
weightList.push(row);
}
return weightList;
}
/**
* 验证一致性;只关注大小
* @param id 当前 select框id
* @param value 值
* @returns {boolean}
*/
function validateConsistency(id, value) {
let firstRowMap = getTableFirstRowMap();
//获取第一行中fromId
let [firstKey, firstValue] = firstRowMap.entries().next().value;
let firstRowFromId = firstKey.split('_')[0];
//获取当前id的from和to
let currentIds = id.split('_');
//当前id 的from
let currentFrom = currentIds[0];
//当前id的to
let currentTo = currentIds[1];
//获取当前id在第一列中firstRowFromId+"_"+currentFrom值
let firstRowFromVal = parseFloat(firstRowMap.get(firstRowFromId + "_" + currentFrom).value);
//获取当前id在第一列中firstRowFromId+"_"+currentTo
let firstRowToVal = parseFloat(firstRowMap.get(firstRowFromId + "_" + currentTo).value);
//获取当前值
let currentVal = parseFloat(value);
if (firstRowFromVal > firstRowToVal) {
if (currentVal > 1) {
showFieldErrorTip('templateForm', id, "权重必须小于1");
return false;
}
}
if (firstRowToVal < firstRowToVal) {
if (currentVal < 1) {
showFieldErrorTip('templateForm', id, "权重必须大于1");
return false;
}
}
return true;
}
function getTableFirstRowMap() {
let trList = document.getElementById("dynamic_tbody_tr").querySelectorAll('tr');
let tdList = trList[0].querySelectorAll('td');
let map = new Map();
for (let i = 1; i < tdList.length; i++) {
let td = tdList[i];
let select = td.querySelector('select');
map.set(select.id, select);
}
return map;
}
</script>

View File

@ -68,8 +68,12 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.11.0</version>
</dependency>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -30,6 +30,16 @@ public interface TableRelationsService extends IService<TableRelations> {
*/
void removeRel(Integer srcId, String srcTable, String dstTable);
/**
* 删除一个引用.
*
* @param srcId 源表ID
* @param srcTable 原表名称
* @param dstId 目标表ID
* @param dstTable 目标表名称
*/
void removeRel(Integer srcId, String srcTable, Integer dstId, String dstTable);
/**
* 增加引用关系.
*
@ -39,4 +49,5 @@ public interface TableRelationsService extends IService<TableRelations> {
* @param dstTable 引用表
*/
void addRel(Integer srcId, String srcTable, List<Integer> dstIdList, String dstTable);
}

View File

@ -43,6 +43,19 @@ public class TableRelationsServiceImpl extends
this.remove(queryWrapper);
}
@Override
public void removeRel(Integer srcId, String srcTable, Integer dstId, String dstTable) {
if (srcId == null || srcTable == null || dstTable == null || dstId == null) {
throw new IllegalArgumentException(ErrorMessage.IllegalArgumentException.getMessage());
}
QueryWrapper<TableRelations> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("dst_table_name", dstTable);
queryWrapper.eq("src_data_id", srcId);
queryWrapper.eq("dst_data_id", dstId);
queryWrapper.eq("src_table_name", srcTable);
this.remove(queryWrapper);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void addRel(Integer srcId, String srcTable, List<Integer> dstIdList, String dstTable) {
@ -58,4 +71,6 @@ public class TableRelationsServiceImpl extends
this.save(tableRelations);
});
}
}

View File

@ -1,16 +1,17 @@
package com.hshh.system.common.Strings;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Extractor
* Strings string 相关的工具.
*
* @author LiDongYU
* @date 2025/7/18
* @description
*/
public class Extractor {
public class StringOperationUtil {
public static String[] extractOption(String input) {
// 匹配形如 xx(xx) 的内容
@ -27,4 +28,18 @@ public class Extractor {
// 如果不匹配返回空数组或抛异常
return new String[0];
}
public static double[][] toDoubleArray(List<String[]> list) {
int rows = list.size();
int cols = list.get(0).length;
double[][] arr = new double[rows][cols];
for (int i = 0; i < rows; i++) {
String[] row = list.get(i);
for (int j = 0; j < cols; j++) {
arr[i][j] = Double.parseDouble(row[j]);
}
}
return arr;
}
}

View File

@ -0,0 +1,38 @@
package com.hshh.system.common.algorithm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 树节点每个非叶子节点可附带对子节点的判断矩阵
*/
public class AhpNode {
public final String name;
public final List<AhpNode> children = new ArrayList<>();
// 对子节点的两两比较矩阵仅在 children.size()>1 时需要
double[][] localMatrix;
// 计算得到
double[] localWeights; // 本节点的子节点局部权重=1
double lambdaMax = Double.NaN, ci = Double.NaN, cr = Double.NaN;
public double globalWeight = Double.NaN; // 本节点的全局权重根设为1叶子递推得到
public AhpNode(String name) {
this.name = name;
}
public AhpNode setLocalMatrix(double[][] m) {
this.localMatrix = m;
return this;
}
public AhpNode add(AhpNode... nodes) {
this.children.addAll(Arrays.asList(nodes));
return this;
}
}

View File

@ -0,0 +1,125 @@
package com.hshh.system.common.algorithm;
public class AhpTreeCompute {
/**
* 递归计算 root 开始将局部权重通过父节点全局权重向下传播
*/
public static void computeTree(AhpNode root) {
if (Double.isNaN(root.globalWeight)) {
root.globalWeight = 1.0; // 根默认 1
}
int k = root.children.size();
if (k == 0) {
return; // 叶子
}
if (k == 1) {
// 单子节点不需要矩阵权重=1直接传播
root.localWeights = new double[]{1.0};
AhpNode c = root.children.get(0);
c.globalWeight = root.globalWeight; // 乘1
computeTree(c);
root.lambdaMax = 1;
root.ci = 0;
root.cr = 0;
return;
}
if (root.localMatrix == null || root.localMatrix.length != k
|| root.localMatrix[0].length != k) {
throw new IllegalStateException(
"Node '" + root.name + "' needs a " + k + "x" + k + " localMatrix.");
}
// AHP对子节点判断矩阵 -> 局部权重 + 一致性
Ahp.EigenResult er = Ahp.principalEigen(root.localMatrix, 1e-12, 10000);
root.localWeights = er.w;
root.lambdaMax = er.lambdaMax;
root.ci = Ahp.consistencyIndex(er.lambdaMax, k);
root.cr = Ahp.consistencyRatio(er.lambdaMax, k);
// 传播子全局 = 父全局 × 局部权重
for (int i = 0; i < k; i++) {
AhpNode child = root.children.get(i);
child.globalWeight = root.globalWeight * root.localWeights[i];
computeTree(child);
}
}
/**
* 打印树缩进展示显示局部/全局权重与一致性
*/
static void printTree(AhpNode node, int depth) {
String indent = " ".repeat(depth);
String info = String.format("%s- %s (global=%.4f)", indent, node.name,
Double.isNaN(node.globalWeight) ? 0.0 : node.globalWeight);
for (AhpNode c : node.children) {
printTree(c, depth + 1);
}
}
// ---------------- 完整示例 ----------------
public static void main(String[] args) {
// 层次H -> A,B ; A -> A1,A2,A3 ; B -> B1,B2
AhpNode H = new AhpNode("H"); // 目标
AhpNode A = new AhpNode("A"); // 准则组 A
AhpNode B = new AhpNode("B"); // 准则组 B
// AhpNode A1 = new AhpNode("A1");
// AhpNode A2 = new AhpNode("A2");
// AhpNode A3 = new AhpNode("A3");
// AhpNode B1 = new AhpNode("B1");
// AhpNode B2 = new AhpNode("B2");
// 组装树
H.add(A, B);
// A.add(A1, A2, A3);
// B.add(B1, B2);
// 顶层 H 的判断矩阵A vs B
H.setLocalMatrix(new double[][]{
{1, 3},
{1.0/3, 1}
});
// // A 组内A1,A2,A3
// A.setLocalMatrix(new double[][]{
// {1, 2, 4},
// {0.5, 1, 3},
// {0.25, 1.0/3, 1}
// });
//
// // B 组内B1,B2
// B.setLocalMatrix(new double[][]{
// {1, 0.5},
// {2, 1}
// });
// 计算
H.globalWeight = 1.0; // 根节点全局权重=1
computeTree(H);
// 打印结果
printTree(H, 0);
// // 校验所有叶子全局之和应为 1
// double sumLeaves = sumLeaves(H);
// System.out.printf("%nSum of leaf global weights = %.6f%n", sumLeaves);
}
static double sumLeaves(AhpNode n) {
if (n.children.isEmpty()) {
return n.globalWeight;
}
double s = 0.0;
for (AhpNode c : n.children) {
s += sumLeaves(c);
}
return s;
}
}

View File

@ -1,132 +0,0 @@
package com.hshh.system.common.algorithm;
import java.util.*;
/** 树节点:每个非叶子节点可附带“对子节点的判断矩阵” */
class AhpNode {
final String name;
final List<AhpNode> children = new ArrayList<>();
// 对子节点的两两比较矩阵仅在 children.size()>1 时需要
double[][] localMatrix;
// 计算得到
double[] localWeights; // 本节点的子节点局部权重=1
double lambdaMax = Double.NaN, ci = Double.NaN, cr = Double.NaN;
double globalWeight = Double.NaN; // 本节点的全局权重根设为1叶子递推得到
AhpNode(String name) { this.name = name; }
AhpNode setLocalMatrix(double[][] m) { this.localMatrix = m; return this; }
AhpNode add(AhpNode... nodes) { this.children.addAll(Arrays.asList(nodes)); return this; }
}
public class AhpTreeDemo {
/** 递归计算:从 root 开始,将局部权重通过父节点全局权重向下传播 */
static void computeTree(AhpNode root) {
if (Double.isNaN(root.globalWeight)) root.globalWeight = 1.0; // 根默认 1
int k = root.children.size();
if (k == 0) return; // 叶子
if (k == 1) {
// 单子节点不需要矩阵权重=1直接传播
root.localWeights = new double[]{1.0};
AhpNode c = root.children.get(0);
c.globalWeight = root.globalWeight; // 乘1
computeTree(c);
root.lambdaMax = 1; root.ci = 0; root.cr = 0;
return;
}
if (root.localMatrix == null || root.localMatrix.length != k || root.localMatrix[0].length != k) {
throw new IllegalStateException("Node '"+root.name+"' needs a "+k+"x"+k+" localMatrix.");
}
// AHP对子节点判断矩阵 -> 局部权重 + 一致性
Ahp.EigenResult er = Ahp.principalEigen(root.localMatrix, 1e-12, 10000);
root.localWeights = er.w;
root.lambdaMax = er.lambdaMax;
root.ci = Ahp.consistencyIndex(er.lambdaMax, k);
root.cr = Ahp.consistencyRatio(er.lambdaMax, k);
// 传播子全局 = 父全局 × 局部权重
for (int i = 0; i < k; i++) {
AhpNode child = root.children.get(i);
child.globalWeight = root.globalWeight * root.localWeights[i];
computeTree(child);
}
}
/** 打印树(缩进展示),显示局部/全局权重与一致性 */
static void printTree(AhpNode node, int depth) {
String indent = " ".repeat(depth);
String info = String.format("%s- %s (global=%.4f)", indent, node.name,
Double.isNaN(node.globalWeight) ? 0.0 : node.globalWeight);
System.out.println(info);
if (node.children.size() > 1) {
System.out.printf("%s local weights = %s | λmax=%.6f CI=%.6f CR=%.6f%n",
indent, Ahp.fmt(node.localWeights), node.lambdaMax, node.ci, node.cr);
}
for (AhpNode c : node.children) printTree(c, depth+1);
}
// ---------------- 完整示例 ----------------
// public static void main(String[] args) {
// // 层次H -> A,B ; A -> A1,A2,A3 ; B -> B1,B2
// AhpNode H = new AhpNode("H"); // 目标
// AhpNode A = new AhpNode("A"); // 准则组 A
// AhpNode B = new AhpNode("B"); // 准则组 B
// AhpNode A1 = new AhpNode("A1");
// AhpNode A2 = new AhpNode("A2");
// AhpNode A3 = new AhpNode("A3");
// AhpNode B1 = new AhpNode("B1");
// AhpNode B2 = new AhpNode("B2");
//
// // 组装树
// H.add(A, B);
// A.add(A1, A2, A3);
// B.add(B1, B2);
//
// // 顶层 H 的判断矩阵A vs B
// H.setLocalMatrix(new double[][]{
// {1, 3},
// {1.0/3, 1}
// });
//
// // A 组内A1,A2,A3
// A.setLocalMatrix(new double[][]{
// {1, 2, 4},
// {0.5, 1, 3},
// {0.25, 1.0/3, 1}
// });
//
// // B 组内B1,B2
// B.setLocalMatrix(new double[][]{
// {1, 0.5},
// {2, 1}
// });
//
// // 计算
// H.globalWeight = 1.0; // 根节点全局权重=1
// computeTree(H);
//
// // 打印结果
// printTree(H, 0);
//
// // 校验所有叶子全局之和应为 1
// double sumLeaves = sumLeaves(H);
// System.out.printf("%nSum of leaf global weights = %.6f%n", sumLeaves);
// }
static double sumLeaves(AhpNode n) {
if (n.children.isEmpty()) return n.globalWeight;
double s = 0.0;
for (AhpNode c : n.children) s += sumLeaves(c);
return s;
}
}

View File

@ -0,0 +1,44 @@
package com.hshh.system.common.enums;
import lombok.Getter;
/**
* 评估模板状态枚举.
*
* @author LiDongYU
* @since 2025/7/22
*/
public enum EvaluationTemplateStatus {
DATA_WHOLE(1, "数据完整"),
NOT_SET_INDICATOR(-1, "指标未设置"),
INDICATOR_WEIGHT_NOT_WELL(-2, "指标权重不完整");
@Getter
final int code;
@Getter
final String desc;
EvaluationTemplateStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
/**
* 根据编码获取说明信息.
*
* @param code 编码
* @return 说明
*/
public static String getMessage(int code) {
if (code == 1) {
return DATA_WHOLE.desc;
}
if (code == -1) {
return NOT_SET_INDICATOR.desc;
}
if (code == -2) {
return INDICATOR_WEIGHT_NOT_WELL.desc;
}
return "";
}
}

View File

@ -0,0 +1,50 @@
package com.hshh.system.common.util;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
/**
* [类的简要说明]
* <p>
* [详细描述可选]
* <p>
*
* @author LiDongYU
* @since 2025/7/22
*/
public final class DraftStore {
private static final Cache<String, Object> CACHE =
Caffeine.newBuilder()
.expireAfterAccess(Duration.ofHours(2)) // 2小时不访问即过期
.maximumSize(100_000) // 视内存设置上限
.recordStats()
.build();
public static void put(String key, Object value) {
CACHE.put(key, value);
}
@SuppressWarnings("unchecked")
public static <T> T get(String key, Class<T> type) {
Object v = CACHE.getIfPresent(key);
return (type.isInstance(v) ? (T) v : null);
}
public static boolean remove(String key) {
Object prev = CACHE.asMap().remove(key);
return prev != null;
}
public static int removeByPrefix(String prefix) {
int n = 0;
for (String k : CACHE.asMap().keySet()) {
if (k.startsWith(prefix)) {
CACHE.invalidate(k);
n++;
}
}
return n;
}
}

View File

@ -54,5 +54,6 @@
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
</dependencies>
</project>

View File

@ -29,7 +29,7 @@ public class CodeGenerator {
basePath + "/src/main/resources/mapper")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("m_data_evaluation_template") // 设置需要生成的表名多个用逗号分隔
builder.addInclude("m_data_evaluation_template_weight") // 设置需要生成的表名多个用逗号分隔
.addTablePrefix("m_data_"); // 设置过滤表前缀
})
.execute();

View File

@ -119,7 +119,13 @@
<artifactId>commons-csv</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>