dynamicRingdiffer.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. <template>
  2. <el-dialog
  3. :visible.sync="isOpenDialog"
  4. :close-on-click-modal="false"
  5. :modal-append-to-body="false"
  6. @close="cancelHandle"
  7. custom-class="dynamic-differ-dialog"
  8. center
  9. width="1090px"
  10. top="8vh"
  11. v-dialogDrag
  12. >
  13. <div slot="title" style="display: flex; align-items: center">
  14. <span style="font-size: 16px">{{ title }}</span>
  15. </div>
  16. <div class="dialog-main">
  17. <ul class="add-cont">
  18. <li class="add-li" v-for="(list, index) in addList" :key="index">
  19. <span class="li-tag">{{ list.tag }}</span>
  20. <el-select
  21. v-model="list.target"
  22. v-loadMore="searchLoad"
  23. :filterable="!list.target"
  24. clearable
  25. placeholder="请输入指标名称"
  26. style="width: 400px"
  27. @change="chooseTarget"
  28. @clear="clearHandle(index)"
  29. remote
  30. :remote-method="searchTarget"
  31. @click.native="inputFocusHandle"
  32. >
  33. <i slot="prefix" class="el-input__icon el-icon-search"></i>
  34. <el-option
  35. v-for="item in searchOptions"
  36. :key="item.EdbInfoId"
  37. :label="item.EdbName"
  38. :value="item.EdbInfoId"
  39. >
  40. <edbDetailPopover :info="item">
  41. <div slot="reference">{{item.EdbName}}</div>
  42. </edbDetailPopover>
  43. </el-option>
  44. </el-select>
  45. <i class="el-icon-tickets" style="color:#409EFF;font-size:18px" @click="$emit('lookHistory',list.target)" v-if="list.target"/>
  46. <i
  47. class="el-icon-error del-tag"
  48. v-if="index > 3"
  49. @click="delTarget(index)"
  50. />
  51. <span class="target-date" v-if="list.start_date">{{
  52. `${list.start_date}至${list.end_date}`
  53. }}</span>
  54. </li>
  55. </ul>
  56. <span class="add-icon" @click="addTargetHandle">
  57. <i
  58. class="el-icon-circle-plus-outline"
  59. style="color: #5882ef; font-size: 16px"
  60. />
  61. 添加更多参数
  62. </span>
  63. <div class="computed-min">
  64. <div class="computed-section">
  65. <div>
  66. <label class="label">空值处理
  67. <el-tooltip placement="top">
  68. <div slot="content" v-html="formTips['null-val']" style="width:300px;line-height:20px;"/>
  69. <i class="el-icon-question"/>
  70. </el-tooltip>
  71. </label>
  72. <el-select
  73. v-model="nullValueForm.nullValueWay"
  74. placeholder="请选择"
  75. >
  76. <el-option
  77. v-for="item in nullWayOptions"
  78. :key="item.value"
  79. :label="item.label"
  80. :value="item.value"
  81. >
  82. </el-option>
  83. </el-select>
  84. </div>
  85. <div style="margin-left: 120px" v-if="showMaxNullDeal">
  86. <label class="label">MAX、MIN空值处理
  87. <el-tooltip placement="top">
  88. <div slot="content" v-html="formTips['max-null-val']" style="width:300px;line-height:20px;"/>
  89. <i class="el-icon-question"/>
  90. </el-tooltip>
  91. </label>
  92. <el-select
  93. v-model="nullValueForm.maxNullWay"
  94. placeholder="请选择"
  95. >
  96. <el-option label="等于0" :value="1" />
  97. <el-option label="跳过空值" :value="2" />
  98. </el-select>
  99. </div>
  100. <el-button type="primary" @click="getResult" style="margin-left: 10px">一键计算</el-button>
  101. </div>
  102. <div class="computed-section">
  103. <label class="label">计算公式
  104. <el-tooltip placement="top">
  105. <div slot="content" v-html="formTips['formula']" style="width:300px;line-height:20px;"/>
  106. <i class="el-icon-question"/>
  107. </el-tooltip>
  108. </label>
  109. <ul class="formula-list">
  110. <li style="margin-bottom: 15px;">
  111. <el-input placeholder="请输入公式" v-model="formulaList[0].formula" clearable style="width: 220px"/>
  112. <span v-if="formulaDateArr.length" class="date-section-text">{{formulaDateArr[formulaDateArr.length-1]}}(含)之后</span>
  113. <span class="example-txt">公式示例:A*0.5+B*C*1.2+120-MAX(A,B,C) &nbsp;函数支持:MAX(),MIN(),ln(A),log(a,A),abs(),exp(),pow(),round()</span>
  114. </li>
  115. <li class="formula-item" v-for="(item,index) in formulaList.slice(1)" :key="index+1">
  116. <el-input
  117. placeholder="请输入公式"
  118. v-model="item.formula"
  119. clearable
  120. style="width: 220px"
  121. />
  122. <el-date-picker
  123. v-model="item.date"
  124. type="date"
  125. value-format="yyyy-MM-dd"
  126. style="margin: 0 10px;width: 220px"
  127. placeholder="选择日期"
  128. @change="selectFormulaDate($event,item)"
  129. />
  130. <i class="el-icon-circle-close" style="font-size:20px;" @click="removeFormulaItem(index+1)"/>
  131. <template v-if="formulaDateArr.length&&item.date">
  132. <span v-if="item.date===formulaDateArr[0]" class="date-section-text">{{formulaDateArr[0]}}之前</span>
  133. <span v-else class="date-section-text">{{formulaDateArr[formulaDateArr.findIndex(_ =>_ ===item.date)-1]}}(含)——{{item.date}}</span>
  134. </template>
  135. </li>
  136. </ul>
  137. </div>
  138. <el-button icon="el-icon-plus" style="margin-left:70px;" @click="addFormulaHandle">新增分段</el-button>
  139. </div>
  140. <el-table
  141. border
  142. :data="resultData"
  143. v-if="resultData.length"
  144. max-height="320"
  145. >
  146. <el-table-column
  147. v-for="item in tableColums"
  148. :key="item.label"
  149. :label="item.label"
  150. :width="item.widthsty"
  151. :min-width="item.minwidthsty"
  152. align="center"
  153. >
  154. <template slot-scope="scope">
  155. <span :style="scope.row.isPredict&&'color: orange'">{{ scope.row[item.key] }}</span>
  156. </template>
  157. </el-table-column>
  158. <div slot="empty" style="padding:20px 0 30px;">
  159. <tableNoData text="暂无指标" size="mini"/>
  160. </div>
  161. </el-table>
  162. </div>
  163. <div class="dia-bot">
  164. <el-button
  165. type="primary"
  166. style="margin-right: 20px"
  167. @click="saveHandle"
  168. >保存</el-button
  169. >
  170. <el-button type="primary" plain @click="cancelHandle">取消</el-button>
  171. </div>
  172. </el-dialog>
  173. </template>
  174. <script>
  175. import * as preDictEdbInterface from '@/api/modules/predictEdbApi.js';
  176. const tag_arr = [];
  177. for(var i=0;i<26;i++) tag_arr.push(String.fromCharCode(65+i));
  178. export default {
  179. name: '',
  180. props: {
  181. isOpenDialog: {
  182. type: Boolean,
  183. },
  184. title: {
  185. type: String,
  186. default: '设置环比增加值',
  187. },
  188. info: {
  189. type: Object,
  190. },
  191. edbList: {
  192. type: Array,
  193. },
  194. },
  195. computed: {
  196. /* max空值处理显示 当输入的公式包含MAX、MIN且空值处理为0时,输入公式失焦后出现右侧选项; */
  197. showMaxNullDeal() {
  198. let haveMaxOrMin = this.formulaList.some(_ => _.formula.toUpperCase().includes('MAX') || _.formula.toUpperCase().includes('MIN'))
  199. return haveMaxOrMin && this.nullValueForm.nullValueWay===4
  200. },
  201. formulaDateArr() {
  202. return this.formulaList.map(_ => _.date).filter(_ => _).sort((a,b) => new Date(a)-new Date(b))
  203. }
  204. },
  205. watch: {
  206. isOpenDialog(newval) {
  207. /* 回显 */
  208. if (this.edbList.length && newval) {
  209. this.addList = _.cloneDeep(this.edbList);
  210. const{ formulaList,nullValueWay,maxNullWay } = this.info;
  211. this.formulaList = JSON.parse(formulaList).map(_=>({
  212. formula: _.f,
  213. date: _.d
  214. }));
  215. this.nullValueForm = {
  216. nullValueWay,
  217. maxNullWay
  218. }
  219. this.searchOptions = this.edbList.map(item => ({
  220. EdbInfoId: item.target,
  221. EdbName: item.name,
  222. }))
  223. }
  224. },
  225. },
  226. data() {
  227. return {
  228. addList: [
  229. {
  230. tag: tag_arr[0],
  231. name: '',
  232. target: '',
  233. start_date: '',
  234. end_date: '',
  235. },
  236. {
  237. tag: tag_arr[1],
  238. name: '',
  239. target: '',
  240. start_date: '',
  241. end_date: '',
  242. },
  243. {
  244. tag: tag_arr[2],
  245. name: '',
  246. target: '',
  247. start_date: '',
  248. end_date: '',
  249. },
  250. {
  251. tag: tag_arr[3],
  252. name: '',
  253. target: '',
  254. start_date: '',
  255. end_date: '',
  256. },
  257. ],
  258. searchOptions: [],
  259. formula: '', //计算公式
  260. dataloading: false,
  261. search_have_more: false,
  262. search_page: 1,
  263. current_search:'',
  264. tableColums: [
  265. {
  266. label: '日期',
  267. key: 'DataTime',
  268. },
  269. {
  270. label: '环比增加值',
  271. key: 'Value',
  272. },
  273. ],
  274. formulaList: [
  275. { formula: '', }
  276. ],
  277. nullValueForm: {
  278. nullValueWay: 0,
  279. maxNullWay: 1
  280. },//空值处理
  281. nullWayOptions: [
  282. { label: '查找前后35天最近值',value: 0 },
  283. { label: '不计算',value: 1 },
  284. { label: '前值填充',value: 2 },
  285. { label: '后值填充',value: 3 },
  286. { label: '等于0',value: 4 },
  287. ],
  288. formTips: {
  289. 'null-val': `1、查找前后35天最近值:在参与计算的日期序列上某指标无值时,该指标往前/往后找距离最近的值作为当天的值进行计算,遍历允许跨年,往前最多35天,往后最多35天<br>
  290. 2、不计算:只要有一个指标在某个日期没有值(即空值),则计算指标在该日期没有值 <br>
  291. 3、前值填充:空值优先以最近的前值填充,没有前值时,用后值填充 <br>
  292. 4、后值填充:空值优先以最近的后值填充,没有前值时,用后值填充 <br>
  293. 5、等于0:空值以0值参与计算 <br>
  294. 注意:此处缺失值的处理,作用于数据全部时间段`,
  295. 'max-null-val': `MAX、MIN公式中指标存在空值时按如下规则处理:<br>
  296. 1、等于0,空值用0参与计算;<br>
  297. 2、跳过空值,去除空值指标,剩余指标进行计算,若该日期所有指标均为空值,则该日期无值;`,
  298. 'formula':`1、支持新增分段,实现不同分段使用不同的计算公式,若未新增分段,则所有日期序列用统一公式计算<br>
  299. 2、新增分段需配置新公式和时间节点,在时间节点之前(不含)使用新公式,在时间节点之后(含)使用已配置公式,每个分段公式支持修改<br>
  300. 3、分段时间节点不允许重复,不允许超出第一个指标的日期区间`
  301. },
  302. resultData: [],//
  303. };
  304. },
  305. methods: {
  306. /* 添加额外的指标列 */
  307. addTargetHandle() {
  308. let tag = this.addList[this.addList.length-1].tag;
  309. let index = tag_arr.findIndex(item => item === tag);
  310. const item = {
  311. tag: tag_arr[index+1],
  312. target: '',
  313. name: '',
  314. start_date: '',
  315. end_date: ''
  316. };
  317. this.addList.push(item);
  318. },
  319. /* 搜索指标 */
  320. searchTarget(query) {
  321. this.search_page = 1;
  322. this.current_search = query;
  323. this.searchApi(this.current_search);
  324. },
  325. /* 聚焦获取当前检索 */
  326. inputFocusHandle(e) {
  327. this.search_page = 1;
  328. this.current_search = e.target.value;
  329. this.searchApi(this.current_search);
  330. },
  331. searchApi(query,page=1) {
  332. preDictEdbInterface
  333. .edbSearch({
  334. Keyword: query,
  335. CurrentIndex: page
  336. })
  337. .then((res) => {
  338. if (res.Ret !== 200) return
  339. const { List,Paging } = res.Data;
  340. this.search_have_more = page < Paging.Pages;
  341. this.searchOptions = page === 1 ? List : this.searchOptions.concat(List);
  342. });
  343. },
  344. searchLoad() {
  345. if(!this.search_have_more) return;
  346. this.searchApi(this.current_search,++this.search_page)
  347. },
  348. /* 选中指标 显示开始日期结束日期 */
  349. chooseTarget(val) {
  350. if (val) {
  351. const choose_obj = this.searchOptions.find(
  352. (item) => item.EdbInfoId === val
  353. );
  354. this.addList.forEach((list) => {
  355. if (list.target === val) {
  356. list.name = choose_obj.EdbName;
  357. list.start_date = choose_obj.StartDate;
  358. list.end_date = choose_obj.EndDate;
  359. }
  360. });
  361. }
  362. },
  363. /* 清空指标和关联日期 */
  364. clearHandle(index) {
  365. this.addList[index].start_date = '';
  366. this.addList[index].end_date = '';
  367. this.addList[index].name = '';
  368. },
  369. // 删除指标
  370. delTarget(index) {
  371. this.addList.splice(index, 1);
  372. },
  373. saveHandle() {
  374. if (!this.formulaList[0].formula) return this.$message.warning('计算公式不能为空');
  375. // 指标id数组
  376. let target_arr = this.addList.filter(item => item.target);
  377. let formula = JSON.stringify(this.formulaList
  378. .filter((_,index) => index===0||(index>0&&_.formula&&_.date))
  379. .map(_ => ({f: _.formula,d: _.date}))
  380. )
  381. this.$emit('ensureBack',{arr:target_arr,formula,...this.nullValueForm})
  382. this.cancelHandle();
  383. },
  384. /* 获取数据 */
  385. async getResult() {
  386. if (!this.formulaList[0].formula) return this.$message.warning('计算公式不能为空');
  387. let EdbInfoIdArr = this.addList.filter(_ => _.target).map(_ => ({
  388. EdbInfoId: _.target,
  389. FromTag: _.tag
  390. }))
  391. const { nullValueWay,maxNullWay } = this.nullValueForm;
  392. let formula = JSON.stringify(this.formulaList
  393. .filter((_,index) => index===0||(index>0&&_.formula&&_.date))
  394. .map(_ => ({f: _.formula,d: _.date}))
  395. )
  396. const params = {
  397. RuleType: 9,
  398. EndDate: '',
  399. Value: formula,
  400. EmptyType: nullValueWay,
  401. MaxEmptyType: maxNullWay,
  402. EdbInfoIdArr
  403. }
  404. const { Ret,Data } = await preDictEdbInterface.getRuleNineData(params);
  405. if(Ret !== 200) return
  406. this.resultData = Data.DataList.map(_ => ({
  407. ..._,
  408. isPredict: _.DataTimestamp > this.$moment(Data.LatestDate+' 8:00').valueOf()
  409. })) || [];
  410. },
  411. /* 新增公式分段 */
  412. addFormulaHandle() {
  413. let addItem = {
  414. formula: this.formulaList[this.formulaList.length-1].formula,
  415. date: ''
  416. }
  417. this.formulaList.push(addItem)
  418. },
  419. /* 移除公式 */
  420. removeFormulaItem(index) {
  421. this.formulaList.splice(index,1)
  422. },
  423. /* 选择日期 检验 */
  424. selectFormulaDate(val,item) {
  425. const { start_date,end_date } = this.addList[0];
  426. if(!start_date) return
  427. let dateStamp = new Date(val).getTime(),
  428. startStamp = new Date(start_date).getTime(),
  429. endStamp = new Date(end_date).getTime();
  430. if (dateStamp > endStamp || dateStamp < startStamp) {
  431. item.date = '';
  432. return this.$message.warning('分段日期必须在第一个指标日期区间')
  433. }
  434. else if(this.formulaList.filter(_ => _.date===val).length>1) {
  435. item.date = '';
  436. return this.$message.warning('分段日期不可重复')
  437. }
  438. },
  439. init() {
  440. this.addList = [
  441. {
  442. tag: tag_arr[0],
  443. name: '',
  444. target: '',
  445. start_date: '',
  446. end_date: '',
  447. },
  448. {
  449. tag: tag_arr[1],
  450. name: '',
  451. target: '',
  452. start_date: '',
  453. end_date: '',
  454. },
  455. {
  456. tag: tag_arr[2],
  457. name: '',
  458. target: '',
  459. start_date: '',
  460. end_date: '',
  461. },
  462. {
  463. tag: tag_arr[3],
  464. name: '',
  465. target: '',
  466. start_date: '',
  467. end_date: '',
  468. },
  469. ];
  470. this.searchOptions = [];
  471. this.formula = '';
  472. this.resultData = [];
  473. this.formulaList = [
  474. { formula: '' }
  475. ]
  476. this.nullValueForm = {
  477. nullValueWay: 0,
  478. maxNullWay: 1
  479. }
  480. },
  481. cancelHandle() {
  482. this.init();
  483. this.$emit('update:isOpenDialog',false);
  484. },
  485. },
  486. mounted() {},
  487. };
  488. </script>
  489. <style lang="scss">
  490. .dynamic-differ-dialog {
  491. overflow: hidden;
  492. div::-webkit-scrollbar {
  493. width: 6px !important;
  494. }
  495. .el-dialog__body {
  496. max-height: 700px;
  497. overflow: auto;
  498. }
  499. .dialog-main {
  500. padding: 25px 42px 25px 25px;
  501. .el-cascader .el-input {
  502. width: 340px;
  503. }
  504. .add-cont {
  505. display: flex;
  506. flex-wrap: wrap;
  507. justify-content: space-between;
  508. .add-li {
  509. position: relative;
  510. margin-bottom: 48px;
  511. .li-tag {
  512. font-size: 16px;
  513. margin-right: 8px;
  514. }
  515. .del-tag {
  516. position: absolute;
  517. right: -30px;
  518. top: 12px;
  519. font-size: 16px;
  520. cursor: pointer;
  521. }
  522. .target-date {
  523. color: #5882ef;
  524. position: absolute;
  525. bottom: -25px;
  526. left: 24px;
  527. }
  528. }
  529. }
  530. .add-icon {
  531. font-size: 16px;
  532. color: #5882ef;
  533. cursor: pointer;
  534. }
  535. .computed-min {
  536. margin: 50px 0;
  537. .example-txt {
  538. display: block;
  539. margin-top: 10px;
  540. }
  541. .computed-section {
  542. display: flex;
  543. margin-top: 20px;
  544. }
  545. .label {
  546. padding:10px 10px 10px 0;
  547. }
  548. .formula-item {
  549. display: flex;
  550. align-items: center;
  551. margin-bottom: 15px;
  552. }
  553. .date-section-text {
  554. margin-left: 15px;
  555. }
  556. }
  557. }
  558. .dia-bot {
  559. padding-bottom: 40px;
  560. display: flex;
  561. justify-content: center;
  562. }
  563. }
  564. </style>