This commit is contained in:
wcq 2023-04-13 15:54:06 +08:00
parent 4093e21047
commit e80e6ac44f
7 changed files with 171 additions and 124 deletions

3
.gitignore vendored
View File

@ -18,4 +18,5 @@ tests/**/coverage/
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
dist01

View File

@ -68,8 +68,7 @@ export default defineComponent({
}
arrayData && (this.value = arrayData)
this.arrayEditVisible = false
},
}
}
})
</script>

View File

@ -30,8 +30,8 @@
:style="{
width: codeEditerCardWidth + 'px',
height: codeEditerCardHeight - 10 + 'px'
}" :code="props.index.code" language="js" />
<textarea v-model="props.index.code" :style="{
}" :code="code" :language="props.lang" />
<textarea v-model="code" :style="{
width: codeEditerCardWidth + 'px',
height: codeEditerCardHeight - 10 + 'px'
}" class="flex-1 focus:outline-none resize-none absolute code-editer-text p-2"></textarea>
@ -50,13 +50,13 @@
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, watch, defineProps } from "vue";
import { nextTick, onMounted, ref, watch, defineProps, computed } from "vue";
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import type { Index } from "../../types";
import type { Index, Lang } from "../../types";
import IndexCalParamInput from "../../IndexCalParam/IndexCalParamInput.vue";
hljs.registerLanguage("javascript", javascript);
const props = defineProps<{ index: Index }>();
const props = defineProps<{ index: Index,lang: Lang }>();
const codeEditerId = ref("codeEditerId");
const codeEditerCardWidth = ref(220);
const codeEditerCardHeight = ref(220);
@ -71,7 +71,7 @@ onMounted(() => {
});
window.addEventListener("resize", setSize);
});
const code = computed({ set(val: string) { props.index.codeConfig[props.lang] = val }, get() { return props.index&&props.index.codeConfig[props.lang] } })
const setSize = () => {
const el = document.getElementById(codeEditerId.value);
codeEditerCardHeight.value = el.offsetHeight;
@ -89,7 +89,7 @@ const funcTest = () => {
const evalCode =
constInParams +
"\n" +
props.index.code +
code +
"\n"
let error = "";
try {
@ -101,7 +101,7 @@ const funcTest = () => {
"参数定义:\n" +
constInParams +
"\n\n执行代码:\n" +
props.index.code +
code +
"\n\n输出结果:\n" +
JSON.stringify(
outValue
@ -116,7 +116,7 @@ const funcTest = () => {
"参数定义:\n" +
constInParams +
"\n\n执行代码:\n" +
props.index.code +
code +
"\n\n执行错误:\n" +
error.message;
});

View File

@ -27,6 +27,12 @@ type ValueConfig =
| ValueNumber
| ValueFile
| ValueNumberArray;
interface CodeConfig {
python: string;
js: string;
}
type Lang = "python" | "js";
interface Index {
id: number;
name: string;
@ -34,9 +40,16 @@ interface Index {
inputValue: any;
calValue: any;
valueConfig: ValueConfig;
code: string;
codeConfig?: CodeConfig;
unit?: string;
description?: string;
children?: Index[];
}
export type { Index, ValueConfig };
interface CalModel {
id: number;
name: string;
data: Index;
defaultLang: Lang;
}
export type { Index, ValueConfig, Lang, CalModel };

View File

@ -4,7 +4,10 @@
"name": "黄土坡GEP",
"inputValue": null,
"calValue": 1706.91,
"code": "let v=双流GDP+武侯GDP+龙泉驿GDP+金牛GDP+青羊GDP+郫都GDP+新都GDP\nreturn Number(v.toFixed(4))",
"codeConfig": {
"python": "",
"js": "let v=双流GDP+武侯GDP+龙泉驿GDP+金牛GDP+青羊GDP+郫都GDP+新都GDP\nreturn Number(v.toFixed(4))"
},
"unit": "万元",
"valueConfig": { "type": "number" },
"key": "黄土坡GEP",
@ -16,7 +19,10 @@
"calValue": 330,
"inputValue": null,
"valueConfig": { "type": "number" },
"code": "let v=农产品+林产品+牧产品+渔产品\nreturn v",
"codeConfig": {
"python": "",
"js": "let v=农产品+林产品+牧产品+渔产品\nreturn v"
},
"unit": "万元",
"children": [
{
@ -26,7 +32,6 @@
"calValue": null,
"inputValue": 100,
"valueConfig": { "type": "number" },
"code": "",
"unit": "万元"
},
{
@ -36,7 +41,6 @@
"calValue": null,
"inputValue": 80,
"valueConfig": { "type": "number" },
"code": "",
"unit": "万元"
},
{
@ -46,7 +50,6 @@
"calValue": null,
"inputValue": 60,
"valueConfig": { "type": "number" },
"code": "",
"unit": "万元"
},
{
@ -56,7 +59,6 @@
"calValue": null,
"inputValue": 90,
"valueConfig": { "type": "number" },
"code": "",
"unit": "万元"
}
]
@ -68,7 +70,6 @@
"calValue": null,
"inputValue": null,
"valueConfig": { "type": "number" },
"code": "",
"unit": "万元",
"children": [
{
@ -78,7 +79,10 @@
"calValue": 2127.4464,
"inputValue": null,
"valueConfig": { "type": "number" },
"code": "let v=实物量*单价/10000\nreturn v",
"codeConfig": {
"python": "",
"js": "let v=实物量*单价/10000\nreturn v"
},
"unit": "万元",
"children": [
{
@ -88,7 +92,10 @@
"calValue": 4246400,
"inputValue": null,
"valueConfig": { "type": "number" },
"code": "let v=森林生态系统+草地生态系统+农田生态系统\nreturn v",
"codeConfig": {
"python": "",
"js": "let v=森林生态系统+草地生态系统+农田生态系统\nreturn v"
},
"unit": "立方米",
"children": [
{
@ -98,7 +105,10 @@
"calValue": 3866400,
"inputValue": null,
"valueConfig": { "type": "number" },
"code": "let v=面积*(降雨量-地表径流量-蒸散发量)*1000\nreturn Number(v.toFixed(2))",
"codeConfig": {
"python": "",
"js": "let v=面积*(降雨量-地表径流量-蒸散发量)*1000\nreturn Number(v.toFixed(2))"
},
"unit": "立方米",
"children": [
{
@ -108,7 +118,6 @@
"calValue": null,
"inputValue": 40,
"valueConfig": { "type": "number" },
"code": "",
"unit": "平方公里"
},
{
@ -118,7 +127,6 @@
"calValue": null,
"inputValue": 100,
"valueConfig": { "type": "number" },
"code": "",
"unit": "mm"
},
{
@ -128,7 +136,6 @@
"calValue": null,
"inputValue": 2.67,
"valueConfig": { "type": "number" },
"code": "",
"unit": "mm"
},
{
@ -138,7 +145,6 @@
"calValue": null,
"inputValue": 0.67,
"valueConfig": { "type": "number" },
"code": "",
"unit": "mm"
}
]
@ -150,7 +156,6 @@
"calValue": null,
"inputValue": 200000,
"valueConfig": { "type": "number" },
"code": "",
"unit": "立方米"
},
{
@ -160,7 +165,6 @@
"calValue": null,
"inputValue": 180000,
"valueConfig": { "type": "number" },
"code": "",
"unit": "立方米"
}
]
@ -172,7 +176,10 @@
"calValue": 5.01,
"inputValue": null,
"valueConfig": { "type": "number" },
"code": "let v=单位库容年运营成本+单位库容工程造价*(水库折旧率/100)\nreturn parseFloat((v).toFixed(2))",
"codeConfig": {
"python": "",
"js": "let v=单位库容年运营成本+单位库容工程造价*(水库折旧率/100)\nreturn parseFloat((v).toFixed(2))"
},
"unit": "元",
"children": [
{
@ -182,7 +189,10 @@
"calValue": 5,
"inputValue": null,
"valueConfig": { "type": "number" },
"code": "let v=年度水库运营成本*10000/水库容量\nreturn v",
"codeConfig": {
"python": "",
"js": "let v=年度水库运营成本*10000/水库容量\nreturn v"
},
"unit": "元",
"children": [
{
@ -192,7 +202,6 @@
"calValue": null,
"inputValue": 10000,
"valueConfig": { "type": "number" },
"code": "",
"unit": "立方米"
},
{
@ -202,7 +211,6 @@
"calValue": null,
"inputValue": 5,
"valueConfig": { "type": "number" },
"code": "",
"unit": "万元"
}
]
@ -214,7 +222,10 @@
"calValue": 2,
"inputValue": null,
"valueConfig": { "type": "number" },
"code": "let v=水库工程造价*10000/水库容量\nreturn v",
"codeConfig": {
"python": "",
"js": "let v=水库工程造价*10000/水库容量\nreturn v"
},
"unit": "元",
"children": [
{
@ -224,7 +235,6 @@
"calValue": null,
"inputValue": 2,
"valueConfig": { "type": "number" },
"code": "",
"unit": "万元"
},
{
@ -234,7 +244,6 @@
"calValue": null,
"inputValue": 10000,
"valueConfig": { "type": "number" },
"code": "",
"unit": "立方米"
}
]
@ -246,7 +255,6 @@
"calValue": null,
"inputValue": 0.3,
"valueConfig": { "type": "number" },
"code": "",
"unit": "%"
}
]

View File

@ -3,17 +3,17 @@
<div class="m-2">
<el-button size="small" type="primary" class="ml-2" @click="saveToCache">保存到缓存</el-button>
<el-button size="small" type="primary" class="ml-2" @click="saveToFile">保存到文件</el-button>
<el-button size="small" type="success" class="ml-2" @click="() => run(tableData[0])">运行</el-button>
<el-button size="small" type="success" class="ml-2" @click="() => run(calModel.data)">运行</el-button>
</div>
<el-table :data="tableData" style="width: 100%; margin-bottom: 20px" row-key="id" align="center" border
default-expand-all>
<el-table highlight-current-row ref="table" :data="[calModel.data]" style="width: 100%; margin-bottom: 20px"
row-key="id" align="center" border default-expand-all>
<el-table-column prop="name" label="指标名称" />
<!-- <el-table-column prop="key" label="键值" align="center" /> -->
<el-table-column prop="calValue" label="计算结果" align="center" />
<el-table-column prop="inputValue" label="输入值" align="center">
<template #default="scope">
<div
v-if="(() => findParent(scope.row, tableData) && ([null, undefined, ''].indexOf(findParent(scope.row, tableData).inputValue) == -1))()"
v-if="(() => findParent(scope.row, calModel.data) && ([null, undefined, ''].indexOf(findParent(scope.row, calModel.data).inputValue) == -1))()"
style="text-decoration: line-through;color: red;">
{{ scope.row.inputValue }}
</div>
@ -21,10 +21,8 @@
<IndexCalParamInput v-model="scope.row.inputValue" :value-config="scope.row.valueConfig">
</IndexCalParamInput>
</div>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="120" align="center" />
<el-table-column label="操作" width="230" align="center">
<template #default="scope">
@ -96,7 +94,8 @@
</el-dialog>
<el-dialog :title="'公式编辑-' + editRowTemp.name" class="funcEditerDialog"
style="width: 90%;margin-top: 1rem;margin-bottom: 0;" v-model="codeEditVisible">
<FuncEditer :key="funcEditerKey" style="max-height: 100%;" :index="editRowTemp"></FuncEditer>
<FuncEditer :lang="calModel.defaultLang" :key="funcEditerKey" style="max-height: 100%;" :index="editRowTemp">
</FuncEditer>
<template #footer>
<el-button type="info" @click="codeEditVisible = false">取消</el-button>
<el-button type="primary" @click="saveCodeEdit">保存</el-button>
@ -105,10 +104,10 @@
</div>
</template>
<script lang="ts" setup>
import { ElMessageBox, ElMessage } from "element-plus";
import { ElMessageBox, ElMessage, ElTable } from "element-plus";
import { onMounted, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
import { Index } from "@/components/IndexCal/types";
import { CalModel, Index } from "@/components/IndexCal/types";
import FuncEditer from "@/components/IndexCal/IndexFuncEditer/components/FuncEditer.vue";
import 黄土坡GEP from "@/mock/黄土坡GEP.json"
import {
@ -117,8 +116,8 @@ import {
Edit,
Plus,
} from '@element-plus/icons-vue'
import { IndexCalRunner, treeMap } from "./indexCalRunner";
import { IndexCalError, IndexCalRunner, treeMapCore } from "./indexCalRunner";
const table = ref<InstanceType<typeof ElTable> | null>(null)
const rowAddVisible = ref(false);
const rowEditVisible = ref(false);
const codeEditVisible = ref(false);
@ -127,7 +126,6 @@ const addRowTemp = ref<Index>(null);
const editRowTemp = ref<Index>({
id: 0, name: '', inputValue: 1,
calValue: 1,
code: "",
valueConfig: { type: 'int' },
key: "",
});
@ -137,8 +135,9 @@ defineOptions({
name: "index_cal_model_edit"
});
//@ts-ignore
const tableData = ref<Index[]>(黄土坡GEP)
// const tableData = ref<Index[]>(GEP)
//@ts-ignore
const calModel = ref<CalModel>({ id: 1, name: "黄土坡GEP", data: 黄土坡GEP[0], defaultLang: 'js' })
function copy(obj) {
if (obj) {
return JSON.parse(JSON.stringify(obj));
@ -151,9 +150,8 @@ function showRowAdd(row: Index) {
// console.log("rrr", rs);
selectRow.value = row;
let idSet = new Set<number>()
treeMap(tableData.value[0], (p, c) => {
idSet.add(p.id)
idSet.add(c.id)
treeMapCore(calModel.value.data, (node) => {
idSet.add(node.id)
})
const newId = Math.max(...idSet) + 1;
@ -164,7 +162,10 @@ function showRowAdd(row: Index) {
calValue: null,
inputValue: null,
valueConfig: { type: "number" },
code: ""
codeConfig: {
python: "",
js: ""
}
};
rowAddVisible.value = true;
}
@ -198,7 +199,7 @@ function saveRowEdit() {
ElMessage.warning("键值不能为空");
return;
}
const parent = findParent(editRowTemp.value, tableData.value)
const parent = findParent(editRowTemp.value, calModel.value.data)
if ((editRowTemp.value.key != selectRow.value.key) && parent && parent.children && parent.children.filter(item => (item.key == addRowTemp.value.key)).length > 1) {
ElMessage.warning("键值不能重复");
return;
@ -210,7 +211,7 @@ function saveRowEdit() {
}
function saveCodeEdit() {
selectRow.value.code = editRowTemp.value.code
selectRow.value.codeConfig = copy(editRowTemp.value.codeConfig)
codeEditVisible.value = false;
}
const addRowUseDebounceFn = useDebounceFn(addRow, 200)
@ -221,14 +222,17 @@ function showRowEdit(row: Index) {
}
function showCodeEdit(row: Index, scope: any) {
editRowTemp.value = copy(row)
if (!editRowTemp.value.codeConfig) {
editRowTemp.value.codeConfig = { js: "", python: "" }
}
selectRow.value = row
funcEditerKey.value = String(new Date())
codeEditVisible.value = true;
}
//
function findParent(findItem, tree = [] as Index[]) {
let parens = tree;
function findParent(findItem, tree: Index) {
let parens = [tree];
let maxTime = 3000;
let times = 0;
while (parens.length) {
@ -266,7 +270,7 @@ function showRowDelete(row: Index) {
}
function rowDelete(row: Index) {
const parent = findParent(row, tableData.value);
const parent = findParent(row, calModel.value.data);
if (!parent) {
ElMessage.warning("无法删除");
} else {
@ -282,22 +286,31 @@ function createAndDownloadFile(fileName, content) {
URL.revokeObjectURL(blob.toString());
}
function saveToFile() {
createAndDownloadFile('data.json', JSON.stringify(tableData.value))
createAndDownloadFile('data.json', JSON.stringify(calModel.value))
}
function saveToCache() {
localStorage.setItem('indexCalModelCache', JSON.stringify(tableData.value))
localStorage.setItem('indexCalModelCache', JSON.stringify(calModel.value))
}
function loadFromCache() {
let item = localStorage.getItem('indexCalModelCache')
item && (tableData.value = JSON.parse(item))
item && (calModel.value = JSON.parse(item))
}
function run(data: Index) {
const runner = new IndexCalRunner(data)
runner.run(data)
try {
runner.run(data, calModel.value.defaultLang)
} catch (e) {
if (e instanceof IndexCalError) {
console.error(e)
ElMessage.warning(e.message)
} else {
throw e;
}
}
}
onMounted(() => {

View File

@ -1,67 +1,74 @@
import { Index } from "@/components/IndexCal/types";
import type { Index, Lang } from "@/components/IndexCal/types";
type IndexCalErrorType = "codeConfigNotConfig" | "paramValueNotInput";
interface errorStore {
index?: Index;
}
class IndexCalError extends Error {
store: errorStore;
type: IndexCalErrorType;
constructor(
type: IndexCalErrorType = "",
message = "计算出错",
store: {} = {}
) {
super(message);
this.name = "IndexCalError";
this.type = type;
this.store = store;
}
}
function treeMap(
function treeMapCore(
tree: Index,
func: (parent: Index, child: Index, dep: number) => any,
dep = 1
func: (child: Index, parent: Index, dep: number) => boolean | any,
dep = 1,
parent: Index = null
) {
if (func(tree, parent, dep) === false) {
return;
}
const children = tree.children;
if (children) {
const len = children.length;
for (let i = 0; i < len; i++) {
const child = children[i];
func(tree, child, dep);
treeMap(child, func, dep + 1);
treeMapCore(child, func, dep + 1, tree);
}
}
}
// //获取所有叶节点
// function getLeafNodes(index: Index) {
// const leafNodes = [];
// treeMap(index, (p, c) => {
// if (!(c.children && c.children.length > 0)) {
// leafNodes.push(c);
// }
// });
// return leafNodes;
// }
// //获取所有叶节点的父节点
// function getLeafParentNodes(index: Index) {
// const parentNodes = [];
// treeMap(index, (p, c) => {
// if (!(c.children && c.children.length > 0)) {
// if (parentNodes.indexOf(p) == -1) {
// parentNodes.push(p);
// }
// }
// });
// return parentNodes;
// }
// //获取节点列表的父节点列表
// function getNodesParent(index: Index, nodes: Index[]) {
// const parentNodes = [];
// treeMap(index, (p, c) => {
// if (nodes.indexOf(c) != -1 && parentNodes.indexOf(p) != -1) {
// parentNodes.push(p);
// }
// });
// return parentNodes;
// }
//获取树的深度字典
function getDepDic(index: Index) {
let depDic = {};
treeMap(index, (p, c, dep) => {
treeMapCore(index, (node, parent, dep) => {
if (!depDic[dep]) {
depDic[dep] = [];
}
depDic[dep].push(p);
depDic[dep].push(node);
});
return depDic;
}
// //获取树的深度字典(不保留有输入值节点的子节点)
function getDepDicExincludeHasInput(index: Index) {
let depDic = {};
treeMapCore(index, (node, parent, dep) => {
if (!depDic[dep]) {
depDic[dep] = [];
}
depDic[dep].push(node);
if ([undefined, null, ""].indexOf(node.inputValue) == -1) {
return false;
}
});
return depDic;
}
//清除保留的计算值
function clearCalValue(index: Index) {
treeMapCore(index, node => (node.calValue = null));
}
//对节点进行计算
function calIndex(index: Index) {
function calIndex(index: Index, lang: Lang = "js") {
const paramDic = {};
index.children.forEach(
//计算值与输入值优先取输入值
@ -70,12 +77,27 @@ function calIndex(index: Index) {
[undefined, null, ""].indexOf(item.inputValue) == -1
? item.inputValue
: item.calValue;
if (paramDic[item.key] === null) {
throw new IndexCalError(
"paramValueNotInput",
`${index.name}-${item.name}值未配置`,
{
index
}
);
}
}
);
if (!index.codeConfig) {
throw new IndexCalError("codeConfigNotConfig", index.name + "公式未配置", {
index
});
}
let code = index.codeConfig[lang];
const defineValueCode = `let {${Object.keys(paramDic).join(
","
)}}=${JSON.stringify(paramDic)}`;
let evalCode = `()=>{${defineValueCode + "\n" + index.code}}`;
let evalCode = `()=>{${defineValueCode + "\n" + code}}`;
let res = eval(evalCode)();
return res;
}
@ -87,30 +109,21 @@ class IndexCalRunner {
this.data = JSON.parse(JSON.stringify(sourecData));
this.sourecData = sourecData;
}
run(data: Index) {
// let data = JSON.parse(JSON.stringify(this.data));
const depDic = getDepDic(data);
run(data: Index, lang: Lang = "js") {
clearCalValue(data);
const depDic = getDepDicExincludeHasInput(data);
let maxDep = Math.max(...Object.keys(depDic).map(item => Number(item)));
// let parents = getLeafParentNodes(data);
for (let i = maxDep; i > 0; i--) {
let parents = depDic[i];
parents.forEach(item => {
const calValue = calIndex(item);
item.calValue = calValue;
if (item.children && item.children.length) {
const calValue = calIndex(item, lang);
item.calValue = calValue;
}
});
}
// while (true) {
// if (!(parents && parents.length > 0)) {
// break;
// }
// parents.forEach(item => {
// const calValue = calIndex(item);
// item.calValue = calValue;
// });
// parents = getNodesParent(data, parents);
// }
return data;
}
}
export { IndexCalRunner, treeMap };
export { IndexCalRunner, IndexCalError, treeMapCore };