前端修改

This commit is contained in:
limengnan 2025-04-11 14:32:54 +08:00
parent 5128489bd7
commit 24570117bb
93 changed files with 27313 additions and 79 deletions

View File

@ -218,6 +218,9 @@ public class DatasetGroupManage {
if (ObjectUtils.isNotEmpty(request.getLeaf())) { if (ObjectUtils.isNotEmpty(request.getLeaf())) {
queryWrapper.eq("node_type", request.getLeaf() ? "dataset" : "folder"); queryWrapper.eq("node_type", request.getLeaf() ? "dataset" : "folder");
} }
if (ObjectUtils.isNotEmpty(request.getAppId())) {
queryWrapper.eq("app_id", request.getAppId());
}
String info = CommunityUtils.getInfo(); String info = CommunityUtils.getInfo();
if (StringUtils.isNotBlank(info)) { if (StringUtils.isNotBlank(info)) {
queryWrapper.notExists(String.format(info, "core_dataset_group.id")); queryWrapper.notExists(String.format(info, "core_dataset_group.id"));

View File

@ -2,13 +2,13 @@ export default {
server: { server: {
proxy: { proxy: {
'/api/f': { '/api/f': {
target: 'http://192.168.1.16:8100', target: 'http://192.168.1.38:8100',
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/api\/f/, '') rewrite: path => path.replace(/^\/api\/f/, '')
}, },
// 使用 proxy 实例 // 使用 proxy 实例
'/api': { '/api': {
target: 'http://192.168.1.16:8100', target: 'http://192.168.1.38:8100',
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '') rewrite: path => path.replace(/^\/api/, '')
} }

View File

@ -3,7 +3,7 @@
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "cross-env NODE_OPTIONS=--max_old_space_size=4096 vite --mode dev --host 0.0.0.0", "dev": "cross-env NODE_OPTIONS=--max_old_space_size=8196 vite --mode dev --host 0.0.0.0",
"build:flush": "cd ./flushbonading && rimraf ./demo.html && npm i && node ./index.js", "build:flush": "cd ./flushbonading && rimraf ./demo.html && npm i && node ./index.js",
"ts:check": "vue-tsc --noEmit", "ts:check": "vue-tsc --noEmit",
"build:base": "cross-env NODE_OPTIONS=--max_old_space_size=4096 vite build --mode base && npm run build:flush", "build:base": "cross-env NODE_OPTIONS=--max_old_space_size=4096 vite build --mode base && npm run build:flush",
@ -26,6 +26,7 @@
"@vueuse/core": "^9.13.0", "@vueuse/core": "^9.13.0",
"ace-builds": "^1.15.3", "ace-builds": "^1.15.3",
"axios": "^1.3.3", "axios": "^1.3.3",
"codemirror": "^6.0.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",

View File

@ -7,6 +7,7 @@ export interface DatasetOrFolder {
action?: string action?: string
id?: number | string id?: number | string
pid?: number | string pid?: number | string
appId?: number | string
nodeType: 'folder' | 'dataset' nodeType: 'folder' | 'dataset'
union?: Array<{}> union?: Array<{}>
allFields?: Array<{}> allFields?: Array<{}>
@ -151,8 +152,11 @@ export const perDelete = async (id): Promise<boolean> => {
}) })
} }
export const getDatasourceList = async (weight?: number): Promise<IResponse> => { export const getDatasourceList = async (weight: number,appId:any): Promise<IResponse> => {
const data = { busiFlag: 'datasource' } const data = {
busiFlag: 'datasource',
appId:appId
}
if (weight) { if (weight) {
data['weight'] = weight data['weight'] = weight
} }

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744253031439" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10299" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M42.666667 42.666667a42.666667 42.666667 0 0 1 42.666666-42.666667h853.333334a42.666667 42.666667 0 0 1 42.666666 42.666667v938.666666a42.666667 42.666667 0 0 1-42.666666 42.666667H85.333333a42.666667 42.666667 0 0 1-42.666666-42.666667V42.666667z m85.333333 42.666666v853.333334h768V85.333333H128z" p-id="10300"></path><path d="M213.333333 341.333333a42.666667 42.666667 0 0 1 42.666667-42.666666h512a42.666667 42.666667 0 1 1 0 85.333333H256a42.666667 42.666667 0 0 1-42.666667-42.666667zM213.333333 597.333333a42.666667 42.666667 0 0 1 42.666667-42.666666h341.333333a42.666667 42.666667 0 1 1 0 85.333333H256a42.666667 42.666667 0 0 1-42.666667-42.666667z" p-id="10301"></path></svg>

After

Width:  |  Height:  |  Size: 1020 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744252975601" class="icon" viewBox="0 0 1117 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2708" xmlns:xlink="http://www.w3.org/1999/xlink" width="218.1640625" height="200"><path d="M930.909091 139.636364H407.272727C377.018182 58.181818 300.218182 0 209.454545 0 109.381818 0 23.272727 72.145455 4.654545 167.563636 2.327273 181.527273 0 195.490909 0 209.454545v628.363637c0 102.4 83.781818 186.181818 186.181818 186.181818h744.727273c102.4 0 186.181818-83.781818 186.181818-186.181818V325.818182c0-102.4-83.781818-186.181818-186.181818-186.181818z m93.090909 186.181818v337.454545l-283.927273-100.072727L826.181818 232.727273H930.909091c51.2 0 93.090909 41.890909 93.090909 93.090909z m-293.236364-93.090909L651.636364 535.272727l-232.727273-81.454545V232.727273h311.854545zM93.090909 232.727273V209.454545c0-25.6 9.309091-51.2 23.272727-69.818181 20.945455-27.927273 53.527273-46.545455 93.090909-46.545455s72.145455 18.618182 93.09091 46.545455c13.963636 18.618182 23.272727 44.218182 23.272727 69.818181v449.163637c-32.581818-23.272727-74.472727-34.909091-116.363637-34.909091s-83.781818 13.963636-116.363636 34.909091V232.727273z m837.818182 698.181818H186.181818c-51.2 0-93.090909-41.890909-93.090909-93.090909v-6.981818c0-65.163636 51.2-116.363636 116.363636-116.363637s116.363636 51.2 116.363637 116.363637c0 25.6 20.945455 46.545455 46.545454 46.545454s46.545455-20.945455 46.545455-46.545454v-279.272728l605.090909 211.781819V837.818182c0 51.2-41.890909 93.090909-93.090909 93.090909z" fill="currentColor" p-id="2709"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744253069658" class="icon" viewBox="0 0 1073 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13333" xmlns:xlink="http://www.w3.org/1999/xlink" width="209.5703125" height="200"><path d="M881.784845 0H191.570683A191.697888 191.697888 0 0 0 0 191.570683v640.858634A191.697888 191.697888 0 0 0 191.570683 1024h690.214162a191.825093 191.825093 0 0 0 191.570683-191.570683V191.570683A191.825093 191.825093 0 0 0 881.784845 0z m24.677764 333.785839h79.503105V394.335404h-79.503105a43.631304 43.631304 0 1 0 0 87.262608h79.503105v60.549566h-79.503105a43.758509 43.758509 0 1 0 0 87.389813h79.503105v60.549566h-79.503105a43.631304 43.631304 0 1 0 0 87.262608h79.503105v54.952547a104.308075 104.308075 0 0 1-104.180869 104.308074H191.570683a104.43528 104.43528 0 0 1-104.308074-104.308074v-54.825342h79.63031a43.631304 43.631304 0 1 0 0-87.262609H87.262609V629.664596h79.63031a43.758509 43.758509 0 0 0 0-87.389813H87.262609v-60.549566h79.63031a43.631304 43.631304 0 1 0 0-87.262608H87.262609v-60.67677h79.63031a43.631304 43.631304 0 1 0 0-87.262609H87.262609v-54.952547a104.43528 104.43528 0 0 1 104.308074-104.308074h690.214162a104.308075 104.308075 0 0 1 104.180869 104.308074v54.952547h-79.503105a43.631304 43.631304 0 1 0 0 87.262609z" fill="currentColor" p-id="13334"></path><path d="M718.199255 437.076273l-233.929938-152.645962a75.559752 75.559752 0 0 0-78.103851-4.579379 88.280248 88.280248 0 0 0-43.631305 78.994285v305.291926a89.043478 89.043478 0 0 0 44.52174 79.63031 73.524472 73.524472 0 0 0 35.617391 9.158758 78.994286 78.994286 0 0 0 42.995279-12.720497l231.894659-152.645962a89.933913 89.933913 0 0 0 40.19677-76.322982 87.008199 87.008199 0 0 0-39.560745-74.160497z m-48.337889 73.906087a7.250683 7.250683 0 0 1 0 2.925715l-220.064596 144.504844v-292.571428l219.937391 143.86882a3.180124 3.180124 0 0 1 0.127205 1.272049z" fill="currentColor" p-id="13335"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744253004677" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5674" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M864 98H162a64.19 64.19 0 0 0-64 64v158a64.19 64.19 0 0 0 64 64h702a64.19 64.19 0 0 0 64-64V162a64.19 64.19 0 0 0-64-64z m-32 214H194a24 24 0 0 1-24-24v-94a24 24 0 0 1 24-24h638a24 24 0 0 1 24 24v94a24 24 0 0 1-24 24zM384 448H162a64.19 64.19 0 0 0-64 64v352a64.19 64.19 0 0 0 64 64h222a64.19 64.19 0 0 0 64-64V512a64.19 64.19 0 0 0-64-64z m-32 408H194a24 24 0 0 1-24-24V544a24 24 0 0 1 24-24h158a24 24 0 0 1 24 24v288a24 24 0 0 1-24 24z m539-387H533.71c-20.19 0-37.09 16.52-36.7 36.7A36 36 0 0 0 533 541h357.29c20.19 0 37.09-16.52 36.7-36.7A36 36 0 0 0 891 469z m0 367H533.71c-20.19 0-37.09 16.52-36.7 36.7A36 36 0 0 0 533 908h357.29c20.19 0 37.09-16.52 36.7-36.7A36 36 0 0 0 891 836z m0-183.5H533.71c-20.19 0-37.09 16.52-36.7 36.7a36 36 0 0 0 36 35.3h357.28c20.19 0 37.09-16.52 36.7-36.7A36 36 0 0 0 891 652.5z" p-id="5675"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744253055932" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12270" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M206.791111 76.572444c38.058667 0 68.892444 30.890667 68.892445 68.949334v2.844444h428.828444V217.315556H275.683556v4.835555c0 38.058667-30.833778 68.892444-68.892445 68.892445h-15.36v398.222222h15.36c38.058667 0 68.892444 30.890667 68.892445 68.949333v4.721778h428.828444v68.949333H275.683556v2.901334c0 38.115556-30.833778 68.949333-68.892445 68.949333H130.161778c-38.058667 0-68.892444-30.890667-68.892445-68.949333v-76.572445c0-38.115556 30.833778-68.949333 68.892445-68.949333h-7.623111v-398.222222h7.623111c-38.058667 0-68.892444-30.833778-68.892445-68.892445V145.521778c0-38.115556 30.833778-68.949333 68.892445-68.949334h76.629333z m566.670222 214.471112c-38.058667 0-68.892444-30.890667-68.892444-68.949334V145.521778c0-38.115556 30.833778-68.949333 68.892444-68.949334h76.572445c38.115556 0 68.949333 30.890667 68.949333 68.949334v76.572444c0 38.058667-30.890667 68.892444-68.949333 68.892445h7.68v398.222222h-7.68c38.115556 0 68.949333 30.890667 68.949333 68.949333v76.572445c0 38.115556-30.890667 68.949333-68.949333 68.949333h-76.572445c-38.058667 0-68.892444-30.890667-68.892444-68.949333v-76.572445c0-38.115556 30.833778-68.949333 68.892444-68.949333h15.303111v-398.222222h-15.303111z m-566.670222 467.057777H130.161778v76.629334h76.629333v-76.572445z m643.242667 0h-76.572445v76.629334h76.572445v-76.572445z m-241.208889-424.96a34.474667 34.474667 0 0 1 0 68.892445h-84.764445v237.397333a34.474667 34.474667 0 0 1-68.835555 0L455.111111 402.033778H371.427556a34.474667 34.474667 0 0 1 0-68.892445h237.397333zM206.791111 145.521778H130.161778v76.572444h76.629333V145.521778z m643.242667 0h-76.572445v76.572444h76.572445V145.521778z" fill="currentColor" p-id="12271"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744253082830" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14326" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1008.241778 940.714667c0 20.992-18.432 38.115556-40.049778 38.115555H62.179556A38.912 38.912 0 0 1 22.755556 940.714667C22.755556 919.665778 40.561778 902.542222 62.179556 902.542222H968.248889c21.617778 0 40.049778 17.180444 40.049778 38.115556" fill="currentColor" p-id="14327"></path><path d="M896.341333 190.407111c22.243556 0 40.049778 17.180444 40.049778 38.115556v578.56c0 21.048889-17.806222 38.229333-40.049778 38.229333h-166.570666a38.968889 38.968889 0 0 1-39.424-38.172444V228.579556c0-21.048889 17.806222-38.172444 39.424-38.172445h166.570666z m-127.146666 578.56h87.722666v-502.328889h-87.722666v502.328889zM598.812444 323.925333c21.617778 0 39.424 17.180444 39.424 38.115556v445.098667a38.968889 38.968889 0 0 1-39.424 38.115555h-167.253333a38.968889 38.968889 0 0 1-39.367111-38.115555V362.097778c0-21.048889 17.806222-38.172444 39.367111-38.172445h167.253333z m-127.146666 445.098667h87.096889V400.270222H471.665778v368.753778zM300.600889 56.888889c21.617778 0 40.049778 17.180444 40.049778 38.115555v712.135112c0 20.992-18.432 38.115556-40.049778 38.115555H133.404444a38.968889 38.968889 0 0 1-39.424-38.115555V95.004444c0-21.048889 17.806222-38.172444 39.424-38.172444h167.196445zM173.454222 769.024H260.551111V133.12H173.454222v635.847111z" fill="currentColor" p-id="14328"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744253044063" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11273" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M841.34 959.36H182.66c-65.06 0-117.99-52.94-117.99-118.02V182.69c0-65.08 52.94-118.04 117.99-118.04h658.68c65.06 0 117.99 52.96 117.99 118.04v658.65c0 65.08-52.93 118.02-117.99 118.02zM182.66 142.17c-22.31 0-40.51 18.18-40.51 40.51v658.65c0 22.34 18.2 40.49 40.51 40.49h658.68c22.31 0 40.51-18.15 40.51-40.49V182.69c0-22.34-18.2-40.51-40.51-40.51H182.66z" fill="#6C6D6E" p-id="11274"></path></svg>

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

View File

@ -1,16 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import dvFilter from '@/assets/svg/dv-filter.svg' import dvFilter from '@/assets/svg/dv-filter.svg'
import dvMaterial from '@/assets/svg/dv-material.svg' import dvMaterial from '@/assets/svg/dv-material.svg'
import dvMedia from '@/assets/svg/dv-media.svg' import dvMedia from '@/assets/newimg/camvassvg/dv-media.svg' //
import dvMap from '@/assets/newimg/camvassvg/dv-map.svg' //
import dvMoreCom from '@/assets/svg/dv-more-com.svg' import dvMoreCom from '@/assets/svg/dv-more-com.svg'
import dvTab from '@/assets/svg/dv-tab.svg' import dvTab from '@/assets/svg/dv-tab.svg'
import dvText from '@/assets/svg/dv-text.svg' import dvText from '@/assets/newimg/camvassvg/dv-text.svg' //
import dvView from '@/assets/svg/dv-view.svg' import dvView from '@/assets/newimg/camvassvg/dv-view.svg' // echarts
import dvForm from '@/assets/newimg/camvassvg/dv-form.svg' // echarts
import dvTemplate from '@/assets/newimg/camvassvg/dv-template.svg' //
// import dvView from '@/assets/newimg/dvCanvas/u4876.png'
import icon_params_setting from '@/assets/svg/icon_params_setting.svg' import icon_params_setting from '@/assets/svg/icon_params_setting.svg'
import icon_copy_filled from '@/assets/svg/icon_copy_filled.svg' import icon_copy_filled from '@/assets/svg/icon_copy_filled.svg'
import icon_left_outlined from '@/assets/svg/icon_left_outlined.svg' import icon_left_outlined from '@/assets/svg/icon_left_outlined.svg'
import icon_undo_outlined from '@/assets/svg/icon_undo_outlined.svg' // import icon_undo_outlined from '@/assets/svg/icon_undo_outlined.svg'
import icon_redo_outlined from '@/assets/svg/icon_redo_outlined.svg' // import icon_redo_outlined from '@/assets/svg/icon_redo_outlined.svg'
import { ElMessage, ElMessageBox } from 'element-plus-secondary' import { ElMessage, ElMessageBox } from 'element-plus-secondary'
import eventBus from '@/utils/eventBus' import eventBus from '@/utils/eventBus'
import { ref, nextTick, computed, toRefs, onBeforeUnmount, onMounted } from 'vue' import { ref, nextTick, computed, toRefs, onBeforeUnmount, onMounted } from 'vue'
@ -327,7 +341,7 @@ const fullScreenPreview = () => {
<span id="dv-canvas-name" class="name-area" @dblclick="editCanvasName"> <span id="dv-canvas-name" class="name-area" @dblclick="editCanvasName">
{{ dvInfo.name }} {{ dvInfo.name }}
</span> </span>
<div class="opt-area"> <!-- <div class="opt-area">
<el-tooltip effect="ndark" :content="$t('visualization.undo')" placement="bottom"> <el-tooltip effect="ndark" :content="$t('visualization.undo')" placement="bottom">
<el-icon <el-icon
class="toolbar-hover-icon" class="toolbar-hover-icon"
@ -348,11 +362,22 @@ const fullScreenPreview = () => {
<Icon name="icon_redo_outlined"><icon_redo_outlined class="svg-icon" /></Icon> <Icon name="icon_redo_outlined"><icon_redo_outlined class="svg-icon" /></Icon>
</el-icon> </el-icon>
</el-tooltip> </el-tooltip>
</div> </div> -->
</div> </div>
<div class="middle-area"> <div class="middle-area">
<div class="undo-redo" @click="undo()">
<img src="@/assets/newimg/dvCanvas/u4860.png" alt="" srcset="">
<div class="undo-redo-text">撤销</div>
</div>
<div class="undo-redo" @click="redo()">
<img src="@/assets/newimg/dvCanvas/u4867.png" alt="" srcset="">
<div class="undo-redo-text">重做</div>
</div>
<div class="tabline"></div>
<component-group <component-group
show-split-line
is-label is-label
:base-width="410" :base-width="410"
:icon-name="dvView" :icon-name="dvView"
@ -360,9 +385,35 @@ const fullScreenPreview = () => {
> >
<user-view-group></user-view-group> <user-view-group></user-view-group>
</component-group> </component-group>
<component-group
is-label
placement="bottom"
:base-width="328"
:icon-name="dvMedia"
:title="t('visualization.media')"
>
<media-group></media-group>
</component-group>
<component-group
is-label
placement="bottom"
:base-width="328"
:icon-name="dvMap"
:title="'地图'"
>
<media-group></media-group>
</component-group>
<component-group <component-group
:base-width="115" :base-width="115"
:show-split-line="true"
is-label is-label
:icon-name="dvFilter" :icon-name="dvFilter"
:title="t('visualization.query_component')" :title="t('visualization.query_component')"
@ -377,15 +428,7 @@ const fullScreenPreview = () => {
> >
<text-group></text-group> <text-group></text-group>
</component-group> </component-group>
<component-group
is-label
placement="bottom"
:base-width="328"
:icon-name="dvMedia"
:title="t('visualization.media')"
>
<media-group></media-group>
</component-group>
<component-group is-label :base-width="115" :icon-name="dvTab" title="Tab"> <component-group is-label :base-width="115" :icon-name="dvTab" title="Tab">
<tabs-group :dv-model="dvModel"></tabs-group> <tabs-group :dv-model="dvModel"></tabs-group>
</component-group> </component-group>
@ -406,12 +449,33 @@ const fullScreenPreview = () => {
> >
<common-group></common-group> <common-group></common-group>
</component-group> </component-group>
<component-button-label <component-button-label
:icon-name="icon_copy_filled" :icon-name="icon_copy_filled"
:title="t('visualization.multiplexing')" :title="t('visualization.multiplexing')"
is-label is-label
@customClick="multiplexingCanvasOpen" @customClick="multiplexingCanvasOpen"
></component-button-label> ></component-button-label>
<component-group
is-label
placement="bottom"
:base-width="328"
:icon-name="dvForm"
:title="'表单'"
>
<media-group></media-group>
</component-group>
<component-group
is-label
placement="bottom"
:base-width="328"
:icon-name="dvTemplate"
:title="'模版'"
>
<media-group></media-group>
</component-group>
</div> </div>
</template> </template>
<div class="right-area"> <div class="right-area">
@ -508,7 +572,7 @@ const fullScreenPreview = () => {
height: @top-bar-height; height: @top-bar-height;
white-space: nowrap; white-space: nowrap;
overflow-x: auto; overflow-x: auto;
background: #1a1a1a; background: rgb(37,38,38);
color: #ffffff; color: #ffffff;
box-shadow: 0px 2px 4px 0px rgba(31, 35, 41, 0.12); box-shadow: 0px 2px 4px 0px rgba(31, 35, 41, 0.12);
display: flex; display: flex;
@ -526,13 +590,18 @@ const fullScreenPreview = () => {
flex-direction: column; flex-direction: column;
.name-area { .name-area {
position: relative; position: relative;
line-height: 24px; line-height: 46px;
height: 24px; height: 46px;
font-size: 16px; font-size: 16px;
width: 300px; width: 300px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
color: @dv-canvas-main-font-color; color: @dv-canvas-main-font-color;
font-weight: 700;
font-style: normal;
font-size: 16px;
color: #F2F4F5;
input { input {
position: absolute; position: absolute;
left: 0; left: 0;
@ -603,4 +672,35 @@ const fullScreenPreview = () => {
margin-right: 20px; margin-right: 20px;
margin-left: 10px; margin-left: 10px;
} }
.undo-redo{
width: 47px;
height: 47px;
display: flex;
justify-content: center;
// items-content: center;
align-content: center;
flex-wrap: wrap;
border-radius: 3px;
cursor: pointer;
.undo-redo-text{
width: 100%;
font-family: '微软雅黑';
font-weight: 400;
font-style: normal;
font-size: 12px;
color: #F2F4F5;
text-align: center;
padding-top: 5px;
}
}
.undo-redo:hover{
background-color: rgba(54, 55, 56, 1);
}
.tabline{
width: 1px;
height: 20px;
background-color: rgba(24, 24, 24, 1);
margin: 0px 10px;
}
</style> </style>

View File

@ -20,7 +20,7 @@ withDefaults(
inTable?: boolean inTable?: boolean
}>(), }>(),
{ {
placement: 'bottom-end', placement: 'right-start',
iconSize: '16px', iconSize: '16px',
inTable: false inTable: false
} }
@ -53,9 +53,9 @@ const emit = defineEmits(['handleCommand'])
:key="ele.label" :key="ele.label"
:disabled="ele.disabled" :disabled="ele.disabled"
> >
<el-icon class="handle-icon" :style="{ fontSize: iconSize }" v-if="ele.svgName"> <!-- <el-icon class="handle-icon" :style="{ fontSize: iconSize }" v-if="ele.svgName">
<Icon><component class="svg-icon" :is="ele.svgName"></component></Icon> <Icon><component class="svg-icon" :is="ele.svgName"></component></Icon>
</el-icon> </el-icon> -->
{{ ele.label }} {{ ele.label }}
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>

View File

@ -21,6 +21,8 @@ const emits = defineEmits(['customClick'])
<el-row class="group_icon" :title="tips" @click="emits('customClick')"> <el-row class="group_icon" :title="tips" @click="emits('customClick')">
<el-col :span="24" class="group_inner" :class="{ 'inner-active': active }"> <el-col :span="24" class="group_inner" :class="{ 'inner-active': active }">
<Icon><component class="svg-icon toolbar-icon" :is="iconName"></component></Icon> <Icon><component class="svg-icon toolbar-icon" :is="iconName"></component></Icon>
<!-- <img src="@/assets/newimg/avatar.png" alt="" srcset=""> -->
<span >{{ title }}</span> <span >{{ title }}</span>
</el-col> </el-col>
</el-row> </el-row>
@ -34,18 +36,18 @@ const emits = defineEmits(['customClick'])
} }
} }
.group_inner { .group_inner {
padding: 8px; padding: 0px 10px 4px;
display: flex; display: flex;
cursor: pointer; cursor: pointer;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
border-radius: 4px; border-radius: 4px;
color: #a6a6a6; color: #ffffff;
span { span {
float: left; float: left;
font-size: 12px; font-size: 12px;
margin-top: 8px; margin-top: 5px;
line-height: 12px; line-height: 12px;
} }
&:hover { &:hover {

View File

@ -3308,7 +3308,8 @@ defineExpose({
} }
.query-condition-list { .query-condition-list {
height: 100%; height: 100%;
background: #f5f6f7; // background: #f5f6f7;
background: rgb(38,38,38);
border-right: 1px solid #dee0e3; border-right: 1px solid #dee0e3;
width: 208px; width: 208px;
overflow-y: auto; overflow-y: auto;
@ -3377,7 +3378,7 @@ defineExpose({
position: sticky; position: sticky;
top: 0; top: 0;
justify-content: space-between; justify-content: space-between;
background: #fff; background: transparent;
z-index: 5; z-index: 5;
.ed-radio { .ed-radio {
height: 20px; height: 20px;
@ -3497,7 +3498,7 @@ defineExpose({
position: sticky; position: sticky;
top: 0; top: 0;
justify-content: space-between; justify-content: space-between;
background: #fff; background: transparent;
z-index: 5; z-index: 5;
.ed-checkbox { .ed-checkbox {
height: 20px; height: 20px;

View File

@ -1,6 +1,7 @@
export interface BusiTreeNode { export interface BusiTreeNode {
id: string | number id: string | number
pid: string | number pid: string | number
appId: string | number
name: string name: string
leaf?: boolean leaf?: boolean
weight: number weight: number
@ -10,6 +11,7 @@ export interface BusiTreeNode {
} }
export interface BusiTreeRequest { export interface BusiTreeRequest {
appId?: string
busiFlag?: string busiFlag?: string
leaf?: boolean leaf?: boolean
weight?: number weight?: number

View File

@ -25,7 +25,7 @@ const { loadStart, loadDone } = usePageLoading()
const whiteList = ['/login', '/de-link', '/chart-view', '/admin-login', '/401'] // 不重定向白名单 const whiteList = ['/login', '/de-link', '/chart-view', '/admin-login', '/401'] // 不重定向白名单
const embeddedWindowWhiteList = ['/dvCanvas', '/dashboard', '/preview', '/dataset-embedded-form'] const embeddedWindowWhiteList = ['/dvCanvas', '/dashboard', '/preview', '/dataset-embedded-form']
const embeddedRouteWhiteList = ['/dataset-embedded', '/dataset-form', '/dataset-embedded-form'] const embeddedRouteWhiteList = ['/dataset-embedded', '/dataset-form','/datasetForm', '/dataset-embedded-form']
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
start() start()
loadStart() loadStart()
@ -138,6 +138,10 @@ router.beforeEach(async (to, from, next) => {
next({ path: '/dataset-embedded-form', query: to.query }) next({ path: '/dataset-embedded-form', query: to.query })
return return
} }
if (to.path.includes('/datasetForm')) {
next({ path: '/datasetEmbeddedForm', query: to.query })
return
}
permissionStore.setCurrentPath(to.path) permissionStore.setCurrentPath(to.path)
next() next()
} else if ( } else if (

View File

@ -1,8 +1,12 @@
import { isExternal } from '@/utils/validate' import { isExternal } from '@/utils/validate'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { XpackComponent } from '@/components/plugin' import { XpackComponent } from '@/components/plugin'
// const modules = import.meta.glob('../viewsnew/**/*.vue')
// export const Layout = () => import('@/viewsnew/layout/index.vue')
const modules = import.meta.glob('../views/**/*.vue') const modules = import.meta.glob('../views/**/*.vue')
export const Layout = () => import('@/layout/index.vue') export const Layout = () => import('@/layout/index.vue')
const xpackComName = 'components/plugin' const xpackComName = 'components/plugin'
export const LayoutTransition = () => import('@/layout/components/LayoutTransition.vue') export const LayoutTransition = () => import('@/layout/components/LayoutTransition.vue')
// 后端控制路由生成 // 后端控制路由生成
@ -33,13 +37,13 @@ export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRe
meta: route.meta, meta: route.meta,
props: route.props as Recordable props: route.props as Recordable
} }
if (route.component) { if (route.component) {
let comModule = null let comModule = null
if (route.component === xpackComName) { if (route.component === xpackComName) {
comModule = XpackComponent comModule = XpackComponent
} else if (!route.component.startsWith('Layout')) { } else if (!route.component.startsWith('Layout')) {
comModule = modules[`../views/${route.component}/index.vue`] comModule = modules[`../views/${route.component}/index.vue`]
// comModule = modules[`../viewsnew/${route.component}/index.vue`]
} }
if (route.component === 'Layout') { if (route.component === 'Layout') {

View File

@ -177,6 +177,27 @@ export const routes: AppRouteRecordRaw[] = [
hidden: true, hidden: true,
meta: {}, meta: {},
component: () => import('@/viewsnew/application/index.vue') component: () => import('@/viewsnew/application/index.vue')
},
{
path: '/datasetnew',
name: 'datasetnew',
hidden: true,
meta: {},
component: () => import('@/viewsnew/application/service/dataset/index.vue')
},
{
path: '/datasetForm',
name: 'datasetForm',
hidden: true,
meta: {},
component: () => import('@/viewsnew/application/service/dataset/form/index.vue')
},
{
path: '/datasourcenew',
name: 'datasourcenew',
hidden: true,
meta: {},
component: () => import('@/viewsnew/application/service/datasource/index.vue')
} }
] ]

View File

@ -1,5 +1,7 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { store } from '@/store' import { store } from '@/store'
import { ref} from 'vue'
import { queryTreeApi, queryBusiTreeApi } from '@/api/visualization/dataVisualization' import { queryTreeApi, queryBusiTreeApi } from '@/api/visualization/dataVisualization'
import { getDatasetTree } from '@/api/dataset' import { getDatasetTree } from '@/api/dataset'
import { listDatasources } from '@/api/datasource' import { listDatasources } from '@/api/datasource'
@ -7,6 +9,8 @@ import type { BusiTreeRequest, BusiTreeNode } from '@/models/tree/TreeNode'
import { pathValid } from '@/store/modules/permission' import { pathValid } from '@/store/modules/permission'
import { useCache } from '@/hooks/web/useCache' import { useCache } from '@/hooks/web/useCache'
import { useAppStoreWithOut } from '@/store/modules/app' import { useAppStoreWithOut } from '@/store/modules/app'
import { useRoute } from 'vue-router'
const appStore = useAppStoreWithOut() const appStore = useAppStoreWithOut()
const { wsCache } = useCache() const { wsCache } = useCache()
export interface InnerInteractive { export interface InnerInteractive {
@ -58,6 +62,7 @@ export const interactiveStore = defineStore('interactive', {
menuAuth: false menuAuth: false
} }
this.data[flag] = tempData this.data[flag] = tempData
if (flag === 0) { if (flag === 0) {
wsCache.set('panel-weight', {}) wsCache.set('panel-weight', {})
} }
@ -68,7 +73,13 @@ export const interactiveStore = defineStore('interactive', {
} }
let res = resParam let res = resParam
if (!resParam) { if (!resParam) {
const route = useRoute()
const appId:any = ref('')
if (route.query.id) {
appId.value = route.query.id
}
const method = apiMap[flag] const method = apiMap[flag]
param.appId = appId.value
res = await method(param) res = await method(param)
} }
this.data[flag] = convertInteractive(res) this.data[flag] = convertInteractive(res)

View File

@ -46,9 +46,38 @@ export const usePermissionStore = defineStore('permission', {
}, },
generateRoutes(routers?: AppCustomRouteRecordRaw[] | string[]): Promise<unknown> { generateRoutes(routers?: AppCustomRouteRecordRaw[] | string[]): Promise<unknown> {
return new Promise<void>(resolve => { return new Promise<void>(resolve => {
let aaaa= [{
children: null,
component: "application",
hidden: false,
inLayout: true,
meta:{
icon: null,
title: "平台项目"
} ,
name: "application",
path: "/application",
plugin: false,
redirect: null,
top: true
},{
children: null,
component: "visualized/view/panel",
hidden: false,
inLayout: true,
meta:{
icon: null,
title: "仪表板"
} ,
name: "panel",
path: "/panel",
plugin: false,
redirect: null,
top: true
}]
let routerMap: AppRouteRecordRaw[] = [] let routerMap: AppRouteRecordRaw[] = []
routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[]) || [] routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[]) || []
this.addRouters = routerMap.concat([ this.addRouters = routerMap.concat([
{ {
path: '/:catchAll(.*)', path: '/:catchAll(.*)',

View File

@ -194,3 +194,32 @@
.vjs-custom-skin > .video-js .vjs-control-bar .vjs-fullscreen-control { .vjs-custom-skin > .video-js .vjs-control-bar .vjs-fullscreen-control {
order: 6; order: 6;
} }
.ed-dialog {
--ed-dialog-bg-color: rgba(33, 33, 33, 1);
}
.ed-dialog__title{
font-family: 'Arial Negreta', 'Arial Normal', 'Arial';
font-weight: 700;
font-style: normal;
font-size: 16px;
color: #F2F4F5;
}
.ed-form-item__label{
font-family: 'Arial Normal', 'Arial';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #D2D2D2;
}
.ed-pagination__total{
color: #fff;
}
.ed-dialog__body{
color: #fff;
}

View File

@ -172,7 +172,7 @@ body {
width: 24px !important; width: 24px !important;
font-size: 16px !important; font-size: 16px !important;
border-radius: 4px; border-radius: 4px;
color: #646a73 !important; color: #ffffff !important;
&[aria-expanded='true'] { &[aria-expanded='true'] {
background: rgba(31, 35, 41, 0.1); background: rgba(31, 35, 41, 0.1);
@ -671,3 +671,28 @@ strong {
.ed-main{ .ed-main{
overflow: hidden !important; overflow: hidden !important;
} }
// 新样式
.ed-drawer{
background: rgba(33, 33, 33, 1) !important;
}
.ed-drawer__footer{
background: rgba(33, 33, 33, 1) !important;
}
.ed-dialog__body{
color: #fff;
}
.ed-radio__label{
color: #fff ;
}
.ed-radio__input.is-checked+.ed-radio__label{
color: #fff !important;
}
.ed-checkbox__label{
color: #fff !important;
}
.ed-checkbox__label:hover{
color: #fff ;
}

View File

@ -17,3 +17,33 @@
right: -@width; right: -@width;
} }
} }
:deep(.ed-pagination__total){
color: #fff;
}
:deep(.ed-pagination__goto){
color: #fff;
}
:deep(.ed-dialog__body){
color: #fff;
}
:deep(.ed-checkbox__label){
color: #fff;
}
:deep(.ed-radio__label){
color: #fff ;
}
// :deep(.ed-checkbox__label){
// color: #fff !important;
// }
// :deep(.is-checked){
// :deep(.ed-radio__label){
// color: #fff;
// }
// }

View File

@ -5250,7 +5250,7 @@ span {
position: relative; position: relative;
line-height: 24px; line-height: 24px;
height: 24px; height: 24px;
font-size: 14px !important; font-size: 14px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
input { input {

View File

@ -263,6 +263,7 @@ const getNodeField = ({ datasourceId, tableName }) => {
} }
const getDatasource = () => { const getDatasource = () => {
debugger
getDatasourceList().then(res => { getDatasourceList().then(res => {
const _list = (res as unknown as DataSource[]) || [] const _list = (res as unknown as DataSource[]) || []
if (_list && _list.length > 0 && _list[0].id === '0') { if (_list && _list.length > 0 && _list[0].id === '0') {

View File

@ -66,6 +66,11 @@ import { cloneDeep, debounce } from 'lodash-es'
import { XpackComponent } from '@/components/plugin' import { XpackComponent } from '@/components/plugin'
import { iconFieldMap } from '@/components/icon-group/field-list' import { iconFieldMap } from '@/components/icon-group/field-list'
import { iconDatasourceMap } from '@/components/icon-group/datasource-list' import { iconDatasourceMap } from '@/components/icon-group/datasource-list'
const route = useRoute()
const appId:any = ref('')
if (route.query.appId) {
appId.value = route.query.appId
}
interface DragEvent extends MouseEvent { interface DragEvent extends MouseEvent {
dataTransfer: DataTransfer dataTransfer: DataTransfer
} }
@ -81,7 +86,6 @@ const { wsCache } = useCache()
const appStore = useAppStoreWithOut() const appStore = useAppStoreWithOut()
const embeddedStore = useEmbedded() const embeddedStore = useEmbedded()
const { t } = useI18n() const { t } = useI18n()
const route = useRoute()
const { push } = useRouter() const { push } = useRouter()
const quotaTableHeight = ref(238) const quotaTableHeight = ref(238)
const creatDsFolder = ref() const creatDsFolder = ref()
@ -1332,7 +1336,7 @@ const getSqlResultHeight = () => {
sqlResultHeight.value = (document.querySelector('.sql-result') as HTMLElement).offsetHeight sqlResultHeight.value = (document.querySelector('.sql-result') as HTMLElement).offsetHeight
} }
const getDatasource = (weight?: number) => { const getDatasource = (weight?: number) => {
getDatasourceList(weight).then(res => { getDatasourceList(weight,appId.value).then(res => {
const _list = (res as unknown as DataSource[]) || [] const _list = (res as unknown as DataSource[]) || []
if (_list && _list.length > 0 && _list[0].id === '0') { if (_list && _list.length > 0 && _list[0].id === '0') {
state.dataSourceList = dfsChild(_list[0].children) state.dataSourceList = dfsChild(_list[0].children)

View File

@ -66,7 +66,7 @@ const handleChange = (val: boolean) => {
</el-icon> </el-icon>
<div class="info"> <div class="info">
<p class="name">{{ $t('auth.dataset') }}</p> <p class="name">{{ $t('auth.dataset') }}</p>
<p class="size">{{ t('data_source.or_large_screen') }}</p> <p class="size">{{ t('data_source.or_large_screen') }}1</p>
</div> </div>
<el-button class="create" secondary :disabled="disabled" @click="createDataset"> <el-button class="create" secondary :disabled="disabled" @click="createDataset">
{{ t('data_source.go_to_create') }} {{ t('data_source.go_to_create') }}

View File

@ -46,22 +46,22 @@ function updateClick(row:any){
paramsObj.value = row paramsObj.value = row
isAppPopUp.value = true isAppPopUp.value = true
} }
function routerClick(item){ function routerClick(item,path){
router.push({ router.push({
path: '/module', path: path,
query: { query: {
id: item.id id: item.id
} }
}) })
} }
function delClick(id){ function delClick(row){
ElMessageBox.confirm('您确定删除该项目模块及内容吗?', { ElMessageBox.confirm('您确定删除'+ row.name+'项目及所有模块吗?', {
confirmButtonType: 'primary', confirmButtonType: 'primary',
type: 'warning', type: 'warning',
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
}).then(() => { }).then(() => {
applicationDel(id).then(res => { applicationDel(row.id).then(res => {
if(res.data.code == '0'){ if(res.data.code == '0'){
getDatasetList() getDatasetList()
ElMessage.success('删除成功') ElMessage.success('删除成功')
@ -107,16 +107,17 @@ function delClick(id){
<div class="mask_box_img"> <div class="mask_box_img">
<img src="@/assets/newimg/icon/edit.png" @click="updateClick(item)" title="编辑项目"> <img src="@/assets/newimg/icon/edit.png" @click="updateClick(item)" title="编辑项目">
<img src="@/assets/newimg/icon/caidan.png" alt="" title="菜单配置"> <img src="@/assets/newimg/icon/caidan.png" alt="" title="菜单配置">
<img src="@/assets/newimg/icon/edit.png" alt="" title="编辑数据"> <img src="@/assets/newimg/icon/edit.png" alt="" title="编辑数据集" @click="routerClick(item,'/datasetnew')">
<img src="@/assets/newimg/icon/edit.png" alt="" title="编辑数据源" @click="routerClick(item,'/datasourcenew')">
<img src="@/assets/newimg/icon/fuwu.png" alt="" title="服务配置"> <img src="@/assets/newimg/icon/fuwu.png" alt="" title="服务配置">
<img src="@/assets/newimg/icon/permission.png" alt="" title="权限设置"> <img src="@/assets/newimg/icon/permission.png" alt="" title="权限设置">
<img src="@/assets/newimg/icon/export.png" alt="" title="导出"> <img src="@/assets/newimg/icon/export.png" alt="" title="导出">
<img src="@/assets/newimg/icon/release.png" alt="" title="发布"> <img src="@/assets/newimg/icon/release.png" alt="" title="发布">
<img src="@/assets/newimg/icon/del.png" alt="" title="删除" @click="delClick(item.id)"> <img src="@/assets/newimg/icon/del.png" alt="" title="删除" @click="delClick(item)">
</div> </div>
<div style="display: flex;justify-content: center;"> <div style="display: flex;justify-content: center;">
<div class="yulan">预览</div> <div class="yulan">预览</div>
<div class="mokuaipeizhi" @click="routerClick(item)">模块配置</div> <div class="mokuaipeizhi" @click="routerClick(item,'/module')">模块配置</div>
</div> </div>
</div> </div>
</div> </div>
@ -284,3 +285,9 @@ function delClick(id){
cursor: pointer; cursor: pointer;
} }
</style> </style>
<style>
.ed-message-box__headerbtn{
top: -15px;
right: -10px;
}
</style>

View File

@ -0,0 +1,60 @@
<script lang="ts" setup>
import { useI18n } from '@/hooks/web/useI18n'
export interface Info {
name: string
value: string
}
defineProps({
creator: {
type: String,
default: ''
},
createTime: {
type: String,
default: ''
}
})
const { t } = useI18n()
</script>
<template>
<div class="info-card">
<div class="info-title">
{{ t('dataset.create_by') }}
</div>
<div class="info-content">
{{ creator }}
</div>
<div class="info-title">
{{ t('dataset.create_time') }}
</div>
<div class="info-content">
{{ createTime }}
</div>
</div>
</template>
<style lang="less" scoped>
.info-card {
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
padding-left: 4px;
font-weight: 400;
line-height: 22px;
.info-title {
color: #646a73;
font-size: 14px;
margin-bottom: 4px;
}
.info-content {
color: #1f2329;
font-size: 14px;
margin-bottom: 12px;
}
:last-child {
margin-bottom: 0;
}
}
</style>

View File

@ -0,0 +1,582 @@
<script lang="ts" setup>
import dvPreviewDownload from '@/assets/svg/icon_download_outlined.svg'
import deDelete from '@/assets/svg/de-delete.svg'
import icon_fileExcel_colorful from '@/assets/svg/icon_file-excel_colorful.svg'
import icon_refresh_outlined from '@/assets/svg/icon_refresh_outlined.svg'
import { ref, h, onUnmounted, computed } from 'vue'
import { EmptyBackground } from '@/components/empty-background'
import { ElButton, ElMessage, ElMessageBox, ElTabPane, ElTabs } from 'element-plus-secondary'
import { RefreshLeft } from '@element-plus/icons-vue'
import {
exportTasks,
exportRetry,
exportDelete,
exportDeleteAll,
exportDeletePost
} from '@/api/dataset'
import { useI18n } from '@/hooks/web/useI18n'
import { useEmitt } from '@/hooks/web/useEmitt'
import Icon from '@/components/icon-custom/src/Icon.vue'
import { useCache } from '@/hooks/web/useCache'
import { useLinkStoreWithOut } from '@/store/modules/link'
import { useAppStoreWithOut } from '@/store/modules/app'
const { t } = useI18n()
const tableData = ref([])
const drawerLoading = ref(false)
const drawer = ref(false)
const msgDialogVisible = ref(false)
const msg = ref('')
const activeName = ref('ALL')
const multipleSelection = ref([])
const description = ref(t('data_set.no_tasks_yet'))
const tabList = ref([
{
label: t('data_set.exporting') + '(0)',
name: 'IN_PROGRESS'
},
{
label: t('data_set.success') + '(0)',
name: 'SUCCESS'
},
{
label: t('data_set.fail') + '(0)',
name: 'FAILED'
},
{
label: t('data_set.waiting') + '(0)',
name: 'PENDING'
},
{
label: t('data_set.all') + '(0)',
name: 'ALL'
}
])
let timer
const handleClose = () => {
drawer.value = false
clearInterval(timer)
}
const { wsCache } = useCache()
const openType = wsCache.get('open-backend') === '1' ? '_self' : '_blank'
const xpack = wsCache.get('xpack-model-distributed')
const desktop = wsCache.get('app.desktop')
onUnmounted(() => {
clearInterval(timer)
})
const handleClick = tab => {
if (tab) {
activeName.value = tab.paneName
}
if (activeName.value === 'ALL') {
description.value = t('data_export.no_file')
} else if (activeName.value === 'FAILED') {
description.value = t('data_export.no_failed_file')
} else {
description.value = t('data_export.no_task')
}
drawerLoading.value = true
exportTasks(activeName.value)
.then(res => {
tabList.value.forEach(item => {
if (item.name === 'ALL') {
item.label = t('data_set.all') + '(' + res.data.length + ')'
}
if (item.name === 'IN_PROGRESS') {
item.label =
t('data_set.exporting') +
'(' +
res.data.filter(task => task.exportStatus === 'IN_PROGRESS').length +
')'
}
if (item.name === 'SUCCESS') {
item.label =
t('data_set.success') +
'(' +
res.data.filter(task => task.exportStatus === 'SUCCESS').length +
')'
}
if (item.name === 'FAILED') {
item.label =
t('data_set.fail') +
'(' +
res.data.filter(task => task.exportStatus === 'FAILED').length +
')'
}
if (item.name === 'PENDING') {
item.label =
t('data_set.waiting') +
'(' +
res.data.filter(task => task.exportStatus === 'PENDING').length +
')'
}
})
if (activeName.value === 'ALL') {
tableData.value = res.data
} else {
tableData.value = res.data.filter(task => task.exportStatus === activeName.value)
}
})
.finally(() => {
drawerLoading.value = false
})
}
const init = params => {
drawer.value = true
if (params && params.activeName !== undefined) {
activeName.value = params.activeName
}
handleClick()
timer = setInterval(() => {
if (activeName.value === 'IN_PROGRESS') {
exportTasks(activeName.value).then(res => {
tabList.value.forEach(item => {
if (item.name === 'ALL') {
item.label = t('data_set.all') + '(' + res.data.length + ')'
}
if (item.name === 'IN_PROGRESS') {
item.label =
t('data_set.exporting') +
'(' +
res.data.filter(task => task.exportStatus === 'IN_PROGRESS').length +
')'
}
if (item.name === 'SUCCESS') {
item.label =
t('data_set.success') +
'(' +
res.data.filter(task => task.exportStatus === 'SUCCESS').length +
')'
}
if (item.name === 'FAILED') {
item.label =
t('data_set.fail') +
'(' +
res.data.filter(task => task.exportStatus === 'FAILED').length +
')'
}
if (item.name === 'PENDING') {
item.label =
t('data_set.waiting') +
'(' +
res.data.filter(task => task.exportStatus === 'PENDING').length +
')'
}
})
if (activeName.value === 'ALL') {
tableData.value = res.data
} else {
tableData.value = res.data.filter(task => task.exportStatus === activeName.value)
}
})
}
}, 5000)
}
const linkStore = useLinkStoreWithOut()
const appStore = useAppStoreWithOut()
const isDataEaseBi = computed(() => appStore.getIsDataEaseBi)
const taskExportTopicCall = task => {
if (!linkStore.getLinkToken && !isDataEaseBi.value && !appStore.getIsIframe) {
if (JSON.parse(task).exportStatus === 'SUCCESS') {
openMessageLoading(
JSON.parse(task).exportFromName + ` ${t('data_set.successful_go_to')}`,
'success',
callbackExportSuc
)
return
}
if (JSON.parse(task).exportStatus === 'FAILED') {
openMessageLoading(
JSON.parse(task).exportFromName + ` ${t('data_set.failed_go_to')}`,
'error',
callbackExportError
)
}
}
}
const openMessageLoading = (text, type = 'success', cb) => {
// success error loading
const customClass = `de-message-${type || 'success'} de-message-export`
ElMessage({
message: h('p', null, [
h(
'span',
{
title: t(text),
class: 'ellipsis m50-export'
},
t(text)
),
h(
ElButton,
{
text: true,
size: 'small',
class: 'btn-text',
onClick: () => {
cb()
}
},
t('data_export.export_center')
)
]),
icon: type === 'loading' ? h(RefreshLeft) : '',
type,
showClose: true,
customClass
})
}
const callbackExportError = () => {
useEmitt().emitter.emit('data-export-center', { activeName: 'FAILED' })
}
const callbackExportSuc = () => {
useEmitt().emitter.emit('data-export-center', { activeName: 'SUCCESS' })
}
const downLoadAll = () => {
if (multipleSelection.value.length === 0) {
tableData.value.forEach(item => {
window.open(PATH_URL + '/exportCenter/download/' + item.id)
})
return
}
multipleSelection.value.map(ele => {
window.open(PATH_URL + '/exportCenter/download/' + ele.id)
})
}
const showMsg = item => {
msg.value = ''
msg.value = item.msg
msgDialogVisible.value = true
}
const timestampFormatDate = value => {
if (!value) {
return '-'
}
return new Date(value).toLocaleString()
}
import { PATH_URL } from '@/config/axios/service'
const downloadClick = item => {
window.open(PATH_URL + '/exportCenter/download/' + item.id, openType)
}
const retry = item => {
exportRetry(item.id).then(() => {
handleClick()
})
}
const deleteField = item => {
ElMessageBox.confirm(t('data_export.sure_del'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDelete(item.id).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
}
const handleSelectionChange = val => {
multipleSelection.value = val
}
const delAll = () => {
if (multipleSelection.value.length === 0) {
ElMessageBox.confirm(t('data_export.sure_del_all'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDeleteAll(
activeName.value,
multipleSelection.value.map(ele => ele.id)
).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
return
}
ElMessageBox.confirm(t('data_export.sure_del'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDeletePost(multipleSelection.value.map(ele => ele.id)).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
}
useEmitt({ name: 'task-export-topic-call', callback: taskExportTopicCall })
defineExpose({
init
})
</script>
<template>
<el-drawer
v-loading="drawerLoading"
custom-class="de-export-excel"
:title="$t('data_export.export_center')"
v-model="drawer"
direction="rtl"
size="1000px"
append-to-body
:before-close="handleClose"
>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane v-for="tab in tabList" :key="tab.name" :label="tab.label" :name="tab.name" />
</el-tabs>
<el-button
v-if="activeName === 'SUCCESS' && multipleSelection.length === 0"
secondary
@click="downLoadAll"
>
<template #icon>
<Icon name="dv-preview-download"><dvPreviewDownload class="svg-icon" /></Icon>
</template>
{{ $t('data_export.download_all') }}
</el-button>
<el-button
v-if="activeName === 'SUCCESS' && multipleSelection.length !== 0"
secondary
@click="downLoadAll"
>
<template #icon>
<Icon name="dv-preview-download"><dvPreviewDownload class="svg-icon" /></Icon>
</template>
{{ $t('data_export.download') }}
</el-button>
<el-button v-if="multipleSelection.length === 0" secondary @click="delAll"
><template #icon>
<Icon name="de-delete"><deDelete class="svg-icon" /></Icon> </template
>{{ $t('data_export.del_all') }}
</el-button>
<el-button v-if="multipleSelection.length !== 0" secondary @click="delAll"
><template #icon>
<Icon name="de-delete"><deDelete class="svg-icon" /></Icon> </template
>{{ $t('commons.delete') }}
</el-button>
<div class="table-container" :class="!tableData.length && 'hidden-bottom'">
<el-table
ref="multipleTable"
:data="tableData"
height="100%"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="fileName" :label="$t('driver.file_name')" width="332">
<template #default="scope">
<div class="name-excel">
<el-icon style="font-size: 24px">
<Icon name="icon_file-excel_colorful"
><icon_fileExcel_colorful class="svg-icon"
/></Icon>
</el-icon>
<div class="name-content">
<div class="fileName">{{ scope.row.fileName }}</div>
<div
v-if="scope.row.exportStatus === 'FAILED'"
class="failed"
@click="showMsg(scope.row)"
>
{{ $t('data_export.export_failed') }}
</div>
<div v-if="scope.row.exportStatus === 'SUCCESS'" class="success">
{{ scope.row.fileSize }}{{ scope.row.fileSizeUnit }}
</div>
</div>
</div>
<div v-if="scope.row.exportStatus === 'FAILED'" class="red-line" />
<el-progress
v-if="scope.row.exportStatus === 'IN_PROGRESS'"
:percentage="+scope.row.exportProgress"
/>
</template>
</el-table-column>
<el-table-column prop="exportFromName" :label="$t('data_export.export_obj')" width="200" />
<el-table-column prop="exportTime" width="180" :label="$t('data_export.export_time')">
<template #default="scope">
<span>{{ timestampFormatDate(scope.row.exportTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="exportFromType" width="120" :label="$t('data_export.export_from')">
<template #default="scope">
<span v-if="scope.row.exportFromType === 'dataset'">{{ t('data_set.data_set') }}</span>
<span v-if="scope.row.exportFromType === 'chart'">{{ t('data_set.view') }}</span>
<span v-if="scope.row.exportFromType === 'data_filling'">{{
t('data_fill.data_fill')
}}</span>
</template>
</el-table-column>
<el-table-column
v-if="!desktop"
prop="orgName"
:label="t('data_set.organization')"
width="200"
/>
<el-table-column fixed="right" prop="operate" width="90" :label="$t('commons.operating')">
<template #default="scope">
<el-tooltip effect="dark" :content="t('data_set.download')" placement="top">
<el-button
v-if="scope.row.exportStatus === 'SUCCESS'"
text
@click="downloadClick(scope.row)"
>
<template #icon>
<el-icon>
<Icon name="dv-preview-download"><dvPreviewDownload class="svg-icon" /></Icon>
</el-icon>
</template>
</el-button>
</el-tooltip>
<el-tooltip effect="dark" :content="t('data_set.re_export')" placement="top">
<el-button v-if="scope.row.exportStatus === 'FAILED'" text @click="retry(scope.row)">
<template #icon>
<Icon name="icon_refresh_outlined"
><icon_refresh_outlined class="svg-icon"
/></Icon>
</template>
</el-button>
</el-tooltip>
<el-tooltip effect="dark" :content="t('data_set.delete')" placement="top">
<el-button text @click="deleteField(scope.row)">
<template #icon>
<Icon name="de-delete"><deDelete class="svg-icon" /></Icon>
</template>
</el-button>
</el-tooltip>
</template>
</el-table-column>
<template #empty>
<empty-background :description="description" img-type="noneWhite" />
</template>
</el-table>
</div>
</el-drawer>
<el-dialog :title="t('data_set.reason_for_failure')" v-model="msgDialogVisible" width="30%">
<span>{{ msg }}</span>
<template v-slot:footer>
<span class="dialog-footer">
<el-button type="primary" @click="msgDialogVisible = false">{{
t('data_set.closure')
}}</el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="less">
.de-export-excel {
.ed-drawer__body {
padding-bottom: 24px;
}
.ed-drawer__header {
border-bottom: none;
}
.ed-tabs {
margin-top: -25px;
.ed-tabs__header {
margin-bottom: 24px;
}
}
.table-container {
margin-top: 16px;
height: calc(100vh - 190px);
.ed-table .cell {
padding-left: 12px;
padding-right: 12px;
}
&.hidden-bottom {
.ed-table::before {
display: none;
}
}
.name-excel {
display: flex;
align-items: center;
.name-content {
max-width: 280px;
margin-left: 4px;
.fileName {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
.failed {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: #f54a45;
cursor: pointer;
}
.success {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: #8f959e;
}
}
}
.ed-table__header {
border-top: 1px solid #1f232926;
}
th.ed-table__cell.is-leaf {
border-color: #1f232926;
}
.red-line {
width: 100%;
height: 4px;
background: #f54a45;
position: absolute;
left: 0;
bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,245 @@
<script lang="ts">
import icon_down_outlined from '@/assets/svg/icon_down_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
export default {
name: 'logic-relation'
}
</script>
<script lang="ts" setup>
import { useI18n } from '@/hooks/web/useI18n'
import { PropType, computed, toRefs } from 'vue'
import FilterFiled from './FilterFiled.vue'
import type { Item } from './FilterFiled.vue'
export type Logic = 'or' | 'and'
export type Relation = {
child?: Relation[]
logic: Logic
x: number
} & Item
const { t } = useI18n()
const props = defineProps({
relationList: {
type: Array as PropType<Relation[]>,
default: () => []
},
x: {
type: Number,
default: 0
},
logic: {
type: String as PropType<Logic>,
default: 'or'
}
})
const marginLeft = computed(() => {
return {
marginLeft: props.x ? '20px' : 0
}
})
const emits = defineEmits([
'addCondReal',
'changeAndOrDfs',
'update:logic',
'removeRelationList',
'del'
])
const { relationList } = toRefs(props)
const handleCommand = type => {
emits('update:logic', type)
emits('changeAndOrDfs', type)
}
const removeRelationList = index => {
relationList.value.splice(index, 1)
}
const addCondReal = type => {
emits('addCondReal', type, props.logic === 'or' ? 'and' : 'or')
}
const add = (type, child, logic) => {
child.push(
type === 'condition'
? {
fieldId: '',
value: '',
enumValue: '',
term: '',
filterType: 'logic',
name: '',
deType: ''
}
: { child: [], logic }
)
}
const del = (index, child) => {
child.splice(index, 1)
}
</script>
<template>
<div class="logic" :style="marginLeft">
<div class="logic-left">
<div class="operate-title">
<span style="color: #bfbfbf" class="mrg-title" v-if="x">
{{ logic === 'or' ? 'OR' : 'AND' }}
</span>
<el-dropdown @command="handleCommand" trigger="click" v-else>
<span style="color: rgba(0 0 0 / 65%)" class="mrg-title fir">
{{ logic === 'or' ? 'OR' : 'AND' }}
<el-icon>
<Icon name="icon_down_outlined"><icon_down_outlined class="svg-icon" /></Icon>
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="and">AND</el-dropdown-item>
<el-dropdown-item command="or">OR</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<span class="operate-icon" v-if="x">
<el-icon @click="emits('removeRelationList')">
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
</el-icon>
</span>
</div>
<div class="logic-right">
<template :key="index" v-for="(item, index) in relationList">
<logic-relation
v-if="item.child"
:x="item.x"
@del="idx => del(idx, item.child)"
@addCondReal="(type, logic) => add(type, item.child, logic)"
:logic="item.logic"
@removeRelationList="removeRelationList(index)"
:relationList="item.child"
>
</logic-relation>
<filter-filed v-else :item="item" @del="emits('del', index)" :index="index"></filter-filed>
</template>
<div class="logic-right-add">
<button @click="addCondReal('condition')" class="operand-btn">
+ {{ t('auth.add_condition') }}
</button>
<button v-if="x < 2" @click="addCondReal('relation')" class="operand-btn">
+ {{ t('auth.add_relationship') }}
</button>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.logic {
display: flex;
align-items: center;
position: relative;
z-index: 1;
width: 100%;
.logic-left {
box-sizing: border-box;
width: 48px;
display: flex;
position: relative;
align-items: center;
z-index: 10;
.operate-title {
font-family: var(--de-custom_font, 'PingFang');
word-wrap: break-word;
box-sizing: border-box;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
display: inline-block;
white-space: nowrap;
margin: 0;
padding: 0;
width: 65px;
background-color: #f8f8fa;
line-height: 28px;
position: relative;
z-index: 1;
height: 28px;
.mrg-title {
text-align: left;
box-sizing: border-box;
position: relative;
display: block;
margin-left: 11px;
margin-right: 11px;
line-height: 28px;
height: 28px;
}
}
&:hover {
.operate-icon {
display: inline-block;
}
.operate-title {
.mrg-title:not(.fir) {
margin: 0 5px;
}
}
}
.operate-icon {
width: 40px;
height: 28px;
line-height: 28px;
background-color: #f8f8fa;
z-index: 1;
display: none;
i {
font-size: 12px;
font-style: normal;
display: unset;
padding: 5px 3px;
cursor: pointer;
position: relative;
z-index: 10;
}
}
}
.logic-right-add {
display: flex;
height: 41.4px;
align-items: center;
padding-left: 26px;
.operand-btn {
box-sizing: border-box;
font-weight: 400;
text-align: center;
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.015);
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
outline: 0;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
cursor: pointer;
height: 28px;
padding: 0 10px;
margin-right: 10px;
font-size: 14px;
color: #246dff;
background: #fff;
border: 1px solid #246dff;
border-radius: 2px;
}
}
}
</style>

View File

@ -0,0 +1,327 @@
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import AuthTree from './AuthTree.vue'
const { t } = useI18n()
const errorMessage = ref('')
const logic = ref<'or' | 'and'>('or')
const relationList = ref([])
const svgRealinePath = computed(() => {
const lg = relationList.value.length
let a = { x: 0, y: 0, child: relationList.value }
a.y = Math.floor(dfsXY(a, 0) / 2)
if (!lg) return ''
let path = calculateDepth(a)
return path
})
const svgDashinePath = computed(() => {
const lg = relationList.value.length
let a = { x: 0, y: 0, child: relationList.value }
a.y = Math.floor(dfsXY(a, 0) / 2)
if (!lg) return `M48 20 L68 20`
let path = calculateDepthDash(a)
return path
})
const init = expressionTree => {
const { items } = expressionTree
logic.value = expressionTree.logic || 'or'
relationList.value = dfsInit(items || [])
}
const submit = () => {
errorMessage.value = ''
emits('save', {
logic: logic.value,
items: dfsSubmit(relationList.value),
errorMessage: errorMessage.value
})
}
const errorDetected = ({ enumValue, deType, filterType, term, value, name }) => {
if (!name) {
errorMessage.value = t('data_set.cannot_be_empty_')
return
}
if (filterType === 'logic') {
if (!term) {
errorMessage.value = t('data_set.cannot_be_empty_de_ruler')
return
}
if (!term.includes('null') && !term.includes('empty') && value === '') {
errorMessage.value = t('chart.filter_value_can_null')
return
}
if ([2, 3].includes(deType)) {
if (parseFloat(value).toString() === 'NaN') {
errorMessage.value = t('chart.filter_value_can_not_str')
return
}
}
}
if (filterType === 'enum') {
if (enumValue.length < 1) {
errorMessage.value = t('chart.enum_value_can_not_null')
return
}
}
}
const dfsInit = arr => {
const elementList = []
arr.forEach(ele => {
const { subTree } = ele
if (subTree) {
const { items = [], logic } = subTree
const child = dfsInit(items)
elementList.push({ logic, child })
} else {
const { enumValue, fieldId, filterType, term, value, field } = ele
const { name, deType } = field || {}
elementList.push({
enumValue: enumValue.join(','),
fieldId,
filterType,
term,
value,
name,
deType
})
}
})
return elementList
}
const dfsSubmit = arr => {
const items = []
arr.forEach(ele => {
const { child = [] } = ele
if (child.length) {
const { logic } = ele
const subTree = dfsSubmit(child)
items.push({
enumValue: [],
fieldId: '',
filterType: '',
term: '',
type: 'tree',
value: '',
subTree: { logic, items: subTree }
})
} else {
const { enumValue, fieldId, filterType, deType, term, value, name } = ele
errorDetected({ deType, enumValue, filterType, term, value, name })
if (fieldId) {
items.push({
enumValue: enumValue ? enumValue.split(',') : [],
fieldId,
filterType,
term,
value,
type: 'item',
subTree: null
})
}
}
})
return items
}
const removeRelationList = () => {
relationList.value = []
}
const getY = arr => {
const [a] = arr
if (a.child?.length) {
return getY(a.child)
}
return a.y
}
const calculateDepthDash = obj => {
const lg = obj.child?.length
let path = ''
if (!lg && Array.isArray(obj.child)) {
const { x, y } = obj
path += `M${48 + x * 68} ${y * 41.4 + 20} L${88 + x * 68} ${y * 41.4 + 20}`
} else if (obj.child?.length) {
let y = Math.max(dfsY(obj, 0), dfs(obj.child, 0) + getY(obj.child) - 1)
let parent = (dfs(obj.child, 0) * 41.4) / 2 + (getY(obj.child) || 0) * 41.4
const { x } = obj
path += `M${24 + x * 68} ${parent} L${24 + x * 68} ${y * 41.4 + 20} L${64 + x * 68} ${
y * 41.4 + 20
}`
obj.child.forEach(item => {
path += calculateDepthDash(item)
})
}
return path
}
const calculateDepth = obj => {
const lg = obj.child.length
if (!lg) return ''
let path = ''
const { x: depth, y } = obj
obj.child.forEach((item, index) => {
const { y: sibingLg, z } = item
if (item.child?.length) {
let parent = (dfs(obj.child, 0) * 41.4) / 2 + (getY(obj.child) || 0) * 41.4
let children = (dfs(item.child, 0) * 41.4) / 2 + getY(item.child) * 41.4
let path1 = 0
let path2 = 0
if (parent < children) {
path1 = parent
path2 = children
} else {
;[path1, path2] = [children, parent]
}
if (y >= sibingLg) {
path1 = parent
path2 = children
}
path += `M${24 + depth * 68} ${path1} L${24 + depth * 68} ${path2} L${
68 + depth * 68
} ${path2}`
path += calculateDepth(item)
}
if (!item.child?.length) {
if (sibingLg >= y) {
path += `M${24 + depth * 68} ${y * 40} L${24 + depth * 68} ${
(sibingLg + 1) * 41.4 - 20.69921875
} L${68 + depth * 68} ${(sibingLg + 1) * 41.4 - 20.69921875}`
} else {
path += `M${24 + depth * 68} ${
(sibingLg +
(lg === 1 && index === 0 ? 0 : 1) +
(obj.child[index + 1]?.child?.length ? y - sibingLg - 1 : 0)) *
41.4 +
20 +
(lg === 1 && index === 0 ? 26 : 0)
} L${24 + depth * 68} ${
(sibingLg + 1) * 41.4 - 20.69921875 - (lg === 1 && index === 0 ? (z || 0) * 1.4 : 0)
} L${68 + depth * 68} ${
(sibingLg + 1) * 41.4 - 20.69921875 - (lg === 1 && index === 0 ? (z || 0) * 1.4 : 0)
}`
}
}
})
return path
}
const changeAndOrDfs = (arr, logic) => {
arr.forEach(ele => {
if (ele.child) {
ele.logic = logic === 'and' ? 'or' : 'and'
changeAndOrDfs(ele.child, ele.logic)
}
})
}
const dfs = (arr, count) => {
arr.forEach(ele => {
if (ele.child?.length) {
count = dfs(ele.child, count)
} else {
count += 1
}
})
count += 1
return count
}
const dfsY = (obj, count) => {
obj.child.forEach(ele => {
if (ele.child?.length) {
count = dfsY(ele, count)
} else {
count = Math.max(count, ele.y, obj.y)
}
})
return count
}
const dfsXY = (obj, count) => {
obj.child.forEach(ele => {
ele.x = obj.x + 1
if (ele.child?.length) {
let l = dfs(ele.child, 0)
ele.y = Math.floor(l / 2) + count
count = dfsXY(ele, count)
} else {
count += 1
ele.y = count - 1
}
})
count += 1
return count
}
const addCondReal = (type, logic) => {
relationList.value.push(
type === 'condition'
? {
fieldId: '',
value: '',
enumValue: '',
term: '',
filterType: 'logic',
name: '',
deType: ''
}
: { child: [], logic }
)
}
const del = index => {
relationList.value.splice(index, 1)
}
defineExpose({
init,
submit
})
const emits = defineEmits(['save'])
</script>
<template>
<div class="rowAuth">
<auth-tree
@del="idx => del(idx)"
@addCondReal="addCondReal"
@removeRelationList="removeRelationList"
@changeAndOrDfs="type => changeAndOrDfs(relationList, type)"
:relationList="relationList"
v-model:logic="logic"
/>
<svg width="388" height="100%" class="real-line">
<path
stroke-linejoin="round"
stroke-linecap="round"
:d="svgRealinePath"
fill="none"
stroke="#CCCCCC"
stroke-width="0.5"
></path>
</svg>
<svg width="388" height="100%" class="dash-line">
<path
stroke-linejoin="round"
stroke-linecap="round"
:d="svgDashinePath"
fill="none"
stroke="#CCCCCC"
stroke-width="0.5"
stroke-dasharray="4,4"
></path>
</svg>
</div>
</template>
<style lang="less" scoped>
.rowAuth {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
position: relative;
}
.real-line,
.dash-line {
position: absolute;
top: 0;
left: 0;
user-select: none;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,897 @@
<script lang="ts" setup>
import icon_info_outlined from '@/assets/svg/icon_info_outlined.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import icon_adjustment_outlined from '@/assets/svg/icon_adjustment_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
import { ref, reactive, onMounted, onBeforeUnmount, watch, unref, computed, nextTick } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import CodeMirror from './CodeMirror.vue'
import { getFunction } from '@/api/dataset'
import { fieldType } from '@/utils/attr'
import { cloneDeep } from 'lodash-es'
import { guid } from './util'
import { iconFieldMap } from '@/components/icon-group/field-list'
export interface CalcFieldType {
id?: string
datasourceId?: string // id
datasetTableId?: string // union node id
datasetGroupId?: string // null
originName: string //
name: string //
dataeaseName?: string //
groupType: 'd' | 'q' // d=q=
type: string
params?: Array<{ id: string; name: string; value: number }>
checked: boolean
deType: number //
deExtractType?: number //
extField?: number // 0=2=
fieldShortName?: string //
}
const { t } = useI18n()
const myCm = ref()
const searchField = ref('')
const searchFunction = ref('')
const mirror = ref()
const props = defineProps({
crossDs: {
type: Boolean,
default: () => false
}
})
const fields = [
{ label: t('dataset.text'), value: 0 },
{ label: t('dataset.time'), value: 1 },
{ label: t('dataset.value'), value: 2 },
{
label: t('dataset.value') + '(' + t('dataset.float') + ')',
value: 3
},
{ label: t('dataset.location'), value: 5 },
{ label: 'URL', value: 7 }
]
const defaultForm = {
originName: '', //
name: '', //
groupType: 'd', // d=q=
type: 'VARCHAR',
deType: 0, //
extField: 2,
id: '',
params: [],
checked: true
}
const state = reactive({
functionData: [],
dimensionData: [],
dimensionList: [],
quotaData: []
})
const formQuotaRef = ref()
const formQuota = reactive({
id: null,
name: '',
value: null
})
const dialogFormVisible = ref(false)
const formQuotaRules = {
name: [
{ required: true, message: t('data_set.enter_parameter_name'), trigger: 'blur' },
{ min: 1, max: 50, message: t('data_set.enter_1_50_characters'), trigger: 'blur' }
],
value: [{ required: true, message: t('data_set.parameter_default_value'), trigger: 'blur' }]
}
const formQuotaClose = () => {
formQuotaRef.value.resetFields()
dialogFormVisible.value = false
}
const formQuotaConfirm = () => {
formQuotaRef.value.validate(val => {
if (val) {
if (!formQuota.id) {
formQuota.id = `params_${guid()}`
}
const q = cloneDeep(unref(formQuota))
fieldForm.params = [q]
const i = state.quotaData.find(ele => ele.id === formQuota.id)
if (i) {
const str = mirror.value.state.doc.toString()
const name2Auto = []
fieldForm.originName = setNameIdTrans('name', 'id', str, name2Auto)
Object.assign(i, cloneDeep(unref(formQuota)))
nextTick(() => {
mirror.value.dispatch({
changes: {
from: 0,
to: mirror.value.viewState.state.doc.length,
insert: setNameIdTrans('id', 'name', fieldForm.originName)
}
})
})
} else {
state.quotaData.push(q)
}
formQuotaClose()
}
})
}
const fieldForm = reactive<CalcFieldType>({ ...(defaultForm as CalcFieldType) })
const setFieldForm = () => {
const str = mirror.value.state.doc.toString()
const name2Auto = []
fieldForm.originName = setNameIdTrans('name', 'id', str, name2Auto)
}
const setNameIdTrans = (from, to, originName, name2Auto?: string[]) => {
let name2Id = originName
const nameIdMap = [...state.dimensionData, ...state.quotaData].reduce((pre, next) => {
pre[next[from]] = next[to]
return pre
}, {})
const on = originName.match(/\[(.+?)\]/g)
if (on) {
on.forEach(itm => {
const ele = itm.slice(1, -1)
if (name2Auto) {
name2Auto.push(nameIdMap[ele])
}
name2Id = name2Id.replace(`[${ele}]`, `[${nameIdMap[ele]}]`)
})
}
return name2Id
}
let quotaDataList = []
let dimensionDataList = []
const initEdit = (obj, dimensionData, quotaData) => {
formQuota.id = null
Object.assign(fieldForm, { ...defaultForm, ...obj })
state.dimensionData = dimensionData
state.quotaData = quotaData.concat(fieldForm.params || [])
quotaDataList = cloneDeep(quotaData.concat(fieldForm.params || []))
dimensionDataList = cloneDeep(dimensionData)
setTimeout(() => {
formField.value.clearValidate()
}, 100)
if (!obj.originName) {
mirror.value.dispatch({
changes: {
from: 0,
to: mirror.value.viewState.state.doc.length,
insert: ''
}
})
return
}
nextTick(() => {
mirror.value.dispatch({
changes: {
from: 0,
to: mirror.value.viewState.state.doc.length,
insert: setNameIdTrans('id', 'name', obj.originName)
}
})
})
}
const insertFieldToCodeMirror = (value: string) => {
mirror.value.dispatch({
changes: { from: mirror.value.viewState.state.selection.ranges[0].from, insert: value },
selection: { anchor: mirror.value.viewState.state.selection.ranges[0].from }
})
}
onMounted(() => {
mirror.value = myCm.value.codeComInit()
})
onBeforeUnmount(() => {
mirror.value.destroy?.()
})
const insertParamToCodeMirror = (value: string) => {
mirror.value.dispatch({
changes: { from: mirror.value.viewState.state.selection.ranges[0].from, insert: value },
selection: { anchor: mirror.value.viewState.state.selection.ranges[0].from }
})
}
let functions = []
const initFunction = () => {
getFunction().then(res => {
functions = cloneDeep(res)
state.functionData = cloneDeep(res)
})
}
watch(
() => searchField.value,
val => {
if (val && val !== '') {
state.dimensionData = JSON.parse(
JSON.stringify(
dimensionDataList.filter(
ele =>
ele.name.toLocaleLowerCase().includes(val.toLocaleLowerCase()) && ele.extField === 0
)
)
)
state.quotaData = JSON.parse(
JSON.stringify(
quotaDataList.filter(
ele =>
ele.name.toLocaleLowerCase().includes(val.toLocaleLowerCase()) && ele.extField === 0
)
)
)
} else {
state.dimensionData = JSON.parse(JSON.stringify(dimensionDataList)).filter(
ele => ele.extField === 0
)
state.quotaData = JSON.parse(JSON.stringify(quotaDataList)).filter(ele => ele.extField === 0)
}
}
)
watch(
() => searchFunction.value,
val => {
if (val && val !== '') {
state.functionData = JSON.parse(
JSON.stringify(
functions.filter(ele => {
return ele.func.toLocaleLowerCase().includes(val.toLocaleLowerCase())
})
)
)
} else {
state.functionData = cloneDeep(functions)
}
}
)
const formField = ref()
defineExpose({
initEdit,
setFieldForm,
fieldForm,
formField
})
const parmasTitle = ref('')
const addParmasToQuota = () => {
if (disableCaParams.value) return
parmasTitle.value = t('data_set.add_calculation_parameters')
if (!fieldForm.params) {
fieldForm.params = []
}
dialogFormVisible.value = true
}
const updateParmasToQuota = () => {
const [o] = fieldForm.params
parmasTitle.value = t('data_set.edit_calculation_parameters')
Object.assign(formQuota, o || {})
dialogFormVisible.value = true
}
const disableCaParams = computed(() => {
return !!fieldForm.params?.length
})
const delParmasToQuota = () => {
const [o] = fieldForm.params
fieldForm.params = []
const str = mirror.value.state.doc.toString()
const name2Auto = []
fieldForm.originName = setNameIdTrans('name', 'id', str, name2Auto).replaceAll(`[${o.id}]`, '')
state.quotaData = state.quotaData.filter(ele => ele.id !== o.id)
mirror.value.dispatch({
changes: {
from: 0,
to: mirror.value.viewState.state.doc.length,
insert: setNameIdTrans('id', 'name', fieldForm.originName)
}
})
}
initFunction()
</script>
<template>
<div @keydown.stop @keyup.stop class="calcu-field">
<div class="calcu-cont">
<div style="flex: 1">
<div style="max-width: 488px">
<el-form
require-asterisk-position="right"
ref="formField"
@keydown.stop.prevent.enter
:model="fieldForm"
label-position="top"
>
<el-form-item
prop="name"
:rules="[
{
required: true,
message: t('dataset.input_edit_name')
},
{
max: 50,
message: t('commons.char_can_not_more_50')
}
]"
:label="t('dataset.field_edit_name')"
>
<el-input v-model="fieldForm.name" :placeholder="t('dataset.input_edit_name')" />
</el-form-item>
</el-form>
<div>
<el-form label-position="top" ref="form" inline :model="fieldForm">
<el-form-item class="mr12" :label="t('dataset.data_type')">
<div class="btn-select">
<el-button
@click="fieldForm.groupType = 'd'"
:class="[fieldForm.groupType === 'd' && 'is-active']"
text
>
{{ t('chart.dimension_abb') }}
</el-button>
<el-button
@click="fieldForm.groupType = 'q'"
:class="[fieldForm.groupType === 'q' && 'is-active']"
text
>
{{ t('chart.quota_abb') }}
</el-button>
</div>
</el-form-item>
<el-form-item class="mr0" :label="t('dataset.field_type')">
<el-select v-model="fieldForm.deType" style="width: 376px">
<template #prefix>
<el-icon>
<Icon
><component
class="svg-icon"
:class="`field-icon-${fieldType[fieldForm.deType]}`"
:is="iconFieldMap[fieldType[fieldForm.deType]]"
></component
></Icon>
</el-icon>
</template>
<el-option
v-for="item in fields"
:key="item.value"
:label="item.label"
:value="item.value"
>
<span style="display: flex; align-items: center">
<el-icon>
<Icon
><component
class="svg-icon"
:class="`field-icon-${fieldType[item.value]}`"
:is="iconFieldMap[fieldType[item.value]]"
></component
></Icon>
</el-icon>
</span>
<span style="margin-left: 5px; font-size: 12px; color: #8492a6">{{
item.label
}}</span>
</el-option>
</el-select>
</el-form-item>
</el-form>
</div>
<div class="mb8 field-exp">
<span>{{ t('dataset.field_exp') }}</span>
<span>*</span>
<el-tooltip class="item" effect="dark" placement="top">
<template #content>
<div v-if="props.crossDs">{{ t('dataset.calc_tips.tip1') }}</div>
<div v-else>{{ t('dataset.calc_tips.tip1_1') }}</div>
<div>{{ t('dataset.calc_tips.tip2') }}</div>
</template>
<el-icon size="16px">
<Icon name="icon_info_outlined"><icon_info_outlined class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
</div>
<code-mirror
:quotaMap="state.quotaData.map(ele => ele.name)"
:dimensionMap="state.dimensionData.map(ele => ele.name)"
ref="myCm"
height="318px"
dom-id="calcField"
></code-mirror>
</div>
</div>
<div class="padding-lr">
<span class="mb8">
{{ t('dataset.click_ref_field') }}
<el-tooltip class="item" effect="dark" placement="bottom">
<template #content>
{{ t('dataset.calc_tips.tip3') }}
<br />
{{ t('dataset.calc_tips.tip4') }}
<br />
{{ t('dataset.calc_tips.tip5') }}
</template>
<el-icon size="16px">
<Icon name="icon_info_outlined"><icon_info_outlined class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
</span>
<div class="padding-lr-content">
<el-input v-model="searchField" :placeholder="t('dataset.edit_search')" clearable>
<template #prefix>
<el-icon>
<Icon name="icon_search-outline_outlined"
><icon_searchOutline_outlined class="svg-icon"
/></Icon>
</el-icon>
</template>
</el-input>
<div class="field-height">
<el-scrollbar>
<span>{{ t('chart.dimension') }}</span>
<div v-if="state.dimensionData.length" class="field-list">
<span
v-for="item in state.dimensionData"
:key="item.id"
class="item-dimension flex-align-center"
:title="item.name"
@click="insertFieldToCodeMirror('[' + item.name + ']')"
>
<el-icon>
<Icon
><component
class="svg-icon"
:class="`field-icon-${fieldType[item.deType]}`"
:is="iconFieldMap[fieldType[item.deType]]"
></component
></Icon>
</el-icon>
<span class="ellipsis" :title="item.name">{{ item.name }}</span>
</span>
</div>
<div v-else class="class-na">{{ t('dataset.na') }}</div>
</el-scrollbar>
</div>
<div class="quota-btn_de">
<span>{{ t('chart.quota') }}</span>
<el-tooltip
effect="dark"
:content="
disableCaParams
? t('data_set.parameter_is_supported')
: t('data_set.add_calculation_parameters')
"
placement="top"
>
<el-icon class="hover-icon_quota" @click="addParmasToQuota">
<Icon
:class="[`field-icon-${fieldType[0]}`, disableCaParams && 'not-allow']"
style="color: #646a73"
name="icon_adjustment_outlined"
><icon_adjustment_outlined class="svg-icon"
/></Icon>
</el-icon>
</el-tooltip>
</div>
<div class="field-height">
<el-scrollbar>
<div v-if="state.quotaData.length" class="field-list">
<span
v-for="item in state.quotaData"
:key="item.id"
class="item-quota flex-align-center"
@click="insertFieldToCodeMirror('[' + item.name + ']')"
>
<el-icon v-if="!item.groupType">
<Icon name="icon_adjustment_outlined"
><icon_adjustment_outlined class="svg-icon"
/></Icon>
</el-icon>
<el-icon v-else>
<Icon
><component
class="svg-icon"
:class="`field-icon-${fieldType[item.deType]}`"
:is="iconFieldMap[fieldType[item.deType]]"
></component
></Icon>
</el-icon>
<span class="ellipsis" :title="item.name">{{ item.name }}</span>
<div v-if="!item.groupType" class="icon-right">
<el-icon @click.stop="updateParmasToQuota" class="hover-icon">
<Icon name="icon_edit_outlined"><icon_edit_outlined class="svg-icon" /></Icon>
</el-icon>
<el-icon @click.stop="delParmasToQuota" class="hover-icon">
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
</el-icon>
</div>
</span>
</div>
<div v-else class="class-na">{{ t('dataset.na') }}</div>
</el-scrollbar>
</div>
</div>
</div>
<div class="padding-lr">
<span class="mb8">
{{ t('dataset.click_ref_function') }}
<el-tooltip class="item" effect="dark" placement="bottom">
<template #content>
<div v-if="props.crossDs">
{{ t('dataset.calc_tips.tip6') }}
<br />
{{ t('dataset.calc_tips.tip8') }}
<br />
https://calcite.apache.org/docs/reference.html
</div>
<div v-else>{{ t('dataset.calc_tips.tip7') }}</div>
</template>
<el-icon size="16px">
<Icon name="icon_info_outlined"><icon_info_outlined class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
</span>
<div class="padding-lr-content">
<el-input
v-model="searchFunction"
style="margin-bottom: 8px"
:placeholder="t('dataset.edit_search')"
clearable
>
<template #prefix>
<el-icon>
<Icon name="icon_search-outline_outlined"
><icon_searchOutline_outlined class="svg-icon"
/></Icon>
</el-icon>
</template>
</el-input>
<el-row class="function-height">
<div v-if="state.functionData.length" style="width: 100%">
<el-popover
v-for="(item, index) in state.functionData"
:key="index"
class="function-pop"
placement="right"
width="200"
trigger="hover"
:open-delay="500"
>
<template #reference>
<span
class="function-style flex-align-center"
@click="insertParamToCodeMirror(item.func)"
>{{ item.func }}</span
>
</template>
<p class="pop-title">{{ item.name }}</p>
<p class="pop-info">{{ item.func }}</p>
<p class="pop-info">{{ item.desc }}</p>
</el-popover>
</div>
<div v-else class="class-na">{{ t('chart.no_function') }}</div>
</el-row>
</div>
</div>
</div>
<el-dialog
:before-close="formQuotaClose"
v-model="dialogFormVisible"
append-to-body
class="create-dialog"
:title="t('data_set.add_calculation_parameters')"
width="500"
>
<el-form
@keydown.stop.prevent.enter
label-position="top"
ref="formQuotaRef"
:model="formQuota"
:rules="formQuotaRules"
>
<el-form-item :label="t('data_set.parameter_name')" prop="name">
<el-input
style="width: 100%"
v-model="formQuota.name"
:placeholder="t('data_set.enter_1_50_characters')"
/>
</el-form-item>
<el-form-item :label="t('data_set.parameter_default_value_de')" prop="value">
<el-input-number
style="width: 100%"
v-model="formQuota.value"
:placeholder="t('data_set.enter_a_number')"
controls-position="right"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="formQuotaClose">{{ t('chart.cancel') }}</el-button>
<el-button type="primary" @click="formQuotaConfirm"> {{ t('chart.confirm') }} </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style lang="less" scoped>
.calcu-field {
.calcu-cont {
color: #606266;
font-size: 14px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
}
.mr12 {
margin-right: 12px;
}
.mr0 {
margin-right: 0;
:deep(.ed-select__prefix--light) {
padding: 0;
border: none;
margin: 0;
}
}
.btn-select {
width: 100px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: rgb(38,38,38);
border: 1px solid #bbbfc4;
border-radius: 4px;
.is-active {
background: var(--ed-color-primary-1a, rgba(95, 95, 95, 0.1));
}
.ed-button:not(.is-active) {
color: #ffffff;
}
.ed-button.is-text {
height: 24px;
width: 44px;
line-height: 24px;
}
.ed-button + .ed-button {
margin-left: 4px;
}
}
.mb8 {
margin-bottom: 8px;
display: inline-flex;
align-items: center;
color: #1f2329;
&.field-exp {
& > :nth-child(2) {
margin: 0 -0.67px 0 2px;
color: #f54a45;
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
}
.ed-icon {
color: #646a73;
margin-left: 4.67px;
}
}
}
.padding-lr {
margin-left: 12px;
width: 214px;
overflow-y: hidden;
.padding-lr-content {
padding: 12px;
border: 1px solid var(--deCardStrokeColor, #dee0e3);
box-sizing: border-box;
height: 500px;
border-radius: 4px;
}
}
.hover-icon_quota {
cursor: pointer;
border-radius: 4px;
font-size: 16px;
position: relative;
&[aria-expanded='true'] {
&::after {
content: '';
position: absolute;
width: 24px;
height: 24px;
background: rgba(31, 35, 41, 0.1);
border-radius: 4px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
}
}
&:hover {
&::after {
content: '';
position: absolute;
width: 24px;
height: 24px;
background: rgba(31, 35, 41, 0.1);
border-radius: 4px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
}
}
&:active {
&::after {
content: '';
position: absolute;
width: 24px;
height: 24px;
background: rgba(31, 35, 41, 0.2);
border-radius: 4px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
}
}
}
.quota-btn_de {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: -12px;
color: #ffffff;
}
.field-height {
height: calc(50% - 41px);
margin-top: 12px;
overflow-y: auto;
& > :nth-child(1) {
color: #ffffff;
}
.not-allow {
cursor: not-allowed;
// color: #bbbfc4 !important;
}
}
.item-dimension,
.item-quota {
padding: 1px 8px;
border: solid 1px #dee0e3;
background-color: rgb(37, 38, 38);
color: #ffffff;
.ed-icon {
font-size: 16px;
margin-right: 4px;
}
height: 28px;
margin-top: 4px;
word-break: break-all;
border-radius: 4px;
.icon-right {
display: none;
margin-left: auto;
align-items: center;
.ed-icon {
margin: 0 0 0 6px;
}
}
}
.item-dimension:hover {
border-color: var(--ed-color-primary, #3370ff);
background: var(--ed-color-primary-1a, rgba(51, 112, 255, 0.1));
cursor: pointer;
}
.item-quota {
.ed-icon {
color: #04b49c;
}
}
.item-quota:hover {
background: rgba(4, 180, 156, 0.1);
border-color: #04b49c;
cursor: pointer;
.icon-right {
display: flex;
}
}
.function-style {
min-height: 28px;
padding: 0px 8px;
margin-bottom: 4px;
border-radius: 4px;
color: #ffffff;
&:hover {
background: rgba(31, 35, 41, 0.1);
}
}
.function-style:hover {
border-color: var(--ed-color-primary, #3370ff);
cursor: pointer;
}
.function-height {
height: calc(100% - 29px);
overflow: auto;
width: calc(100% + 16px);
margin-left: -8px;
}
.function-pop :deep(.ed-popover) {
padding: 6px !important;
}
.pop-title {
margin: 6px 0 0 0;
font-size: 14px;
font-weight: 500;
}
.pop-info {
margin: 6px 0 0 0;
font-size: 12px;
}
.class-na {
margin-top: 8px;
text-align: center;
font-size: 14px;
color: var(--deTextDisable);
}
</style>
<style lang="less">
.calcu-field {
.cm-scroller {
height: 320px;
border: 1px solid #bbbfc4;
border-radius: 4px;
overflow-y: auto;
background: #fff;
}
.cm-focused {
outline: none;
}
}
</style>

View File

@ -0,0 +1,156 @@
<script lang="ts" setup>
import { sql } from '@codemirror/lang-sql'
import { basicSetup } from 'codemirror'
import { indentWithTab } from '@codemirror/commands'
import {
Decoration,
EditorView,
ViewPlugin,
WidgetType,
MatchDecorator,
keymap
} from '@codemirror/view'
import { propTypes } from '@/utils/propTypes'
const props = defineProps({
domId: propTypes.string.def('editor'),
height: propTypes.string.def('250px'),
quotaMap: propTypes.arrayOf(String).def(() => []),
dimensionMap: propTypes.arrayOf(String).def(() => [])
})
const emits = defineEmits(['change'])
const codeComInit = (doc: string, sqlMode?: boolean) => {
function _optionalChain(ops) {
let lastAccessLHS = undefined
let value = ops[0]
let i = 1
while (i < ops.length) {
const op = ops[i]
const fn = ops[i + 1]
i += 2
if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) {
return undefined
}
if (op === 'access' || op === 'optionalAccess') {
lastAccessLHS = value
value = fn(value)
} else if (op === 'call' || op === 'optionalCall') {
value = fn((...args) => value.call(lastAccessLHS, ...args))
lastAccessLHS = undefined
}
}
return value
} //!placeholderMatcher
const placeholderMatcher = new MatchDecorator({
regexp: /\[(.*?)\]/g,
decoration: match =>
Decoration.replace({
widget: new PlaceholderWidget(match[1])
})
})
//!placeholderPlugin
const placeholders = ViewPlugin.fromClass(
class {
placeholders
constructor(view) {
this.placeholders = placeholderMatcher.createDeco(view)
}
update(update) {
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders)
}
},
{
decorations: instance => instance.placeholders,
provide: plugin =>
EditorView.atomicRanges.of(view => {
return (
_optionalChain([
view,
'access',
_ => _.plugin,
'call',
_2 => _2(plugin),
'optionalAccess',
_3 => _3.placeholders
]) || Decoration.none
)
})
}
)
//!placeholderWidget
class PlaceholderWidget extends WidgetType {
name: string
constructor(name: string) {
super()
this.name = name
}
eq(other) {
return this.name == other.name
}
toDOM() {
let elt = document.createElement('span')
elt.textContent = `[${this.name}]`
const { dimensionMap, quotaMap } = props
if (!dimensionMap?.length && !quotaMap?.length) {
return elt
}
const isQuota = quotaMap.includes(this.name)
elt.style.borderRadius = '2px'
elt.style.margin = '0 4px'
elt.style.padding = '0 6px'
elt.style.background = isQuota ? 'rgba(0, 214, 185, 0.20)' : 'rgba(51, 112, 255, 0.20)'
elt.style.color = isQuota ? '#04B49C' : '#2B5FD9'
return elt
}
ignoreEvent() {
return false
}
}
const extensionsAttach = sqlMode
? [
basicSetup,
sql(),
placeholders,
keymap.of([indentWithTab]),
EditorView.updateListener.of(v => {
if (v.docChanged) {
emits('change')
}
})
]
: [basicSetup, placeholders, keymap.of([indentWithTab])]
return new EditorView({
doc,
extensions: extensionsAttach,
parent: document.querySelector(`#${props.domId}`)
})
}
defineExpose({
codeComInit
})
</script>
<template>
<div :style="{ height: height }" class="editor-placeholder" :id="domId"></div>
</template>
<style lang="less" scoped>
.editor-placeholder {
width: 100%;
}
</style>
<style lang="less">
.cm-editor,
.cm-scroller {
height: 100% !important;
background: #eff0f1;
}
</style>

View File

@ -0,0 +1,454 @@
<script lang="ts" setup>
import dvFolder from '@/assets/svg/dv-folder.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { ref, reactive, computed, watch, nextTick, unref } from 'vue'
import treeSort from '@/utils/treeSortUtils'
import { useCache } from '@/hooks/web/useCache'
import { ElMessage } from 'element-plus-secondary'
import { cloneDeep } from 'lodash-es'
import { useI18n } from '@/hooks/web/useI18n'
import {
getDatasetTree,
moveDatasetTree,
createDatasetTree,
renameDatasetTree
} from '@/api/dataset'
import type { DatasetOrFolder } from '@/api/dataset'
import nothingTree from '@/assets/img/nothing-tree.png'
import { BusiTreeRequest } from '@/models/tree/TreeNode'
import { filterFreeFolder } from '@/utils/utils'
export interface Tree {
name: string
value?: string | number
id: string | number
nodeType: string
appId: string | number
createBy?: string
level: number
leaf?: boolean
pid: string | number
union?: Array<{}>
createTime: number
allfields?: Array<{}>
children?: Tree[]
}
const { t } = useI18n()
const { wsCache } = useCache()
const state = reactive({
tData: [],
nameList: []
})
const placeholder = ref('')
const nodeType = ref()
const pid = ref()
const appId = ref()
const id = ref()
const cmd = ref('')
const treeRef = ref()
const filterText = ref('')
let union = []
let allfields = []
const datasetForm = reactive({
pid: '',
name: ''
})
const searchEmpty = ref(false)
const filterNode = (value: string, data: Tree) => {
nextTick(() => {
searchEmpty.value = treeRef.value.isEmpty
})
if (!value) return true
return data.name.includes(value)
}
watch(filterText, val => {
showAll.value = !val
treeRef.value.filter(val)
nextTick(() => {
document.querySelectorAll('.node-text').forEach(ele => {
const content = ele.getAttribute('title')
ele.innerHTML = content.replace(val, `<span class="highLight">${val}</span>`)
})
})
})
const showPid = computed(() => {
if (nodeType.value === 'folder' && !!pid.value) {
return false
}
return !['rename', 'move'].includes(cmd.value) && !!pid.value
})
const labelName = computed(() => {
return nodeType.value === 'folder' ? t('deDataset.folder_name') : t('dataset.name')
})
const dialogTitle = computed(() => {
let title = ''
switch (nodeType.value) {
case 'folder':
title = t('deDataset.new_folder')
break
case 'dataset':
title = t('common.save') + t('auth.dataset')
break
default:
break
}
switch (cmd.value) {
case 'move':
title = t('chart.move_to')
break
case 'rename':
title = t('chart.rename')
break
default:
break
}
return title
})
const showName = computed(() => {
return cmd.value !== 'move'
})
const datasetFormRules = ref()
const activeAll = ref(false)
const showAll = ref(true)
const dataset = ref()
const loading = ref(false)
const createDataset = ref(false)
const filterMethod = (value, data) => data.name.includes(value)
const resetForm = () => {
createDataset.value = false
}
const dfs = (arr: Tree[]) => {
arr?.forEach(ele => {
ele.value = ele.id
if (ele.children?.length) {
dfs(ele.children)
}
})
}
const formatRootMiss = (id: string | number, treeData: Tree[]) => {
if (!treeData?.length) {
return ''
}
if (id === '0' && treeData[0].id !== '0') {
return treeData[0].id
}
return id
}
const originResourceTree = ref([])
const sortList = ['time_asc', 'time_desc', 'name_asc', 'name_desc']
const createInit = (type, data: Tree, exec, name: string) => {
appId.value = ''
pid.value = ''
id.value = ''
cmd.value = ''
datasetForm.pid = ''
datasetForm.name = ''
filterText.value = ''
nodeType.value = type
if(type === 'folder' && data !=null && data.appId !=null){
appId.value = data.appId
}
placeholder.value =
type === 'folder' ? t('data_set.a_folder_name') : t('data_set.the_dataset_name')
if (type === 'dataset') {
union = data.union
allfields = data.allfields
}
if (data.id) {
const request = { leaf: false, weight: 7 } as BusiTreeRequest
getDatasetTree(request).then(res => {
filterFreeFolder(res, 'dataset')
dfs(res as unknown as Tree[])
state.tData = (res as unknown as Tree[]) || []
let curSortType = sortList[Number(wsCache.get('TreeSort-backend')) ?? 1]
curSortType = wsCache.get('TreeSort-dataset') ?? curSortType
originResourceTree.value = cloneDeep(unref(state.tData))
state.tData = treeSort(originResourceTree.value, curSortType)
if (state.tData.length && state.tData[0].name === 'root' && state.tData[0].id === '0') {
state.tData[0].name = t('data_set.data_set')
}
data.id = formatRootMiss(data.id, state.tData)
if (exec) {
pid.value = data.pid
id.value = data.id
datasetForm.pid = data.pid as string
datasetForm.name = data.name
} else {
datasetForm.pid = data.id as string
pid.value = data.id
}
})
cmd.value = exec
}
name && (datasetForm.name = name)
createDataset.value = true
datasetFormRules.value = {
name: [
{
required: true,
message: placeholder.value,
trigger: 'change'
},
{
required: true,
message: placeholder.value,
trigger: 'blur'
},
{
min: 1,
max: 64,
message: t('datasource.input_limit_1_64', [1, 64]),
trigger: 'blur'
}
],
pid: [
{
required: true,
message: t('common.please_select'),
trigger: 'blur'
}
]
}
setTimeout(() => {
dataset.value.clearValidate()
}, 50)
}
const editeInit = (param: Tree) => {
pid.value = param.pid
id.value = param.id
}
const props = {
label: 'name',
children: 'children',
isLeaf: node => !node.children?.length
}
const nodeClick = (data: Tree) => {
activeAll.value = false
datasetForm.pid = data.id as string
}
const checkPid = pid => {
if (pid !== 0 && !pid) {
ElMessage.error(t('data_set.the_destination_folder'))
return false
}
return true
}
const saveDataset = () => {
dataset.value.validate(result => {
if (result) {
const params: DatasetOrFolder = {
nodeType: nodeType.value as 'folder' | 'dataset',
name: datasetForm.name,
appId: appId.value
}
switch (cmd.value) {
case 'move':
params.pid = activeAll.value ? '0' : (datasetForm.pid as string)
params.id = id.value
break
case 'rename':
params.pid = pid.value as string
params.id = id.value
break
default:
params.pid = datasetForm.pid || pid.value || '0'
break
}
if (nodeType.value === 'dataset') {
params.union = union
params.allFields = allfields
}
if (cmd.value === 'move' && !checkPid(params.pid)) {
return
}
loading.value = true
const req =
cmd.value === 'move' ? moveDatasetTree : params.id ? renameDatasetTree : createDatasetTree
req(params)
.then(res => {
dataset.value.resetFields()
createDataset.value = false
emits('finish', res)
switch (cmd.value) {
case 'move':
ElMessage.success(t('data_set.moved_successfully'))
break
case 'rename':
ElMessage.success(t('data_set.rename_successful'))
break
default:
emits('onDatasetSave')
ElMessage.success(t('common.save_success'))
break
}
})
.finally(() => {
loading.value = false
})
}
})
}
defineExpose({
createInit,
editeInit
})
const emits = defineEmits(['finish', 'onDatasetSave'])
</script>
<template>
<el-dialog
:title="dialogTitle"
v-model="createDataset"
class="create-dialog"
:width="cmd === 'move' ? '600px' : '420px'"
:before-close="resetForm"
>
<el-form
label-position="top"
require-asterisk-position="right"
ref="dataset"
@keydown.stop.prevent.enter
:model="datasetForm"
:rules="datasetFormRules"
>
<el-form-item v-if="showName" :label="labelName" prop="name">
<el-input :placeholder="placeholder" v-model="datasetForm.name" />
</el-form-item>
<el-form-item v-if="showPid" :label="t('deDataset.folder')" prop="pid">
<el-tree-select
v-model="datasetForm.pid"
:data="state.tData"
popper-class="dataset-tree-select"
:render-after-expand="false"
style="width: 100%"
:props="props"
@node-click="nodeClick"
:filter-node-method="filterMethod"
filterable
>
<template #default="{ data: { name } }">
<el-icon>
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<span :title="name">{{ name }}</span>
</template>
</el-tree-select>
</el-form-item>
<div v-if="cmd === 'move'">
<el-input style="margin-bottom: 12px" v-model="filterText" clearable>
<template #prefix>
<el-icon>
<Icon name="icon_search-outline_outlined"
><icon_searchOutline_outlined class="svg-icon"
/></Icon>
</el-icon>
</template>
</el-input>
<div class="tree-content">
<el-tree
ref="treeRef"
:filter-node-method="filterNode"
filterable
v-model="datasetForm.pid"
menu
empty-text=""
:data="state.tData"
:props="props"
@node-click="nodeClick"
>
<template #default="{ data }">
<span class="custom-tree-node">
<el-icon style="font-size: 18px">
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<span class="node-text" :title="data.name">{{ data.name }}</span>
</span>
</template>
</el-tree>
<div v-if="searchEmpty" class="empty-search">
<img :src="nothingTree" />
<span>{{ t('data_set.relevant_content_found') }}</span>
</div>
</div>
</div>
</el-form>
<template #footer>
<el-button secondary @click="resetForm">{{ t('dataset.cancel') }} </el-button>
<el-button v-loading="loading" type="primary" @click="saveDataset"
>{{ t('dataset.confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<style lang="less" scoped>
.tree-content {
width: 552px;
height: 380px;
border: 1px solid #dee0e3;
border-radius: 4px;
padding: 8px;
overflow-y: auto;
.custom-tree-node {
display: flex;
align-items: center;
.node-text {
margin-left: 8.75px;
width: 120px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
:deep(.highLight) {
color: var(--el-color-primary, #3370ff);
}
}
}
.empty-search {
width: 100%;
margin-top: 57px;
display: flex;
flex-direction: column;
align-items: center;
img {
width: 100px;
height: 100px;
margin-bottom: 8px;
}
span {
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: #646a73;
}
}
}
</style>
<style lang="less">
.dataset-tree-select {
.ed-select-dropdown__item {
display: flex;
align-items: center;
.ed-icon {
margin-right: 5px;
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,251 @@
<script lang="ts" setup>
import custom_sort from '@/assets/svg/custom_sort.svg'
import dvRename from '@/assets/svg/dv-rename.svg'
import icon_calendar_outlined from '@/assets/svg/icon_calendar_outlined.svg'
import icon_copy_outlined from '@/assets/svg/icon_copy_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import icon_local_outlined from '@/assets/svg/icon_local_outlined.svg'
import icon_number_outlined from '@/assets/svg/icon_number_outlined.svg'
import icon_url_outlined from '@/assets/svg/icon_url_outlined.svg'
import icon_switch_outlined from '@/assets/svg/icon_switch_outlined.svg'
import icon_text_outlined from '@/assets/svg/icon_text_outlined.svg'
import more_v from '@/assets/svg/more_v.svg'
import { ref, computed, nextTick } from 'vue'
import { ElCascaderPanel } from 'element-plus-secondary'
import { timeTypes } from './util'
import { fieldType } from '@/utils/attr'
import { useI18n } from '@/hooks/web/useI18n'
export interface Menu {
svgName: string
label?: string
command: string
divided?: boolean
}
const { t } = useI18n()
const props = defineProps({
extField: {
type: Number,
default: 0
},
showTime: {
type: Boolean,
default: false
},
transType: {
type: String,
default: ''
}
})
const timeTypesChildren = timeTypes.map(ele => {
return {
label: ele === 'custom' ? t('data_set.customize') : ele,
value: ele
}
})
const options = computed(() => {
const optionArr = [
{
label: props.transType,
value: 'translate',
icon: icon_switch_outlined
},
{
label: t('data_set.change_field_type'),
value: 'translateType',
icon: custom_sort,
children: [
{
label: t('data_set.text'),
icon: icon_text_outlined,
value: 'text'
},
{
label: t('data_set.time'),
icon: icon_calendar_outlined,
value: 'time',
children: props.showTime ? timeTypesChildren : []
},
{
label: t('data_set.geographical_location'),
icon: icon_local_outlined,
value: 'location'
},
{
label: t('data_set.numerical_value'),
icon: icon_number_outlined,
value: 'value'
},
{
label: t('data_set.numeric_value_decimal'),
icon: icon_number_outlined,
value: 'float'
},
{
label: 'URL',
icon: icon_url_outlined,
value: 'url'
}
]
},
{
label: t('data_set.edit'),
value: 'editor',
icon: icon_edit_outlined
},
{
label: t('data_set.rename'),
value: 'rename',
icon: dvRename
},
{
label: t('data_set.copy'),
value: 'copy',
icon: icon_copy_outlined
},
{
label: t('data_set.delete'),
value: 'delete',
icon: icon_deleteTrash_outlined
}
]
if (![2, 3].includes(props.extField)) {
optionArr.splice(2, 1)
}
if ([3].includes(props.extField)) {
optionArr.splice(0, 1)
optionArr.splice(0, 1)
}
return optionArr
})
const deTypeArr = ref([])
const popover = ref()
const level = ref(1)
const cascaderPanel = ref()
const handleExpand = val => {
level.value = val.left + 1
}
const emit = defineEmits(['handleCommand'])
const handleCommand = () => {
const [translate, fieldType, timeType] = deTypeArr.value
popover.value.hide()
emit('handleCommand', timeType || fieldType || translate)
nextTick(() => {
deTypeArr.value = []
})
}
const handleChange = () => {
handleCommand()
}
</script>
<template>
<el-popover
:popper-class="
options.length === 6
? 'menu-more_popper_one menu-more_popper_six'
: extField === 3
? 'menu-more_popper_one menu-more_popper_three'
: 'menu-more_popper_one'
"
:persistent="false"
ref="popover"
placement="right"
:width="level * 175"
trigger="click"
>
<template #reference>
<el-icon class="menu-more">
<Icon name="more_v"><more_v class="svg-icon" /></Icon>
</el-icon>
</template>
<ElCascaderPanel
v-model="deTypeArr"
@expand-change="handleExpand"
ref="cascaderPanel"
:border="false"
:options="options"
@change="handleChange"
>
<template #default="{ data }">
<div class="flex-align-center icon">
<el-icon v-if="data.icon">
<Icon
><component
class="svg-icon"
:class="
['text', 'location', 'value', 'float', 'time', 'url'].includes(data.value) &&
`field-icon-${fieldType[['float', 'value'].includes(data.value) ? 2 : 0]}`
"
:is="data.icon"
></component
></Icon>
</el-icon>
<span>
{{ data.label }}
</span>
</div>
</template>
</ElCascaderPanel>
</el-popover>
</template>
<style lang="less" scoped>
.menu-more {
cursor: pointer;
height: 24px;
width: 24px;
border-radius: 4px;
font-size: 16px;
&:hover {
background: rgba(31, 35, 41, 0.1);
}
}
</style>
<style lang="less">
.menu-more_popper_six > :first-child > :first-child > :first-child {
height: 210px;
}
.menu-more_popper_three > :first-child > :first-child > :first-child {
height: 145px;
}
.menu-more_popper_one {
padding: 0 !important;
background: transparent !important;
box-shadow: none !important;
border: none !important;
.ed-cascader-node.in-active-path {
color: #1f2329;
font-weight: 400;
}
.ed-cascader-panel {
.ed-cascader-menu {
border-radius: 4px;
border: 1px solid #dee0e3;
background: #fff;
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.1);
border-right: none;
&:nth-child(2) {
> div {
height: 210px;
}
}
&:nth-child(3) {
margin-top: 64px;
}
.arrow-right {
position: absolute;
top: 50%;
right: 11px;
transform: translateY(-50%);
}
}
}
}
</style>

View File

@ -0,0 +1,167 @@
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import UnionFieldList from './UnionFieldList.vue'
import UnionItemEdit from './UnionItemEdit.vue'
import type { Field, NodeType, UnionType, Node } from './util'
import { getTableField } from '@/api/dataset'
import { cloneDeep } from 'lodash-es'
const changeParentFields = val => {
parent.currentDsFields = val
}
const changeNodeFields = val => {
node.currentDsFields = val
}
const changeUnionFields = (index?: number) => {
if (index !== undefined) {
node.unionFields.splice(index, 1)
} else {
node.unionFields.push({
parentField: null,
currentField: null
})
}
}
const defaultNode = {
info: '',
tableName: '',
type: 'db' as NodeType,
datasourceId: '',
id: '',
unionType: 'left' as UnionType,
unionFields: [],
currentDsFields: [],
sqlVariableDetails: null,
confirm: false,
isShadow: false,
flag: ''
}
const parentField = ref<Field[]>([])
const nodeField = ref<Field[]>([])
const node = reactive<Node>(cloneDeep(defaultNode))
const parent = reactive<Node>(cloneDeep(defaultNode))
const props = defineProps({
editArr: {
type: Array,
default: () => []
}
})
const clearState = () => {
Object.assign(node, cloneDeep(defaultNode))
Object.assign(parent, cloneDeep(defaultNode))
parentField.value = []
nodeField.value = []
}
const initState = () => {
node.confirm = false
node.isShadow = false
node.flag = ''
parent.confirm = false
parent.isShadow = false
parent.flag = ''
Object.assign(node, cloneDeep(props.editArr[0]))
Object.assign(parent, cloneDeep(props.editArr[1]))
getFields()
}
const getParams = (obj: Node) => {
return ['datasourceId', 'id', 'info', 'tableName', 'type'].reduce((pre, next) => {
pre[next] = obj[next]
return pre
}, {})
}
const getFields = async () => {
const [n, p] = props.editArr as Node[]
const [nr, pr] = await Promise.all([getTableField(getParams(n)), getTableField(getParams(p))])
parentField.value = pr as unknown as Field[]
parentField.value.forEach(ele => {
ele.checked = p.currentDsFields.map(ele => ele.originName).includes(ele.originName)
})
nodeField.value = nr as unknown as Field[]
nodeField.value.forEach(ele => {
ele.checked = n.currentDsFields.map(ele => ele.originName).includes(ele.originName)
})
}
defineExpose({
node,
parent,
clearState,
initState
})
</script>
<template>
<div style="height: 100%; overflow-y: auto">
<div class="field-style">
<div class="fields" v-loading="!parentField.length">
<p :title="parent.tableName">
{{ parent.tableName }}
</p>
<union-field-list
:field-list="parentField"
v-if="parentField.length"
:node="parent"
@checkedFields="changeParentFields"
/>
</div>
<div class="fields" v-loading="!nodeField.length">
<p :title="node.tableName">
{{ node.tableName }}
</p>
<union-field-list
:field-list="nodeField"
:node="node"
v-if="nodeField.length"
@checkedFields="changeNodeFields"
/>
</div>
</div>
<union-item-edit
:parent-field-list="parentField"
:node-field-list="nodeField"
:node="node"
@change-union-type="val => (node.unionType = val)"
v-if="node.tableName"
@change-union-fields="changeUnionFields"
:table-name="parent.tableName"
/>
</div>
</template>
<style lang="less" scoped>
.field-style {
height: 430px;
border: 1px solid var(--deCardStrokeColor, #dee0e3);
border-radius: 4px;
width: 100%;
box-sizing: border-box;
display: flex;
margin-bottom: 36px;
}
.fields {
box-sizing: border-box;
padding: 16px;
width: 50%;
& > p {
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-weight: 500;
margin: 0;
margin-bottom: 16px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
color: var(--deTextPrimary, #1f2329);
}
&:nth-child(1) {
border-right: 1px solid var(--deCardStrokeColor, #dee0e3);
}
}
</style>

View File

@ -0,0 +1,200 @@
<script lang="ts" setup>
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { ref, PropType, watch } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { propTypes } from '@/utils/propTypes'
import { ElTable } from 'element-plus-secondary'
import { fieldType } from '@/utils/attr'
import { type Field } from './util'
import { iconFieldMap } from '@/components/icon-group/field-list'
const { t } = useI18n()
const props = defineProps({
fieldList: {
type: Array as PropType<Field[]>,
default: () => []
},
node: propTypes.object.def({})
})
const emit = defineEmits(['checkedFields'])
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
const search = ref('')
const checkAll = ref(false)
const isIndeterminate = ref(true)
const handleCheckAllChange = (val: boolean) => {
fieldSearchList.value.forEach(ele => {
ele.checked = val
})
const org = fieldSearchList.value.map(ele => ele.originName)
if (val) {
multipleSelection.value = [
...multipleSelection.value.filter(ele => !org.includes(ele.originName)),
...fieldSearchList.value
]
} else {
multipleSelection.value = multipleSelection.value.filter(ele => !org.includes(ele.originName))
}
isIndeterminate.value = false
emit('checkedFields', multipleSelection.value)
}
const fieldSearchList = ref([])
const multipleSelection = ref<Field[]>([])
const checkChange = () => {
handleSelectionChange(fieldSearchList.value.filter(ele => ele.checked))
}
const handleSelectionChange = val => {
const checkedCount = val.length
checkAll.value = checkedCount === fieldSearchList.value.length
isIndeterminate.value = checkedCount > 0 && checkedCount < fieldSearchList.value.length
multipleSelection.value = props.fieldList.filter(ele => ele.checked)
emit('checkedFields', multipleSelection.value)
}
watch(
search,
val => {
if (val.trim() !== '') {
fieldSearchList.value = props.fieldList.filter(ele =>
ele.originName.toLocaleLowerCase().includes(val.trim().toLocaleLowerCase())
)
} else {
fieldSearchList.value = props.fieldList
}
handleSelectionChange(fieldSearchList.value.filter(ele => ele.checked))
},
{
immediate: true
}
)
watch(
() => props.fieldList,
() => {
fieldSearchList.value = props.fieldList
handleSelectionChange(fieldSearchList.value.filter(ele => ele.checked))
}
)
</script>
<template>
<div class="field-block-style">
<div class="field-block-option">
<span class="option-field"
>{{ $t('dataset.field_select') }}({{ multipleSelection.length }}/{{
fieldList.length
}})</span
>
<el-input
v-model="search"
:placeholder="$t('auth.search_by_field')"
clearable
class="option-input"
>
<template #prefix>
<el-icon>
<Icon name="icon_search-outline_outlined"
><icon_searchOutline_outlined class="svg-icon"
/></Icon>
</el-icon>
</template>
</el-input>
</div>
<div class="field-block-body">
<el-table
header-cell-class-name="header-cell"
ref="multipleTableRef"
:data="fieldSearchList"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column align="center" width="55">
<template #header>
<el-checkbox
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
/>
</template>
<template #default="scope">
<el-checkbox @change="checkChange" v-model="scope.row.checked" />
</template>
</el-table-column>
<el-table-column :label="t('dataset.origin_name')">
<template #default="scope">
<el-icon>
<Icon
><component
class="svg-icon"
:class="`field-icon-${fieldType[scope.row.deType]}`"
:is="iconFieldMap[fieldType[scope.row.deType]]"
></component
></Icon>
</el-icon>
{{ scope.row.originName }}
</template>
</el-table-column>
<el-table-column property="description" :label="t('deDataset.description')" />
</el-table>
</div>
</div>
</template>
<style lang="less" scoped>
.field-block-style {
height: 100%;
width: 100%;
font-family: var(--de-custom_font, 'PingFang');
.field-block-body {
height: 327px;
overflow-y: auto;
}
.field-origin-style {
display: flex;
margin-left: 12px;
width: 140px;
align-items: center;
font-size: 14px;
font-weight: 500;
color: var(--deTextSecondary, #646a73);
}
.field-style {
width: 140px;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
font-size: 14px;
font-weight: 500;
color: var(--deTextSecondary, #646a73);
}
.field-block-option {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.option-field {
font-size: 14px;
font-weight: 400;
color: var(--deTextSecondary, #646a73);
}
.option-input {
width: 200px;
}
}
</style>
<style lang="less">
.field-block-body {
.cell {
display: flex;
align-items: center;
.ed-icon {
font-size: 14px;
margin-right: 5.25px;
}
}
}
</style>

View File

@ -0,0 +1,297 @@
<script lang="ts" setup>
import noJoin from '@/assets/svg/no-join.svg'
import icon_fullAssociation from '@/assets/svg/icon_full-association.svg'
import icon_intersect from '@/assets/svg/icon_intersect.svg'
import icon_leftAssociation from '@/assets/svg/icon_left-association.svg'
import icon_rightAssociation from '@/assets/svg/icon_right-association.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import joinJoin from '@/assets/svg/join-join.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
import { PropType, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import type { Field } from '@/api/chart'
import { fieldType } from '@/utils/attr'
import { iconFieldMap } from '@/components/icon-group/field-list'
const unionTypeFromParent = ref('left')
const { t } = useI18n()
const iconName = {
left: icon_leftAssociation,
right: icon_rightAssociation,
inner: icon_intersect,
full: icon_fullAssociation
}
const props = defineProps({
tableName: {
type: String,
default: 'left'
},
parentFieldList: {
type: Array as PropType<Field[]>,
default: () => []
},
nodeFieldList: {
type: Array as PropType<Field[]>,
default: () => []
},
node: {
type: Object,
default: () => ({})
}
})
const unionOptions = [
{ label: t('dataset.left_join'), value: 'left' },
{ label: t('dataset.right_join'), value: 'right' },
{ label: t('dataset.inner_join'), value: 'inner' },
{ label: t('dataset.full_join'), value: 'full' }
]
const init = () => {
unionTypeFromParent.value = props.node.unionType
if (props.node.unionFields.length < 1) {
addUnion()
}
}
const emit = defineEmits(['changeUnionFields', 'changeUnionType'])
const addUnion = () => {
emit('changeUnionFields')
}
const removeUnionItem = index => {
emit('changeUnionFields', index)
}
init()
</script>
<template>
<div class="union-container">
<div class="union-header">
{{ t('dataset.union_relation') }}
<div class="union-header-operator">
<el-select
v-model="unionTypeFromParent"
class="union-selector"
@change="emit('changeUnionType', unionTypeFromParent)"
>
<template #prefix>
<el-icon>
<Icon
><component
class="svg-icon"
:is="iconName[unionTypeFromParent] || noJoin"
></component
></Icon>
</el-icon>
</template>
<el-option
v-for="item in unionOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button type="primary" class="union-add" @click="addUnion">
<template #icon>
<el-icon>
<Icon name="icon_add_outlined"><icon_add_outlined class="svg-icon" /></Icon>
</el-icon>
</template>
{{ t('dataset.add_union_field') }}
</el-button>
</div>
</div>
<div class="union-body">
<div class="union-body-header">
<span class="column" :title="tableName">{{ tableName }}</span>
<span class="column" :title="node.tableName">{{ node.tableName }}</span>
</div>
<div class="union-body-container">
<div v-for="(field, index) in node.unionFields" :key="index" class="union-body-item">
<!--左侧父级field-->
<span class="column">
<el-select
v-model="field.parentField"
:placeholder="t('dataset.pls_slc_union_field')"
filterable
value-key="originName"
clearable
class="select-field"
>
<el-option
v-for="item in parentFieldList"
:key="item.originName"
:label="item.name"
:value="item"
>
<el-icon>
<Icon
><component
class="svg-icon"
:class="`field-icon-${fieldType[item.deType]}`"
:is="iconFieldMap[fieldType[item.deType]]"
></component
></Icon>
</el-icon>
<span>
{{ item.name }}
</span>
</el-option>
</el-select>
</span>
<el-icon>
<Icon name="join-join"><joinJoin class="svg-icon" /></Icon>
</el-icon>
<!--右侧孩子field-->
<span class="column">
<el-select
v-model="field.currentField"
:placeholder="t('dataset.pls_slc_union_field')"
filterable
clearable
value-key="originName"
class="select-field"
>
<el-option
v-for="item in nodeFieldList"
:key="item.originName"
:label="item.name"
:value="item"
>
<el-icon>
<Icon
><component
class="svg-icon"
:class="`field-icon-${fieldType[item.deType]}`"
:is="iconFieldMap[fieldType[item.deType]]"
></component
></Icon>
</el-icon>
<span>
{{ item.name }}
</span>
</el-option>
</el-select>
</span>
<span class="column-last">
<el-button
v-if="node.unionFields && node.unionFields.length > 1"
text
@click="removeUnionItem(index)"
>
<template #icon>
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
</template>
</el-button>
</span>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.union-container {
height: 275px;
font-family: var(--de-custom_font, 'PingFang');
}
.union-header {
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
margin-bottom: 8px;
color: var(--deTextPrimary, #1f2329);
font-size: 16px;
font-weight: 500;
}
.union-header-operator {
display: flex;
align-items: center;
justify-content: flex-end;
position: relative;
.select-svg-icon {
position: absolute;
left: 12px;
top: 50%;
z-index: 2;
transform: translateY(-50%);
}
}
.union-selector {
width: 180px;
:deep(.ed-select__prefix--light) {
border-right: none;
font-size: 22px;
padding: 0;
}
}
.union-add {
margin-left: 12px;
}
.union-body {
height: 240px;
width: 100%;
}
.union-body-header {
height: 22px;
display: flex;
align-items: center;
font-size: 14px;
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
font-weight: 400;
margin: 20px 0 8px 0;
color: var(--deTextSecondary, #1f2329);
}
.union-body-header .column {
width: 364px;
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:nth-child(2) {
margin-left: 37px;
}
}
.union-body-header .column-last {
width: 40px;
text-align: center;
}
.union-body-container {
height: 180px;
overflow-y: auto;
}
.select-field {
width: 364px;
display: inline-block;
}
.union-body-item {
height: 32px;
align-items: center;
display: flex;
margin-bottom: 8px;
font-size: 18px;
& > .ed-icon {
margin: 0 9px;
}
}
.union-body-item .column {
width: 364px;
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.union-body-item .column-last {
margin-left: auto;
:deep(.ed-icon) {
font-size: 16px;
color: var(--deTextSecondary, #646a73);
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,172 @@
import { useI18n } from '@/hooks/web/useI18n'
import SnowflakeId from 'snowflake-id'
const snowflake = new SnowflakeId()
const { t } = useI18n()
const guid = () => {
return snowflake.generate()
}
const timestampFormatDate = (timestamp, showMs?: boolean) => {
if (!timestamp || timestamp === -1) {
return '-'
}
const date = new Date(timestamp)
const y = date.getFullYear()
let MM = date.getMonth() + 1
MM = (MM < 10 ? '0' + MM : MM) as number
let d = date.getDate()
d = (d < 10 ? '0' + d : d) as number
let h = date.getHours()
h = (h < 10 ? '0' + h : h) as number
let m = date.getMinutes()
m = (m < 10 ? '0' + m : m) as number
let s = date.getSeconds()
s = (s < 10 ? '0' + s : s) as number
let format = y + '-' + MM + '-' + d + ' ' + h + ':' + m + ':' + s
if (showMs === true) {
const ms = date.getMilliseconds()
format += ':' + ms
}
return format
}
const defaultValueScopeList = [
{ label: t('dataset.scope_edit'), value: 'EDIT' },
{ label: t('dataset.scope_all'), value: 'ALLSCOPE' }
]
const fieldOptions = [
{ label: t('dataset.text'), value: 'TEXT' },
{ label: t('dataset.value'), value: 'LONG' },
{
label: t('dataset.value') + '(' + t('dataset.float') + ')',
value: 'DOUBLE'
},
{ label: t('dataset.time_year'), value: 'DATETIME-YEAR' },
{
label: t('dataset.time_year_month'),
value: 'DATETIME-YEAR-MONTH',
children: [
{
value: 'YYYY-MM',
label: 'YYYY-MM'
},
{
value: 'YYYY/MM',
label: 'YYYY/MM'
}
]
},
{
label: t('dataset.time_year_month_day'),
value: 'DATETIME-YEAR-MONTH-DAY',
children: [
{
value: 'YYYY-MM-DD',
label: 'YYYY-MM-DD'
},
{
value: 'YYYY/MM/DD',
label: 'YYYY/MM/DD'
}
]
},
{
label: t('dataset.time_all'),
value: 'DATETIME',
children: [
{
value: 'YYYY-MM-DD HH:mm:ss',
label: 'YYYY-MM-DD HH:MI:SS'
},
{
value: 'YYYY/MM/DD HH:mm:ss',
label: 'YYYY/MM/DD HH:MI:SS'
}
]
}
]
const getFieldName = (fields, name) => {
let n = name
n = n + '_copy'
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
if (field.name === n) {
n = getFieldName(fields, n)
}
}
return n
}
const timeTypes = [
'yyyy-MM-dd',
'yyyy/MM/dd',
'yyyy-MM-dd HH:mm:ss',
'yyyy/MM/dd HH:mm:ss',
'custom'
]
type NodeType = 'db' | 'sql'
type UnionType = 'left' | 'right' | 'inner'
interface UnionField {
currentField: Field
parentField: Field
}
interface Node {
tableName: string
type: NodeType
datasourceId: string
id: string
unionType: UnionType
unionFields: UnionField[]
info: string
sqlVariableDetails: string
currentDsFields: Field[]
children?: Node[]
confirm?: boolean
isShadow?: boolean
flag?: string
}
interface Field {
checked: boolean
deExtractType: number
deType: number
name: string
type: string
originName: string
id: string
}
interface DataSource {
id: string
name: string
children?: DataSource[]
}
export {
NodeType,
UnionType,
UnionField,
DataSource,
Node,
Field,
timestampFormatDate,
defaultValueScopeList,
fieldOptions,
guid,
getFieldName,
timeTypes
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
function formatEnum(ele) {
return {
value: ele,
label: `chart.filter_${ele.replace(' ', '_')}`
}
}
function toLine(name) {
return name.replace(/([A-Z])/g, '_$1').toLowerCase()
}
const textEnum = ['eq', 'not_eq', 'like', 'not like', 'null', 'not_null', 'empty', 'not_empty']
const textOptions = textEnum.map(formatEnum)
const dateEnum = ['eq', 'not_eq', 'lt', 'gt', 'le', 'ge']
const dateOptions = dateEnum.concat(['null', 'not_null']).map(formatEnum)
const valueEnum = [...dateEnum]
const valueOptions = valueEnum.map(formatEnum)
const sysParams = ['eq', 'not_eq', 'like', 'not like', 'in', 'not in']
const textOptionsForSysParams = sysParams.map(formatEnum)
const sysParamsEnum = ['userId', 'userName', 'userEmail']
const sysParamsIlns = sysParamsEnum.map(_ => {
return { value: `\${sysParams.${_}}`, label: `auth.sysParams_type.${toLine(_)}` }
})
const fieldEnums = ['text', 'time', 'value', 'value', 'value', 'location', 'binary', 'url']
export {
textOptions,
dateOptions,
valueOptions,
textOptionsForSysParams,
sysParamsIlns,
fieldEnums
}

View File

@ -0,0 +1,86 @@
<script lang="ts" setup>
import icon_expandRight_filled from '@/assets/svg/icon_expand-right_filled.svg'
import { ref } from 'vue'
import { propTypes } from '@/utils/propTypes'
defineProps({
name: propTypes.string.def('')
})
const active = ref(true)
defineExpose({
active
})
</script>
<template>
<div :class="[active ? 'active' : 'deactivate', 'base-info-content']">
<p class="title" @click="active = !active">
<el-icon style="font-size: 10px">
<Icon name="icon_expand-right_filled"><icon_expandRight_filled class="svg-icon" /></Icon>
</el-icon>
<span class="name">{{ name }}</span>
</p>
<slot :active="active"></slot>
</div>
</template>
<style lang="less" scoped>
.base-info-content {
padding: 24px;
border-radius: 4px;
// background: #fff;
margin: 24px 24px 0 24px;
position: relative;
& + .base-info-content {
margin-top: 16px;
}
.update-records-time {
color: #646a73;
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
margin-left: 8px;
}
.title {
display: flex;
align-items: center;
cursor: pointer;
}
.name {
color: #fff;
font-family: var(--de-custom_font, 'PingFang');
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
margin-left: 8px;
}
&.active {
.title {
.ed-icon {
transform: rotate(90deg);
color: #B8BCBF;
}
}
overflow: auto;
height: auto;
}
&.deactivate {
height: 72px;
overflow: hidden;
.title {
.ed-icon {
transform: rotate(0);
color: var(--ed-color-primary);
}
}
}
}
</style>

View File

@ -0,0 +1,35 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
defineProps({
label: propTypes.string.def('')
})
</script>
<template>
<div class="base-info-item">
<p class="label">{{ label }}</p>
<p class="value">
<slot></slot>
</p>
</div>
</template>
<style lang="less">
.base-info-item {
margin-top: 16px;
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
width: 100%;
.label {
color: #B8BCBF;
}
.value {
margin-top: 4px;
color: #F2F4F5;
white-space: pre-wrap;
}
}
</style>

View File

@ -0,0 +1,78 @@
<script lang="ts" setup>
import icon_excel from '@/assets/svg/icon_excel.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
const props = withDefaults(
defineProps<{
name?: string
size?: number
showDel?: boolean
}>(),
{
name: '',
size: 0,
showDel: false
}
)
const emits = defineEmits(['del'])
const del = () => {
emits('del')
}
</script>
<template>
<div class="excel-info">
<el-icon class="excel">
<Icon name="icon_excel"><icon_excel class="svg-icon" /></Icon>
</el-icon>
<div class="info">
<p class="name ellipsis">{{ name || '-' }}</p>
<p class="size ellipsis">{{ size || '-' }}</p>
</div>
<el-icon v-if="showDel" @click="del" class="delete">
<Icon name="icon_delete-trash_outlined"><icon_deleteTrash_outlined class="svg-icon" /></Icon>
</el-icon>
</div>
</template>
<style lang="less" scoped>
.excel-info {
display: flex;
align-items: center;
width: 100%;
height: 58px;
padding: 0 16px 0 12px;
border-radius: 4px;
border: 1px solid #dee0e3;
.excel {
font-size: 32px;
margin-right: 14.67px;
}
.info {
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
font-weight: 400;
width: 80%;
.name {
color: #1f2329;
font-size: 14px;
line-height: 22px;
width: 100%;
}
.size {
color: #8f959e;
font-size: 12px;
line-height: 20px;
}
}
.delete {
cursor: pointer;
font-size: 16px;
margin-left: auto;
}
}
</style>

View File

@ -0,0 +1,176 @@
<script lang="ts" setup>
import icon_succeed_colorful from '@/assets/svg/icon_succeed_colorful.svg'
import icon_dataset from '@/assets/svg/icon_dataset.svg'
import { ref } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { useCache } from '@/hooks/web/useCache'
import { setShowFinishPage } from '@/api/datasource'
import { useI18n } from '@/hooks/web/useI18n'
defineProps({
name: propTypes.string.def(''),
disabled: propTypes.bool.def(false)
})
const { t } = useI18n()
const { wsCache } = useCache()
const emits = defineEmits(['createDataset', 'backToDatasourceList', 'continueCreating'])
const checked = ref(false)
const createDataset = () => {
emits('createDataset')
}
const backToDatasourceList = () => {
emits('backToDatasourceList')
}
const continueCreating = () => {
emits('continueCreating')
}
checked.value = wsCache.get('ds-create-success') || false
const handleChange = (val: boolean) => {
setShowFinishPage({})
wsCache.set('ds-create-success', val)
emits('backToDatasourceList')
}
</script>
<template>
<div class="finish-page-content">
<div class="finish-page">
<el-icon class="succeed-icon">
<Icon name="icon_succeed_colorful"><icon_succeed_colorful class="svg-icon" /></Icon>
</el-icon>
<div class="succeed-text">{{ t('data_source.successfully_created') }}</div>
<div class="btn-list">
<el-button @click="continueCreating" secondary>
{{ t('data_source.continue_to_create') }}
</el-button>
<el-button @click="backToDatasourceList" type="primary">
{{ t('data_source.data_source_list') }}
</el-button>
</div>
<div class="nolonger-tips">
<el-checkbox
@change="handleChange"
v-model="checked"
:label="t('data_source.prompts_next_time')"
/>
</div>
<div class="maybe-want" v-permission="['dataset']">
<div class="title">{{ t('data_source.also_want_to') }}</div>
<div class="ds-info">
<el-icon class="ds">
<Icon name="icon_dataset"><icon_dataset class="svg-icon" /></Icon>
</el-icon>
<div class="info">
<p class="name">{{ $t('auth.dataset') }}</p>
<p class="size">{{ t('data_source.or_large_screen') }}</p>
</div>
<el-button class="create" secondary :disabled="disabled" @click="createDataset">
{{ t('data_source.go_to_create') }}
</el-button>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.finish-page-content {
width: 100%;
height: 100%;
background: rgb(0, 0, 0);
display: flex;
justify-content: center;
position: absolute;
bottom: 0;
left: 0;
z-index: 10;
.ed-button,
:deep(.ed-checkbox__label) {
font-weight: 400;
}
.finish-page {
width: 592px;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 83px;
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
font-weight: 400;
.succeed-icon {
font-size: 58px;
color: #34c724;
}
.succeed-text {
color: #ffffff;
font-size: 20px;
font-weight: 500;
line-height: 28px;
margin: 16px 0;
}
.btn-list {
margin-bottom: 16px;
}
.nolonger-tips {
margin-bottom: 42px;
}
.maybe-want {
width: 100%;
.title {
font-size: 14px;
font-weight: 500;
line-height: 22px;
width: 100%;
margin-bottom: 8px;
}
.ds-info {
display: flex;
align-items: center;
width: 100%;
height: 82px;
padding: 0 16px 0 12px;
border-radius: 4px;
border: 1px solid #dee0e3;
.ds {
font-size: 32px;
margin-right: 14.67px;
}
.info {
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
font-weight: 400;
.name {
color: #e9e9e9;
font-size: 14px;
line-height: 22px;
}
.size {
color: #8f959e;
font-size: 12px;
line-height: 20px;
}
}
.create {
cursor: pointer;
margin-left: auto;
line-height: 22px;
}
}
}
}
}
</style>

View File

@ -0,0 +1,204 @@
<script lang="ts" setup>
import icon_expandLeft_filled from '@/assets/svg/icon_expand-left_filled.svg'
import icon_expandRight_filled from '@/assets/svg/icon_expand-right_filled.svg'
import { toRefs, ref, watch, nextTick } from 'vue'
import { propTypes } from '@/utils/propTypes'
const props = defineProps({
tabList: propTypes.arrayOf(
propTypes.shape({
label: String,
value: String
})
),
activeTab: propTypes.string.def('')
})
const activeTabIndex = ref(0)
const emits = defineEmits(['TabClick'])
const { activeTab } = toRefs(props)
const handleTabClick = tab => {
let tabDom = document.getElementById(`tab-${tab.value}`)
if (tabDom.offsetLeft + tabDom.offsetWidth > tabWrapper.value.offsetWidth) {
tabWrapper.value.scrollLeft =
tabDom.offsetLeft + tabDom.offsetWidth - tabWrapper.value.offsetWidth
} else {
tabWrapper.value.scrollLeft = 0
}
emits('TabClick', tab)
}
const tabWrapper = ref()
const showBtn = ref(false)
watch(
() => activeTab.value,
val => {
activeTabIndex.value = props.tabList.findIndex(ele => ele.value === val)
},
{ immediate: true }
)
watch(
() => props.tabList,
() => {
nextTick(() => {
showBtn.value = tabWrapper.value.scrollWidth > tabWrapper.value.offsetWidth
})
},
{ immediate: true }
)
const prevClick = () => {
let domWrapper = tabWrapper.value
if (!domWrapper.scrollLeft) return
domWrapper.scrollLeft -= 30
}
const nextClick = () => {
let domWrapper = tabWrapper.value
domWrapper.scrollLeft += 30
}
</script>
<template>
<div class="sheet-tabs">
<div ref="tabWrapper" class="tab-wrapper">
<div
v-for="tab in tabList"
:key="tab.label"
:id="`tab-${tab.value}`"
:title="tab.label"
:class="[{ active: activeTab === tab.value }, 'sheet-tab']"
@click="handleTabClick(tab)"
>
<span class="ellipsis">
{{ tab.label }}
</span>
</div>
</div>
<div class="tab-btn" v-if="showBtn">
<el-icon size="12px" @click="prevClick">
<Icon name="icon_expand-left_filled"><icon_expandLeft_filled class="svg-icon" /></Icon>
</el-icon>
<el-icon size="12px" @click="nextClick">
<Icon name="icon_expand-right_filled"><icon_expandRight_filled class="svg-icon" /></Icon>
</el-icon>
</div>
</div>
</template>
<style lang="less" scoped>
.sheet-tabs {
border-top-left-radius: 3px;
width: 100%;
position: relative;
padding-right: 60px;
.tab-wrapper {
height: 100%;
display: flex;
overflow-x: auto;
&::-webkit-scrollbar {
display: none;
}
}
.tab-btn {
padding: 8px 12px;
display: flex;
justify-content: space-between;
width: 60px;
height: 28px;
position: absolute;
right: 0;
top: 4px;
background: #fff;
.ed-icon {
color: #8d9199;
cursor: pointer;
&.disabled {
cursor: not-allowed;
}
&:not(.disabled):hover {
color: var(--ed-color-primary);
}
& + .ed-icon {
margin-left: 12px;
}
}
}
.sheet-tab {
color: #1f2329;
cursor: pointer;
position: relative;
padding: 0 20px;
display: flex;
align-items: center;
height: 36px;
max-width: 200px;
border-bottom: 1px solid rgba(31, 35, 41, 0.15);
&:hover {
color: var(--ed-color-primary);
}
.ellipsis {
max-width: 200px;
font-size: 14px;
}
&::after,
&::before {
content: '';
position: absolute;
height: 24px;
width: 1px;
top: 50%;
transform: translateY(-50%);
background: rgba(31, 35, 41, 0.15);
}
&::after {
right: 0;
}
&::before {
left: 0;
}
& + .active {
::before {
content: '';
left: -3px;
height: 30px;
width: 2px;
position: absolute;
top: 0;
background: #fff;
}
}
}
.active {
box-shadow: 0px -1px 0px 0px #f5f6f7 inset;
color: var(--ed-color-primary);
border: 1px solid rgba(31, 35, 41, 0.15);
border-bottom: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background: #f5f6f7;
&::before,
&::after {
display: none;
}
& + .sheet-tab {
&::before {
display: none;
}
}
}
}
</style>

View File

@ -0,0 +1,75 @@
<script lang="ts" setup>
import { PropType, toRefs } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
export interface AuthConfig {
verification: string
username: string
password: string
}
const props = defineProps({
request: {
type: Object as PropType<{ authManager: AuthConfig }>,
default: () => ({})
}
})
const { t } = useI18n()
const { request } = toRefs(props)
const options = [{ name: 'No Auth' }, { name: 'Basic Auth' }]
const change = () => {
const { username, password, verification } = request.value.authManager
const isBasic = verification === 'Basic Auth'
request.value.authManager.username = isBasic ? username : ''
request.value.authManager.password = isBasic ? password : ''
}
</script>
<template>
<el-form label-position="top">
<el-form-item class="api-auth-config" :label="t('datasource.verification_method')">
<el-select
style="width: 100%"
v-model="request.authManager.verification"
@change="change"
:placeholder="t('datasource.verification_method')"
>
<el-option v-for="item in options" :key="item.name" :label="item.name" :value="item.name" />
</el-select>
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item
v-if="request.authManager.verification === 'Basic Auth'"
:label="t('datasource.username')"
>
<el-input
v-model="request.authManager.username"
:placeholder="t('datasource.username')"
class="ms-http-input"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
v-if="request.authManager.verification === 'Basic Auth'"
:label="t('datasource.password')"
>
<el-input
v-model="request.authManager.password"
type="password"
:placeholder="t('datasource.password')"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<style lang="less">
.api-auth-config {
margin-bottom: 16px !important;
}
</style>

View File

@ -0,0 +1,286 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { onBeforeMount, watch, toRefs, PropType } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import ApiVariable from './ApiVariable.vue'
import CodeEdit from './CodeEdit.vue'
import Convert from './convert.js'
import { KeyValue, BODY_TYPE } from './ApiTestModel.js'
export interface ApiBodyItem {
raw?: string
typeChange: string
format?: string
jsonSchema?: string
type?: string
kvs: Item[]
}
export interface Item {
name: string
value: string
description: string
type: string
}
const props = defineProps({
isReadOnly: propTypes.bool.def(false),
isShowEnable: propTypes.bool.def(false),
body: {
type: Object as PropType<ApiBodyItem>,
default: () => ({
raw: '',
typeChange: '',
format: '',
jsonSchema: '',
type: '',
kvs: []
})
},
headers: {
type: Array as PropType<Item[]>,
default: () => []
},
valueList: {
type: Array as PropType<Item[]>,
default: () => []
}
})
const { t } = useI18n()
const modes = ['text', 'json', 'xml', 'html']
const hasOwnProperty = Object.prototype.hasOwnProperty
const propIsEnumerable = Object.prototype.propertyIsEnumerable
const { body: apiBody, headers } = toRefs(props)
watch(
() => apiBody.value.raw,
() => {
if (apiBody.value.format !== 'JSON-SCHEMA' && apiBody.value.raw) {
try {
const MsConvert = new Convert()
const data = MsConvert.format(JSON.parse(apiBody.value.raw))
if (apiBody.value.jsonSchema) {
apiBody.value.jsonSchema = deepAssign(data)
} else {
apiBody.value.jsonSchema = data
}
} catch (ex) {
apiBody.value.jsonSchema = ''
}
}
}
)
onBeforeMount(() => {
if (!apiBody.value.type) {
apiBody.value.type = BODY_TYPE.FORM_DATA
}
if (apiBody.value.kvs) {
apiBody.value.kvs.forEach(param => {
if (!param.type) {
param.type = 'text'
}
})
}
})
const isObj = x => {
const type = typeof x
return x !== null && (type === 'object' || type === 'function')
}
const toObject = val => {
if (val === null || val === undefined) {
return
}
return Object(val)
}
const assignKey = (to, from, key) => {
const val = from[key]
if (val === undefined || val === null) {
return
}
if (!hasOwnProperty.call(to, key) || !isObj(val)) {
to[key] = val
} else {
to[key] = assign(Object(to[key]), from[key])
}
}
const assign = (to, from) => {
if (to === from) {
return to
}
from = Object(from)
for (const key in from) {
if (hasOwnProperty.call(from, key)) {
assignKey(to, from, key)
}
}
if (Object.getOwnPropertySymbols) {
const symbols = Object.getOwnPropertySymbols(from)
for (let i = 0; i < symbols.length; i++) {
if (propIsEnumerable.call(from, symbols[i])) {
assignKey(to, from, symbols[i])
}
}
}
return to
}
const deepAssign = function (target) {
target = toObject(target)
for (let s = 1; s < arguments.length; s++) {
assign(target, arguments[s])
}
return target
}
const modeChange = () => {
switch (apiBody.value.type) {
case 'JSON':
setContentType('application/json')
break
case 'XML':
setContentType('text/xml')
break
case 'WWW_FORM':
setContentType('application/x-www-form-urlencoded')
break
case 'BINARY':
setContentType('application/octet-stream')
break
default:
removeContentType()
break
}
}
const setContentType = value => {
let isType = false
headers.value.forEach(item => {
if (item.name === 'Content-Type') {
item.value = value
isType = true
}
})
if (!isType) {
headers.value.unshift(new KeyValue({ name: 'Content-Type', value: value }))
}
}
const removeContentType = () => {
for (const index in headers.value) {
if (headers.value[index].name === 'Content-Type') {
headers.value.splice(parseInt(index), 1)
emits('headersChange')
return
}
}
}
const changeParameters = val => {
if (!isNaN(val)) {
apiBody.value.kvs.splice(val, 1)
} else {
apiBody.value.kvs.push(val)
}
}
const emits = defineEmits(['headersChange'])
</script>
<template>
<div class="radio-group_api">
<el-radio-group v-model="apiBody.type">
<el-radio :disabled="isReadOnly" :label="BODY_TYPE.FORM_DATA" @change="modeChange">
{{ t('datasource.body_form_data') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="BODY_TYPE.WWW_FORM" @change="modeChange">
{{ t('datasource.body_x_www_from_urlencoded') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="BODY_TYPE.JSON" @change="modeChange">
{{ t('datasource.body_json') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="BODY_TYPE.XML" @change="modeChange">
{{ t('datasource.body_xml') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="BODY_TYPE.RAW" @change="modeChange">
{{ t('datasource.body_raw') }}
</el-radio>
</el-radio-group>
<div style="padding-top: 16px" v-if="apiBody.type == 'Form_Data' || apiBody.type == 'WWW_FORM'">
<api-variable
:is-read-only="isReadOnly"
:parameters="apiBody.kvs"
:is-show-enable="isShowEnable"
type="body"
@change-parameters="changeParameters"
:value-list="valueList"
/>
</div>
<div v-if="apiBody.type == 'JSON'" class="ms-body">
<code-edit
ref="codeEdit"
:read-only="isReadOnly"
v-model="apiBody.raw"
class="api-body-code"
:data="apiBody.raw"
:modes="modes"
mode="json"
height="200px"
/>
</div>
<div v-if="apiBody.type == 'XML'" class="ms-body">
<code-edit
ref="codeEdit"
:read-only="isReadOnly"
class="api-body-code"
v-model="apiBody.raw"
:data="apiBody.raw"
height="200px"
mode="xml"
:modes="modes"
/>
</div>
<div v-if="apiBody.type == 'Raw'" class="ms-body">
<code-edit
ref="codeEdit"
:read-only="isReadOnly"
v-model="apiBody.raw"
:data="apiBody.raw"
height="200px"
class="api-body-code"
:modes="modes"
/>
</div>
</div>
</template>
<style lang="less" scoped>
.radio-group_api {
:deep(.ed-radio) {
height: 22px !important;
}
}
.ms-body {
padding: 15px 0;
}
</style>
<style lang="less">
.ace_print-margin {
display: none;
}
.api-body-code {
border-radius: 4px;
border: 1px solid #bbbfc4;
}
</style>

View File

@ -0,0 +1,249 @@
<script lang="ts" setup>
import { ref, watch, onBeforeMount, PropType, toRefs } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import ApiKeyValue from './ApiKeyValue.vue'
import ApiBody from './ApiBody.vue'
import Pagination from './Pagination.vue'
import ApiVariable from './ApiVariable.vue'
import ApiAuthConfig from './ApiAuthConfig.vue'
import { Body } from './ApiTestModel.js'
import type { Item } from './ApiKeyValue.vue'
import type { AuthConfig } from './ApiAuthConfig.vue'
import type { ApiBodyItem } from './ApiBody.vue'
import { PageSetting } from '@/views/visualized/data/datasource/form/Pagination.vue'
export interface ApiRequest {
changeId: string
headers: Item[]
rest: Item[]
arguments: Item[]
authManager: AuthConfig
body: ApiBodyItem
page: PageSetting
}
const props = defineProps({
showScript: {
type: Boolean,
default: true
},
valueList: {
type: Array as PropType<Item[]>,
default: () => []
},
request: {
type: Object as PropType<ApiRequest>,
default: () => ({
changeId: '',
authManager: { verification: '', username: '', password: '' },
headers: [],
rest: [],
arguments: [],
body: {
typeChange: '',
kvs: []
},
page: {
pageType: 'empty',
requestData: [],
responseData: []
}
})
},
referenced: {
type: Boolean,
default: false
},
isShowEnable: {
type: Boolean,
default: false
},
isReadOnly: {
type: Boolean,
default: false
}
})
const { t } = useI18n()
const spanCount = ref(21)
const activeName = ref('headers')
const headerSuggestions = [
{ value: 'Accept' },
{ value: 'Accept-Charset' },
{ value: 'Accept-Language' },
{ value: 'Accept-Datetime' },
{ value: 'X-DE-TOKEN' },
{ value: 'Cache-Control' },
{ value: 'Connection' },
{ value: 'Cookie' },
{ value: 'Content-Length' },
{ value: 'Content-MD5' },
{ value: 'Content-Type' },
{ value: 'Date' },
{ value: 'Expect' },
{ value: 'From' },
{ value: 'Host' },
{ value: 'If-Match' },
{ value: 'If-Modified-Since' },
{ value: 'If-None-Match' },
{ value: 'If-Range' },
{ value: 'If-Unmodified-Since' },
{ value: 'Max-Forwards' },
{ value: 'Origin' },
{ value: 'Pragma' },
{ value: 'Proxy-Authorization' },
{ value: 'Range' },
{ value: 'Referer' },
{ value: 'TE' },
{ value: 'User-Agent' },
{ value: 'Upgrade' },
{ value: 'Via' },
{ value: 'Warning' }
]
const bodyRef = ref()
const { request: apiRequest } = toRefs(props)
onBeforeMount(() => {
if (!props.referenced && props.showScript) {
spanCount.value = 21
} else {
spanCount.value = 24
}
init()
})
watch(
() => apiRequest.value.changeId,
() => {
if (apiRequest.value.headers?.length > 1) {
activeName.value = 'headers'
}
if (apiRequest.value.rest?.length > 1) {
activeName.value = 'rest'
}
if (apiRequest.value.arguments?.length > 1) {
activeName.value = 'parameters'
}
if (apiRequest.value.body) {
apiRequest.value.body.typeChange = apiRequest.value.changeId
emits('changeId', apiRequest.value.changeId)
}
}
)
const init = () => {
if (!apiRequest.value.body) {
apiRequest.value.body = new Body()
}
if (!apiRequest.value.body.kvs) {
apiRequest.value.body.kvs = []
}
if (!apiRequest.value.rest) {
apiRequest.value.rest = []
}
if (!apiRequest.value.arguments) {
apiRequest.value.arguments = []
}
}
const emits = defineEmits(['changeId'])
</script>
<template>
<div class="request-content">
<el-tabs v-model="activeName" class="request-tabs">
<!-- 请求头-->
<el-tab-pane :label="t('datasource.headers')" name="headers" key="headers">
<api-key-value
:show-desc="true"
:suggestions="headerSuggestions"
:items="apiRequest.headers"
:value-list="valueList"
/>
</el-tab-pane>
<!--query 参数-->
<el-tab-pane key="parameters" :label="t('datasource.query_param')" name="parameters">
<api-variable
:is-read-only="isReadOnly"
:isShowEnable="isShowEnable"
:parameters="apiRequest.arguments"
:value-list="valueList"
/>
</el-tab-pane>
<!--请求体-->
<el-tab-pane
key="body"
:label="t('datasource.request_body')"
name="body"
style="overflow: hidden"
>
<api-body
ref="bodyRef"
:headers="apiRequest.headers"
:body="apiRequest.body"
:is-show-enable="isShowEnable"
:value-list="valueList"
/>
</el-tab-pane>
<!-- 认证配置 -->
<el-tab-pane key="authConfig" :label="t('datasource.auth_config')" name="authConfig">
<el-tooltip
class="item-tabs"
effect="dark"
:content="t('datasource.auth_config_info')"
placement="top-start"
>
<template #label>
<span>{{ t('datasource.auth_config') }}</span>
</template>
</el-tooltip>
<api-auth-config :request="apiRequest" />
</el-tab-pane>
<el-tab-pane key="pagination" :label="t('api_pagination.paging_ettings')" name="pagination">
<Pagination :page="apiRequest.page" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<style lang="less" scoped>
.request-content {
border: 1px #dcdfe6 solid;
height: 100%;
border-radius: 4px;
width: 100%;
.ms-query {
background: #409eff;
color: white;
height: 18px;
border-radius: 42%;
}
.ms-header {
background: #409eff;
color: white;
height: 18px;
border-radius: 42%;
}
.request-tabs {
margin: 0 16px;
:deep(.ed-tabs__item) {
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-style: normal;
font-weight: 400;
}
:deep(.ed-tabs__content) {
padding-top: 16px;
}
}
.ms-el-link {
float: right;
margin-right: 45px;
}
}
</style>

View File

@ -0,0 +1,288 @@
<script lang="ts" setup>
import icon_drag_outlined from '@/assets/svg/icon_drag_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { propTypes } from '@/utils/propTypes'
import { computed, onBeforeMount, PropType, toRefs, inject, ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { KeyValue } from './ApiTestModel.js'
import draggable from 'vuedraggable'
export interface Item {
name: string
value: string
description: string
type: string
}
const props = defineProps({
keyPlaceholder: propTypes.string.def(''),
valuePlaceholder: propTypes.string.def(''),
unShowSelect: propTypes.bool.def(false),
isReadOnly: propTypes.bool.def(false),
needMock: propTypes.bool.def(false),
showDesc: propTypes.bool.def(false),
items: {
type: Array as PropType<Item[]>,
default: () => []
},
valueList: {
type: Array as PropType<Item[]>,
default: () => []
},
suggestions: {
type: Array,
default: () => []
}
})
const { t } = useI18n()
const keyText = computed(() => {
return props.keyPlaceholder || t('datasource.key')
})
const valueText = computed(() => {
return props.valuePlaceholder || t('datasource.value')
})
const { suggestions, items } = toRefs(props)
onBeforeMount(() => {
if (!items.value.length || items.value[items.value.length - 1].name) {
items.value.push(new KeyValue({ enable: true, name: '', value: '' }))
}
for (let i = 0; i < items.value.length; i++) {
if (!items.value[i].hasOwnProperty('nameType')) {
items.value[i].nameType = 'fixed'
}
}
})
const activeName = inject('api-active-name')
const remove = (index: number) => {
if (isDisable()) return
//
items.value.splice(index, 1)
}
const change = () => {
items.value.push(new KeyValue({ enable: true, nameType: 'fixed' }))
}
const isDisable = () => {
return items.value.length === 1
}
const querySearch = (queryString, cb) => {
const results = queryString
? suggestions.value.filter(createFilter(queryString))
: suggestions.value
cb(results)
}
const changeNameType = element => {
element.value = ''
}
const createFilter = (queryString: string) => {
return restaurant => {
return restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
}
}
const options = [
{
label: t('data_source.parameter'),
value: 'params'
},
{
label: t('data_source.fixed_value'),
value: 'fixed'
},
{
label: t('data_source.time_function'),
value: 'timeFun'
},
{
label: t('data_source.customize'),
value: 'custom'
}
]
const timeFunLists = [
{
label: t('data_source.that_day') + 'yyyy-MM-dd',
value: 'currentDay yyyy-MM-dd'
},
{
label: t('data_source.that_day') + 'yyyy/MM/dd',
value: 'currentDay yyyy/MM/dd'
}
]
</script>
<template>
<div class="api-key-value">
<draggable tag="div" :list="items" handle=".handle" class="draggable-content_api">
<template #item="{ element, index }">
<div style="margin-bottom: 16px">
<el-row :gutter="8">
<el-icon class="drag handle">
<Icon name="icon_drag_outlined"><icon_drag_outlined class="svg-icon" /></Icon>
</el-icon>
<el-col :span="activeName === 'params' ? 8 : 6" v-if="!unShowSelect">
<el-input
v-if="!suggestions"
v-model="element.name"
:disabled="isReadOnly"
maxlength="200"
:placeholder="keyText"
show-word-limit
/>
<el-autocomplete
v-else
v-model="element.name"
:disabled="isReadOnly"
:maxlength="400"
:fetch-suggestions="querySearch"
:placeholder="keyText"
show-word-limit
/>
</el-col>
<el-col :span="3" v-if="activeName === 'table'">
<el-select v-model="element.nameType" @change="changeNameType(element)">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col :span="8" v-if="unShowSelect">
<el-input
v-if="!!suggestions.length"
v-model="element.name"
:disabled="isReadOnly"
maxlength="200"
:placeholder="keyText"
show-word-limit
/>
</el-col>
<el-col :span="activeName === 'params' ? 7 : 6">
<el-input
v-if="!needMock && activeName === 'params'"
v-model="element.value"
:disabled="isReadOnly"
:placeholder="unShowSelect ? t('common.description') : valueText"
show-word-limit
/>
<el-select
v-model="element.value"
v-if="!needMock && activeName === 'table' && element.nameType === 'params'"
style="width: 100%"
>
<el-option
v-for="item in valueList"
:key="item.originName"
:label="item.name"
:value="item.originName"
/>
</el-select>
<el-select
v-model="element.value"
v-if="!needMock && activeName === 'table' && element.nameType === 'timeFun'"
style="width: 100%"
>
<el-option
v-for="item in timeFunLists"
:key="item.originName"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-if="
!needMock &&
activeName === 'table' &&
element.nameType !== 'params' &&
element.nameType !== 'timeFun'
"
v-model="element.value"
:disabled="isReadOnly"
:placeholder="
element.nameType === 'fixed'
? t('data_source.value')
: t('data_source.name_use_parameters')
"
show-word-limit
/>
</el-col>
<el-col :span="7" v-if="showDesc">
<el-input
v-model="element.description"
maxlength="200"
:placeholder="t('common.description')"
show-word-limit
/>
</el-col>
<el-col :span="1">
<el-button
class="api-variable_del"
text
:disabled="isDisable()"
@click="remove(index)"
>
<template #icon>
<Icon name="icon_delete-trash_outlined"
><icon_deleteTrash_outlined class="svg-icon"
/></Icon>
</template>
</el-button>
</el-col>
</el-row>
</div>
</template>
</draggable>
<el-button style="margin-top: 14px" @click="change" text>
<template #icon>
<icon name="icon_add_outlined"><icon_add_outlined class="svg-icon" /></icon>
</template>
{{ t('data_source.add_parameters') }}
</el-button>
</div>
</template>
<style lang="less" scoped>
.api-key-value {
padding-bottom: 14px;
& > .ed-input,
.ed-autocomplete {
width: 100%;
}
.api-variable_del {
color: #646a73;
:deep(.ed-icon) {
font-size: 16px;
}
&:hover {
background: rgba(31, 35, 41, 0.1) !important;
}
&:focus {
background: rgba(31, 35, 41, 0.1) !important;
}
&:active {
background: rgba(31, 35, 41, 0.2) !important;
}
}
.drag {
margin-top: 10px;
cursor: pointer;
}
:deep(.draggable-content_api) > :last-child {
margin-bottom: 0 !important;
}
}
</style>

View File

@ -0,0 +1,154 @@
export class BaseConfig {
set(options, notUndefined) {
options = this.initOptions(options)
for (const name in options) {
if (Object.prototype.hasOwnProperty.call(options, name)) {
if (!(this[name] instanceof Array)) {
if (notUndefined === true) {
this[name] = options[name] === undefined ? this[name] : options[name]
} else {
this[name] = options[name]
}
}
}
}
}
sets(types, options) {
options = this.initOptions(options)
if (types) {
for (const name in types) {
if (
Object.prototype.hasOwnProperty.call(types, name) &&
Object.prototype.hasOwnProperty.call(options, name)
) {
options[name].forEach(o => {
this[name].push(new types[name](o))
})
}
}
}
}
initOptions(options) {
return options || {}
}
isValid() {
return true
}
}
export class KeyValue extends BaseConfig {
constructor(options) {
options = options || {}
options.enable = options.enable === undefined ? true : options.enable
super()
this.name = undefined
this.value = undefined
this.type = undefined
this.files = undefined
this.enable = undefined
this.uuid = undefined
this.time = undefined
this.contentType = undefined
this.set(options)
}
isValid() {
return (!!this.name || !!this.value) && this.type !== 'file'
}
isFile() {
return (!!this.name || !!this.value) && this.type === 'file'
}
}
export class Body extends BaseConfig {
constructor(options) {
super()
this.type = 'KeyValue'
this.raw = undefined
this.kvs = []
this.binary = []
this.set(options)
this.sets({ kvs: KeyValue }, { binary: KeyValue }, options)
}
isValid() {
if (this.isKV()) {
return this.kvs.some(kv => {
return kv.isValid()
})
} else {
return !!this.raw
}
}
isKV() {
return [BODY_TYPE.FORM_DATA, BODY_TYPE.WWW_FORM, BODY_TYPE.BINARY].indexOf(this.type) > 0
}
}
export const BODY_TYPE = {
KV: 'KeyValue',
FORM_DATA: 'Form_Data',
RAW: 'Raw',
WWW_FORM: 'WWW_FORM',
XML: 'XML',
JSON: 'JSON'
}
export class Scenario extends BaseConfig {
constructor(options = {}) {
super()
this.id = undefined
this.name = undefined
this.url = undefined
this.variables = []
this.headers = []
this.requests = []
this.environmentId = undefined
this.dubboConfig = undefined
this.environment = undefined
this.enableCookieShare = false
this.enable = true
this.databaseConfigs = []
this.tcpConfig = undefined
this.set(options)
this.sets(
{
variables: KeyValue,
headers: KeyValue
},
options
)
}
initOptions(options = {}) {
options.databaseConfigs = options.databaseConfigs || []
return options
}
clone() {
const clone = new Scenario(this)
return clone
}
isValid() {
if (this.enable) {
for (let i = 0; i < this.requests.length; i++) {
const validator = this.requests[i].isValid(this.environmentId, this.environment)
if (!validator.isValid) {
return validator
}
}
}
return { isValid: true }
}
isReference() {
return this.id.indexOf('#') !== -1
}
}

View File

@ -0,0 +1,345 @@
<script lang="ts" setup>
import icon_drag_outlined from '@/assets/svg/icon_drag_outlined.svg'
import icon_deleteTrash_outlined from '@/assets/svg/icon_delete-trash_outlined.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import { propTypes } from '@/utils/propTypes'
import { computed, onBeforeMount, PropType, toRefs, inject } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { KeyValue } from './ApiTestModel.js'
import { guid } from '@/views/visualized/data/dataset/form/util'
import draggable from 'vuedraggable'
export interface Item {
name: string
value: string
description: string
type: string
}
const props = defineProps({
keyPlaceholder: propTypes.string.def(''),
valuePlaceholder: propTypes.string.def(''),
description: propTypes.string.def(''),
type: propTypes.string.def(''),
isReadOnly: propTypes.bool.def(false),
parameters: {
type: Array as PropType<Item[]>,
default: () => []
},
valueList: {
type: Array as PropType<Item[]>,
default: () => []
},
suggestions: {
type: Array,
default: () => []
}
})
const { t } = useI18n()
const keyText = computed(() => {
return props.keyPlaceholder || t('datasource.key')
})
const valueText = computed(() => {
return props.valuePlaceholder || t('datasource.value')
})
const { parameters, suggestions } = toRefs(props)
onBeforeMount(() => {
if (parameters.value.length === 0 || parameters.value[parameters.value.length - 1].name) {
parameters.value.push(
new KeyValue({
type: 'text',
nameType: 'fixed',
enable: true,
required: true,
uuid: guid(),
contentType: 'text/plain'
})
)
}
})
const typeChange = item => {
if (item.type === 'file') {
item.contentType = 'application/octet-stream'
} else if (item.type === 'text') {
item.contentType = 'text/plain'
} else {
item.contentType = 'application/json'
}
}
const remove = (index: number) => {
if (isDisable()) return
//
parameters.value.splice(index, 1)
}
const change = () => {
parameters.value.push(
new KeyValue({
type: 'text',
enable: true,
nameType: 'fixed',
uuid: guid(),
contentType: 'text/plain'
})
)
}
const isDisable = () => {
return parameters.value.length === 1
}
const querySearch = (queryString, cb) => {
const results = queryString
? suggestions.value.filter(createFilter(queryString))
: suggestions.value
cb(results)
}
const createFilter = (queryString: string) => {
return restaurant => {
return restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
}
}
const changeNameType = element => {
element.value = ''
}
const activeName = inject('api-active-name')
const options = [
{
label: t('data_source.parameter'),
value: 'params'
},
{
label: t('data_source.page_parameter'),
value: 'pageParams'
},
{
label: t('data_source.fixed_value'),
value: 'fixed'
},
{
label: t('data_source.time_function'),
value: 'timeFun'
},
{
label: t('data_source.customize'),
value: 'custom'
}
]
const pageParams = [
{
label: '${pageNumber}',
value: '${pageNumber}'
},
{
label: '${pageSize}',
value: '${pageSize}'
},
{
label: '${pageToken}',
value: '${pageToken}'
}
]
const timeFunLists = [
{
label: t('data_source.that_day') + 'yyyy-MM-dd',
value: 'currentDay yyyy-MM-dd'
},
{
label: t('data_source.that_day') + 'yyyy/MM/dd',
value: 'currentDay yyyy/MM/dd'
}
]
</script>
<template>
<div class="api-variable">
<span v-if="description" class="kv-description">
{{ description }}
</span>
<draggable class="draggable-content_api" tag="div" :list="parameters" handle=".handle">
<template #item="{ element, index }">
<div :key="index" style="margin-bottom: 16px">
<el-row :gutter="8">
<el-icon class="drag handle">
<Icon name="icon_drag_outlined"><icon_drag_outlined class="svg-icon" /></Icon>
</el-icon>
<el-col :span="6">
<el-input
v-if="!suggestions"
v-model="element.name"
:disabled="isReadOnly"
maxlength="200"
:placeholder="keyText"
show-word-limit
>
<template #prepend>
<el-select
v-if="type === 'body'"
v-model="element.type"
:disabled="isReadOnly"
class="kv-type"
@change="typeChange(item)"
>
<el-option value="text" />
<el-option value="json" />
</el-select>
</template>
</el-input>
<el-autocomplete
v-else
v-model="element.name"
:disabled="isReadOnly"
:fetch-suggestions="querySearch"
:placeholder="keyText"
show-word-limit
/>
</el-col>
<el-col :span="3" v-if="activeName === 'table'">
<el-select v-model="element.nameType" @change="changeNameType(element)">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-col v-if="element.type !== 'file'" :span="6">
<el-input
v-if="activeName === 'params'"
v-model="element.value"
:disabled="isReadOnly"
class="input-with-autocomplete"
:placeholder="valueText"
value-key="name"
highlight-first-item
/>
<el-select
v-model="element.value"
v-if="!needMock && activeName === 'table' && element.nameType === 'params'"
style="width: 100%"
>
<el-option
v-for="item in valueList"
:key="item.originName"
:label="item.name"
:value="item.originName"
/>
</el-select>
<el-select
v-model="element.value"
v-if="!needMock && activeName === 'table' && element.nameType === 'timeFun'"
style="width: 100%"
>
<el-option
v-for="item in timeFunLists"
:key="item.originName"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select
v-model="element.value"
v-if="!needMock && activeName === 'table' && element.nameType === 'pageParams'"
style="width: 100%"
>
<el-option
v-for="item in pageParams"
:key="item.originName"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-if="
activeName === 'table' &&
element.nameType !== 'params' &&
element.nameType !== 'timeFun' &&
element.nameType !== 'pageParams'
"
v-model="element.value"
:disabled="isReadOnly"
class="input-with-autocomplete"
:placeholder="
element.nameType === 'fixed'
? t('data_source.value')
: t('data_source.name_use_parameters')
"
value-key="name"
highlight-first-item
/>
</el-col>
<el-col :span="activeName === 'params' ? 10 : 7">
<el-input
v-model="element.description"
maxlength="200"
:placeholder="$t('common.description')"
show-word-limit
/>
</el-col>
<el-col :span="1">
<el-button
class="api-variable_del"
text
:disabled="isDisable() || isReadOnly"
@click="remove(index)"
>
<template #icon>
<Icon><icon_deleteTrash_outlined class="svg-icon" /></Icon>
</template>
</el-button>
</el-col>
</el-row>
</div>
</template>
</draggable>
<el-button style="margin-top: 14px" @click="change" text>
<template #icon>
<icon name="icon_add_outlined"><icon_add_outlined class="svg-icon" /></icon>
</template>
{{ t('data_source.add_parameters') }}
</el-button>
</div>
</template>
<style lang="less" scoped>
.api-variable {
padding-bottom: 14px;
& > .ed-input,
:deep(.ed-autocomplete) {
width: 100%;
}
.drag {
margin-top: 10px;
cursor: pointer;
}
:deep(.draggable-content_api) > :last-child {
margin-bottom: 0 !important;
}
.api-variable_del {
color: #646a73;
:deep(.ed-icon) {
font-size: 16px;
}
&:hover {
background: rgba(31, 35, 41, 0.1) !important;
}
&:focus {
background: rgba(31, 35, 41, 0.1) !important;
}
&:active {
background: rgba(31, 35, 41, 0.2) !important;
}
}
.kv-description {
font-size: 13px;
}
}
</style>

View File

@ -0,0 +1,70 @@
<script lang="ts" setup>
import { ref, PropType, watch, onMounted } from 'vue'
import { propTypes } from '@/utils/propTypes'
import { VAceEditor } from 'vue3-ace-editor'
import { formatJson, formatXml } from './format-utils'
import './ace-config'
const props = defineProps({
height: [String, Number],
data: propTypes.string.def(''),
theme: propTypes.string.def('chrome'),
init: Function,
enableFormat: propTypes.bool.def(true),
readOnly: propTypes.bool.def(true),
modes: {
type: Array as PropType<string[]>,
default: () => ['text', 'json', 'xml']
},
mode: propTypes.string.def('text')
})
const formatData = ref('')
watch(formatData, () => {
emits('update:modelValue', formatData.value)
})
watch([props.theme], () => {
format()
})
onMounted(() => {
format()
})
const editorInit = editor => {
if (props.readOnly) {
editor.setReadOnly(true)
}
if (props.init) {
props.init(editor)
}
}
const format = () => {
if (props.enableFormat) {
if (props.data) {
switch (props.mode) {
case 'json':
formatData.value = formatJson(props.data)
break
case 'xml':
formatData.value = formatXml(props.data)
break
default:
formatData.value = props.data
}
}
} else {
formatData.value = props.data
}
}
const emits = defineEmits(['update:modelValue'])
</script>
<template>
<v-ace-editor
v-model:value="formatData"
:lang="mode"
:theme="theme"
:style="{ height }"
@init="editorInit"
/>
</template>

View File

@ -0,0 +1,494 @@
<script lang="ts" setup>
import dvFolder from '@/assets/svg/dv-folder.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { ref, reactive, computed, watch, nextTick, shallowRef, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { checkRepeat, listDatasources, save, update } from '@/api/datasource'
import { ElMessage, ElMessageBox, ElMessageBoxOptions } from 'element-plus-secondary'
import treeSort from '@/utils/treeSortUtils'
import type { DatasetOrFolder } from '@/api/dataset'
import { cloneDeep } from 'lodash-es'
import nothingTree from '@/assets/img/nothing-tree.png'
import { useCache } from '@/hooks/web/useCache'
import { filterFreeFolder } from '@/utils/utils'
import { useRoute } from 'vue-router'
const route = useRoute()
const appId:any = ref('')
if (route.query.id) {
appId.value = route.query.id
}
export interface Tree {
name: string
value?: string | number
id: string | number
nodeType: string
createBy?: string
level: number
leaf?: boolean
pid: string | number
type?: string
createTime: number
children?: Tree[]
request: any
}
const { t } = useI18n()
const { wsCache } = useCache()
const state = reactive({
tData: []
})
const nodeType = ref()
const pid = ref()
const id = ref()
const oldName = ref()
const cmd = ref('')
const treeRef = ref()
const filterText = ref('')
const datasetForm = reactive({
pid: '',
name: ''
})
const searchEmpty = ref(false)
const filterNode = (value: string, data: Tree) => {
nextTick(() => {
searchEmpty.value = treeRef.value.isEmpty
})
if (!value) return true
return data.name.includes(value)
}
watch(filterText, val => {
showAll.value = !val
treeRef.value.filter(val)
nextTick(() => {
document.querySelectorAll('.node-text').forEach(ele => {
const content = ele.getAttribute('title')
ele.innerHTML = content.replace(val, `<span class="highLight">${val}</span>`)
})
})
})
const showPid = computed(() => {
if (nodeType.value === 'folder' && !!pid.value) {
return false
}
return !['rename', 'move'].includes(cmd.value) && !!pid.value
})
const labelName = computed(() => {
return nodeType.value === 'folder'
? t('deDataset.folder_name')
: t('data_source.data_source_name')
})
const dialogTitle = computed(() => {
let title = ''
switch (nodeType.value) {
case 'folder':
title = t('deDataset.new_folder')
break
case 'datasource':
title = t('deDataset.create') + t('auth.datasource')
break
default:
break
}
switch (cmd.value) {
case 'move':
title = t('chart.move_to')
break
case 'rename':
title = t('chart.rename')
break
default:
break
}
return title
})
const showName = computed(() => {
return cmd.value !== 'move'
})
const placeholder = ref('')
const datasetFormRules = ref()
const activeAll = ref(false)
const showAll = ref(true)
const datasource = ref()
const loading = ref(false)
const createDataset = ref(false)
const filterMethod = (value, data) => {
if (!data) return false
data.name.includes(value)
}
const resetForm = () => {
createDataset.value = false
}
const originResourceTree = shallowRef([])
const sortTypeChange = sortType => {
state.tData = treeSort(originResourceTree.value, sortType)
}
const dfs = (arr: Tree[]) => {
arr.forEach(ele => {
ele.value = ele.id
if (ele.children?.length) {
dfs(ele.children)
}
})
}
let request = null
let dsType = ''
const sortList = ['time_asc', 'time_desc', 'name_asc', 'name_desc']
const createInit = (type, data: Tree, exec, name: string) => {
pid.value = ''
id.value = ''
cmd.value = ''
datasetForm.pid = ''
datasetForm.name = ''
nodeType.value = type
filterText.value = ''
placeholder.value =
type === 'folder' ? t('data_source.a_folder_name') : t('data_source.data_source_name_de')
dsType = data.type
if (type === 'datasource') {
request = data.request
}
if (data.id) {
if (exec !== 'rename') {
listDatasources({ leaf: false, id: data.id, weight: 7, appId: appId.value }).then(res => {
filterFreeFolder(res, 'datasource')
dfs(res as unknown as Tree[])
state.tData = (res as unknown as Tree[]) || []
if (state.tData.length && state.tData[0].name === 'root' && state.tData[0].id === '0') {
state.tData[0].name = t('data_source.data_source')
}
originResourceTree.value = cloneDeep(unref(state.tData))
let curSortType = sortList[Number(wsCache.get('TreeSort-backend')) ?? 1]
curSortType = wsCache.get('TreeSort-datasource') ?? curSortType
sortTypeChange(curSortType)
})
}
if (exec) {
pid.value = data.pid
id.value = data.id
datasetForm.pid = data.pid as string
datasetForm.name = data.name
oldName.value = data.name
} else {
datasetForm.pid = data.id as string
pid.value = data.id
}
}
cmd.value = data.id ? exec : ''
name && (datasetForm.name = name)
createDataset.value = true
datasetFormRules.value = {
name: [
{
required: true,
message: placeholder.value,
trigger: 'change'
},
{
required: true,
message: placeholder.value,
trigger: 'blur'
},
{
min: 1,
max: 64,
message: t('datasource.input_limit_1_64', [1, 64]),
trigger: 'blur'
}
],
pid: [
{
required: true,
message: t('common.please_select'),
trigger: 'blur'
}
]
}
setTimeout(() => {
datasource.value.clearValidate()
}, 50)
}
const editeInit = (param: Tree) => {
pid.value = param.pid
id.value = param.id
}
const props = {
label: 'name',
children: 'children',
isLeaf: node => !node.children?.length
}
const nodeClick = (data: Tree) => {
activeAll.value = false
datasetForm.pid = data.id as string
}
const successCb = () => {
wsCache.set('ds-new-success', true)
datasource.value.resetFields()
request = null
datasetForm.pid = ''
datasetForm.name = ''
createDataset.value = false
}
const finallyCb = () => {
loading.value = false
}
const checkPid = pid => {
if (pid !== 0 && !pid) {
ElMessage.error(t('data_source.the_destination_folder'))
return false
}
return true
}
const saveDataset = () => {
datasource.value.validate(result => {
if (result) {
const params: Omit<DatasetOrFolder, 'nodeType'> & { nodeType: 'folder' | 'datasource' } = {
nodeType: nodeType.value as 'folder' | 'datasource',
name: datasetForm.name.trim()
}
switch (cmd.value) {
case 'move':
params.pid = activeAll.value ? '0' : (datasetForm.pid as string)
params.id = id.value
params.action = 'move'
break
case 'rename':
params.pid = pid.value as string
params.id = id.value
params.action = 'rename'
break
default:
params.pid = datasetForm.pid || pid.value || '0'
params.action = 'create'
break
}
if (cmd.value === 'rename' && oldName.value === params.name) {
successCb()
return
}
if (cmd.value === 'move' && !checkPid(params.pid)) {
return
}
if (loading.value) {
return
}
loading.value = true
if (request) {
let options = {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false,
tip: ''
}
request.apiConfiguration = ''
request.appId = appId.value
debugger
checkRepeat(request).then(res => {
let method = request.id === '' ? save : update
if (!request.type.startsWith('API')) {
request.syncSetting = null
}
if (res) {
ElMessageBox.confirm(t('datasource.has_same_ds'), options as ElMessageBoxOptions)
.then(() => {
method({ ...request, name: datasetForm.name, pid: params.pid })
.then(res => {
if (res !== undefined) {
wsCache.set('ds-new-success', true)
emits('handleShowFinishPage', { ...res, pid: params.pid })
ElMessage.success(t('data_source.source_saved_successfully'))
successCb()
}
})
.finally(() => {
loading.value = false
})
})
.catch(() => {
loading.value = false
createDataset.value = false
})
} else {
method({ ...request, name: datasetForm.name, pid: params.pid })
.then(res => {
if (res !== undefined) {
wsCache.set('ds-new-success', true)
emits('handleShowFinishPage', { ...res, pid: params.pid })
ElMessage.success(t('data_source.source_saved_successfully'))
successCb()
}
})
.finally(() => {
loading.value = false
})
}
})
return
}
emits('finish', params, successCb, finallyCb, cmd.value, dsType)
}
})
}
defineExpose({
createInit,
editeInit
})
const emits = defineEmits(['finish', 'handleShowFinishPage'])
</script>
<template>
<el-dialog
v-loading="loading"
:title="dialogTitle"
v-model="createDataset"
:width="cmd === 'move' ? '600px' : '420px'"
class="create-dialog"
:before-close="resetForm"
>
<el-form
label-position="top"
require-asterisk-position="right"
ref="datasource"
@keydown.stop.prevent.enter
:model="datasetForm"
:rules="datasetFormRules"
>
<el-form-item v-if="showName" :label="labelName" prop="name">
<el-input :placeholder="placeholder" v-model="datasetForm.name" />
</el-form-item>
<el-form-item v-if="showPid" :label="t('deDataset.folder')" prop="pid">
<el-tree-select
v-model="datasetForm.pid"
:data="state.tData"
popper-class="dataset-tree-select"
style="width: 100%"
:render-after-expand="false"
:props="props"
@node-click="nodeClick"
:filter-method="filterMethod"
filterable
>
<template #default="{ data: { name } }">
<el-icon>
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<span :title="name">{{ name }}</span>
</template>
</el-tree-select>
</el-form-item>
<div v-if="cmd === 'move'">
<el-input style="margin-bottom: 12px" v-model="filterText" clearable>
<template #prefix>
<el-icon>
<Icon name="icon_search-outline_outlined"
><icon_searchOutline_outlined class="svg-icon"
/></Icon>
</el-icon>
</template>
</el-input>
<div class="tree-content">
<el-tree
ref="treeRef"
:filter-node-method="filterNode"
filterable
v-model="datasetForm.pid"
menu
empty-text=""
:data="state.tData"
:props="props"
@node-click="nodeClick"
>
<template #default="{ data }">
<span class="custom-tree-node">
<el-icon style="font-size: 18px">
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<span class="node-text" :title="data.name">{{ data.name }}</span>
</span>
</template>
</el-tree>
<div v-if="searchEmpty" class="empty-search">
<img :src="nothingTree" />
<span>{{ t('data_source.relevant_content_found') }}</span>
</div>
</div>
</div>
</el-form>
<template #footer>
<el-button secondary @click="resetForm">{{ t('dataset.cancel') }} </el-button>
<el-button type="primary" @click="saveDataset">{{ t('dataset.confirm') }} </el-button>
</template>
</el-dialog>
</template>
<style lang="less" scoped>
.tree-content {
width: 552px;
height: 380px;
border: 1px solid #dee0e3;
border-radius: 4px;
padding: 8px;
overflow-y: auto;
.custom-tree-node {
display: flex;
align-items: center;
.node-text {
margin-left: 8.75px;
width: 120px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
:deep(.highLight) {
color: var(--el-color-primary, #3370ff);
}
}
}
.empty-search {
width: 100%;
margin-top: 57px;
display: flex;
flex-direction: column;
align-items: center;
img {
width: 100px;
height: 100px;
margin-bottom: 8px;
}
span {
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: #646a73;
}
}
}
</style>
<style lang="less">
.dataset-tree-select {
.ed-select-dropdown__item {
display: flex;
align-items: center;
.ed-icon {
margin-right: 5px;
}
}
}
</style>

View File

@ -0,0 +1,187 @@
<script lang="ts" setup>
import { shallowRef, PropType, computed } from 'vue'
import { dsTypes, typeList, nameMap } from './option'
import Icon from '@/components/icon-custom/src/Icon.vue'
import { XpackComponent } from '@/components/plugin'
import { iconDatasourceMap } from '@/components/icon-group/datasource-list'
import { useI18n } from '@/hooks/web/useI18n'
export type DsType = 'OLTP' | 'OLAP' | 'DL' | 'OTHER' | 'LOCAL' | 'latestUse' | 'all'
const props = defineProps({
currentType: {
type: String as PropType<DsType>,
default: 'OLTP'
},
filterText: {
type: String,
default: ''
},
latestUseTypes: {
type: Array
}
})
const { t } = useI18n()
const databaseList = shallowRef([])
const currentTypeList = computed(() => {
if (props.currentType == 'all') {
return typeList.map((ele, index) => {
return {
name: nameMap[ele],
dbList: databaseList.value[index].filter(ele =>
ele.name.toLowerCase().includes(props.filterText.trim())
)
}
})
}
if (props.currentType === 'latestUse') {
let catalogList = []
let dstypes = []
props.latestUseTypes.forEach(type => {
dsTypes.forEach(item => {
if (item.type === type && catalogList.indexOf(item.catalog) === -1) {
catalogList.push(item.catalog)
}
})
})
let dbList = []
catalogList.forEach(catalog => {
props.latestUseTypes.forEach(type => {
dsTypes.forEach(item => {
if (item.type === type && item.catalog === catalog) {
dbList.push(item)
}
})
})
})
dbList = dbList.filter(ele => ele.name.toLowerCase().includes(props.filterText.trim()))
dstypes.push({ name: t('data_source.recently_created'), dbList })
return dstypes
}
const index = typeList.findIndex(ele => props.currentType === ele)
return (
[
{
name: nameMap[props.currentType],
dbList: databaseList.value[index].filter(ele =>
ele.name.toLowerCase().includes(props.filterText.trim())
)
}
] || []
)
})
const getDatasourceTypes = () => {
const arr = [[], [], [], [], []]
dsTypes.forEach(item => {
const index = typeList.findIndex(ele => ele === item.catalog)
if (index !== -1) {
arr[index].push(item)
}
})
databaseList.value = arr.map(ele => {
return ele.sort((a, b) => {
return a.name.toLowerCase().charCodeAt(0) - b.name.toLowerCase().charCodeAt(0)
})
})
}
getDatasourceTypes()
const loadDsPlugin = data => {
data.forEach(item => {
const { name, category, type, icon, extraParams, staticMap } = item
const node = {
name,
catalog: category,
type,
icon,
extraParams,
isPlugin: true,
staticMap
}
const index = typeList.findIndex(ele => ele === node.catalog)
if (index !== -1) {
let copiedArr = JSON.parse(JSON.stringify(databaseList.value))
copiedArr[index].push(node)
databaseList.value = copiedArr
}
})
}
const emits = defineEmits(['selectDsType'])
const selectDs = ({ type }) => {
emits('selectDsType', type)
}
</script>
<template>
<div class="ds-type-list">
<template v-for="ele in currentTypeList" :key="ele.name">
<div class="title-form_primary">
{{ ele.name }}
</div>
<div class="item-container">
<div v-for="db in ele.dbList" :key="db.type" class="db-card" @click="selectDs(db)">
<el-icon class="icon-border">
<Icon v-if="db['isPlugin']" :static-content="db.icon"></Icon>
<Icon v-else
><component :is="iconDatasourceMap[db.type]" class="svg-icon"></component
></Icon>
</el-icon>
<p class="db-name">{{ db.name }}</p>
</div>
</div>
</template>
<XpackComponent
jsname="L2NvbXBvbmVudC9wbHVnaW5zLWhhbmRsZXIvRHNDYXRlZ29yeUhhbmRsZXI="
@load-ds-plugin="loadDsPlugin"
/>
</div>
</template>
<style lang="less" scoped>
.ds-type-list {
width: 100%;
position: relative;
display: flex;
width: 100%;
flex-wrap: wrap;
.title-form_primary {
margin-bottom: 16px;
}
.item-container {
display: flex;
width: calc(100% + 16px);
flex-wrap: wrap;
margin-left: -16px;
}
.db-card {
height: 64px;
width: 266px;
display: flex;
align-items: center;
background: #ffffff;
border: 1px solid #dee0e3;
border-radius: 4px;
margin-bottom: 16px;
margin-left: 16px;
padding: 16px;
cursor: pointer;
.icon-border {
margin-right: 12px;
font-size: 32px;
}
&:hover {
box-shadow: 0px 6px 24px rgba(31, 35, 41, 0.08);
}
}
.marLeft {
margin-left: 0;
}
}
</style>

View File

@ -0,0 +1,875 @@
<script lang="tsx" setup>
import icon_upload_outlined from '@/assets/svg/icon_upload_outlined.svg'
import icon_refresh_outlined from '@/assets/svg/icon_refresh_outlined.svg'
import { Icon } from '@/components/icon-custom'
import { ElIcon } from 'element-plus-secondary'
import { useI18n } from '@/hooks/web/useI18n'
import {
ref,
shallowRef,
reactive,
h,
computed,
toRefs,
onMounted,
onBeforeUnmount,
nextTick
} from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus-secondary'
import { save, update } from '@/api/datasource'
import type { Action } from 'element-plus-secondary'
import { Base64 } from 'js-base64'
import ExcelInfo from '../ExcelInfo.vue'
import SheetTabs from '../SheetTabs.vue'
import { cloneDeep, debounce } from 'lodash-es'
import { uploadFile } from '@/api/datasource'
import { useEmitt } from '@/hooks/web/useEmitt'
import { iconFieldMap } from '@/components/icon-group/field-list'
import { boolean } from 'mathjs'
import { useRoute } from 'vue-router'
const route = useRoute()
const appId:any = ref('')
if (route.query.id) {
appId.value = route.query.id
}
export interface Param {
editType: number
pid?: string
type?: string
id?: string
name?: string
creator?: string
isPlugin?: boolean
staticMap?: any
}
export interface Field {
accuracy: number
originName: string
fieldSize: number
fieldType: string
name: string
}
const props = defineProps({
param: {
required: false,
default() {
return reactive<{
id: string
name: string
desc: string
type: string
editType: number
}>({
id: '0',
name: '',
desc: '',
type: 'Excel',
editType: 0
})
},
type: Object
},
isSupportSetKey: {
type: boolean,
required: true
}
})
const { param, isSupportSetKey } = toRefs(props)
const { t } = useI18n()
const { emitter } = useEmitt()
const loading = ref(false)
const columns = shallowRef([])
const multipleSelection = shallowRef([])
const multipleTable = ref()
const defaultSheetObj = {
tableName: ' ',
sheetExcelId: '',
fields: [],
jsonArray: [],
nameExist: false,
empty: '',
overLength: false
}
const sheetObj = reactive(cloneDeep(defaultSheetObj))
const state = reactive({
excelData: [],
defaultExpandedKeys: [],
defaultCheckedKeys: [],
fileList: null,
sheets: []
})
const sheetFile = computed(() => {
const [sheet = {}] = state.excelData
return {
name: sheet.excelLabel,
size: sheet.excelLabel
}
})
const uploading = ref(false)
const fieldType = {
TEXT: 'text',
DATETIME: 'time',
LONG: 'value',
DOUBLE: 'value'
}
const generateColumns = (arr: Field[]) =>
arr.map(ele => ({
key: ele.originName,
fieldType: ele.fieldType,
dataKey: ele.originName,
title: ele.name,
checked: ele.checked,
primaryKey: ele.primaryKey,
length: ele.length,
width: 150,
headerCellRenderer: ({ column }) => (
<div class="flex-align-center icon">
<ElIcon>
<Icon>
{h(iconFieldMap[fieldType[column.fieldType]], {
class: `svg-icon field-icon-${fieldType[column.fieldType]}`
})}
</Icon>
</ElIcon>
<span class="ellipsis" title={column.title} style={{ width: '100px' }}>
{column.title}
</span>
</div>
)
}))
const handleNodeClick = data => {
if (data.sheet) {
Object.assign(sheetObj, data)
columns.value = generateColumns(data.fields)
multipleSelection.value = columns.value.filter(item => item.checked)
currentMode.value = 'preview'
}
}
const beforeUpload = () => {
uploading.value = true
}
const handleTabClick = tab => {
activeTab.value = tab.value
const sheet = state.excelData[0]?.sheets.find(ele => ele.sheetId === tab.value)
handleNodeClick(sheet)
}
const uploadFail = response => {
state.excelData = []
activeTab.value = ''
tabList.value = []
Object.assign(sheetObj, cloneDeep(defaultSheetObj))
let myError = response.toString()
myError.replace('Error: ', '')
}
const tabList = shallowRef([])
const activeTab = ref('')
const handleExcelDel = () => {
state.excelData = []
activeTab.value = ''
tabList.value = []
Object.assign(sheetObj, cloneDeep(defaultSheetObj))
}
const uploadSuccess = response => {
if (!response) {
return
}
if (response?.code !== 0) {
state.excelData = []
activeTab.value = ''
tabList.value = []
Object.assign(sheetObj, cloneDeep(defaultSheetObj))
ElMessage.warning(response.msg)
return
}
columns.value = []
Object.assign(sheetObj, cloneDeep(defaultSheetObj))
multipleSelection.value = []
uploading.value = false
if (!param.value.name) {
param.value.name = response.data.excelLabel
}
tabList.value = response.data.sheets.map(ele => {
const { sheetId, tableName, newSheet } = ele
return {
value: sheetId,
label: tableName,
newSheet: newSheet
}
})
state.excelData = [response.data]
const [sheet] = tabList.value
sheet && handleTabClick(sheet)
}
const saveExcelDs = (params, successCb, finallyCb) => {
let validate = true
let selectedSheet = []
let sheetFileMd5 = []
let effectExtField = false
let changeFiled = false
let selectNode = state.excelData[0]?.sheets
for (let i = 0; i < selectNode.length; i++) {
if (selectNode[i].sheet) {
if (selectNode[i].effectExtField) {
effectExtField = true
}
if (selectNode[i].changeFiled) {
changeFiled = true
}
if (selectNode[i].fields.filter(field => field.checked).length == 0) {
ElMessage({
message: selectNode[i].excelLabel + t('datasource.api_field_not_empty'),
type: 'error'
})
finallyCb?.()
return
}
for (let j = 0; j < selectNode[i].fields.length; j++) {
if (
selectNode[i].fields[j].checked &&
selectNode[i].fields[j].primaryKey &&
!selectNode[i].fields[j].length &&
selectNode[i].fields[j].deExtractType === 0
) {
ElMessage({
message:
t('datasource.primary_key_length') +
selectNode[i].excelLabel +
': ' +
selectNode[i].fields[j].name,
type: 'error'
})
finallyCb?.()
return
}
}
selectedSheet.push(selectNode[i])
sheetFileMd5.push(selectNode[i].fieldsMd5)
}
}
if (!selectedSheet.length) {
ElMessage({
message: t('dataset.ple_select_excel'),
type: 'error'
})
finallyCb?.()
return
}
if (!validate) {
finallyCb?.()
return
}
let table = {}
if (params) {
param.value.name = params.name
}
if (!props.param.id) {
table = {
id: props.param.id,
name: props.param.name,
type: 'Excel',
sheets: selectedSheet,
editType: 0
}
} else {
table = {
id: props.param.id,
name: props.param.name,
type: 'Excel',
sheets: selectedSheet,
editType: props.param.editType ? props.param.editType : 0
}
}
if (props.param.editType === 0 && props.param.id && (effectExtField || changeFiled)) {
ElMessageBox.confirm(t('deDataset.replace_the_data'), {
confirmButtonText: t('dataset.confirm'),
tip: t('data_source.to_replace_it'),
cancelButtonText: 'Cancel',
confirmButtonType: 'primary',
type: 'warning',
autofocus: false,
showClose: false,
callback: (action: Action) => {
if (action === 'confirm') {
saveExcelData(sheetFileMd5, table, params, successCb, finallyCb)
}
}
})
} else {
saveExcelData(sheetFileMd5, table, params, successCb, finallyCb)
}
}
const saveExcelData = (sheetFileMd5, table, params, successCb, finallyCb) => {
for (let i = 0; i < table.sheets.length; i++) {
table.sheets[i].data = []
table.sheets[i].jsonArray = []
}
table.configuration = Base64.encode(JSON.stringify(table.sheets))
let method = save
debugger
if (!table.id || table.id === '0') {
delete table.id
table.pid = params.pid
} else {
method = update
}
if (new Set(sheetFileMd5).size !== sheetFileMd5.length && !props.param.id) {
ElMessageBox.confirm(t('dataset.merge_title'), {
confirmButtonText: t('dataset.merge'),
tip: t('dataset.task.excel_replace_msg'),
cancelButtonText: t('dataset.no_merge'),
confirmButtonType: 'primary',
type: 'warning',
autofocus: false,
callback: (action: Action) => {
if (action === 'close') return
loading.value = true
table.mergeSheet = action === 'confirm'
table.appId = appId.value
if (action === 'confirm') {
method(table)
.then(res => {
emitter.emit('showFinishPage', res)
successCb?.()
ElMessage({
message: t('commons.save_success'),
type: 'success'
})
})
.finally(() => {
finallyCb?.()
loading.value = false
})
}
if (action === 'cancel') {
method(table)
.then(res => {
emitter.emit('showFinishPage', res)
successCb?.()
ElMessage({
message: t('commons.save_success'),
type: 'success'
})
})
.finally(() => {
finallyCb?.()
loading.value = false
})
}
}
})
} else {
if (loading.value) return
loading.value = true
method(table)
.then(res => {
emitter.emit('showFinishPage', res)
successCb?.()
ElMessage({
message: t('commons.save_success'),
type: 'success'
})
})
.finally(() => {
finallyCb?.()
loading.value = false
})
}
}
const onChange = file => {
state.fileList = file
}
const isResize = ref(true)
const handleResize = debounce(() => {
isResize.value = false
nextTick(() => {
isResize.value = true
})
}, 500)
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
const upload = ref()
const uploadAgain = ref()
const uploadExcel = () => {
const formData = new FormData()
formData.append('file', state.fileList.raw)
formData.append('type', '')
formData.append('editType', param.value.editType)
formData.append('id', param.value.id || 0)
loading.value = true
return uploadFile(formData)
.then(res => {
upload.value?.clearFiles()
uploadAgain.value?.clearFiles()
uploadSuccess(res)
loading.value = false
})
.catch(error => {
state.excelData = []
activeTab.value = ''
tabList.value = []
Object.assign(sheetObj, cloneDeep(defaultSheetObj))
if (error.code === 'ECONNABORTED') {
ElMessage({
type: 'error',
message: error.message,
showClose: true
})
}
loading.value = false
})
}
const excelForm = ref()
const submitForm = () => {
return excelForm.value.validate
}
const showName = ref(true)
const appendReplaceExcel = response => {
showName.value = false
uploadSuccess(response)
}
const status = ref(false)
const initMultipleTable = ref(false)
const currentMode = ref('preview')
const refreshData = () => {
currentMode.value = 'preview'
}
const lengthChange = val => {
const sheet = state.excelData[0]?.sheets.find(ele => ele.sheetId === activeTab.value)
sheet.fields.forEach(row => {
if (row.originName === val.dataKey) {
row.length = val.length
}
})
}
const primaryKeyChange = val => {
const sheet = state.excelData[0]?.sheets.find(ele => ele.sheetId === activeTab.value)
sheet.fields.forEach(row => {
if (row.originName === val.dataKey) {
row.primaryKey = val.primaryKey
}
})
}
const handleSelectionChange = val => {
if (!initMultipleTable.value) {
multipleSelection.value = val
multipleSelection.value.forEach(row => {
row.checked = true
})
columns.value.forEach(row => {
let item
for (let i = 0; i < multipleSelection.value.length; i++) {
if (row.dataKey === multipleSelection.value[i].dataKey) {
item = multipleSelection.value[i]
}
}
if (item) {
row.checked = item.checked
} else {
row.checked = false
}
})
const sheet = state.excelData[0]?.sheets.find(ele => ele.sheetId === activeTab.value)
sheet.fields.forEach(row => {
let item
for (let i = 0; i < multipleSelection.value.length; i++) {
if (row.originName === multipleSelection.value[i].dataKey) {
item = multipleSelection.value[i]
}
}
if (item) {
row.checked = item.checked
} else {
row.checked = false
}
})
}
}
const disabledFieldLength = item => {
if (!item.checked) {
return true
}
if (item.fieldType !== 'TEXT') {
return true
}
}
const changeCurrentMode = val => {
currentMode.value = val
if (val === 'select') {
nextTick(() => {
initMultipleTable.value = true
for (let i = 0; i < columns.value.length; i++) {
if (columns.value[i].checked) {
multipleTable?.value?.toggleRowSelection(columns.value[i], true)
}
}
initMultipleTable.value = false
})
}
}
const uploadStatus = val => {
status.value = val
}
defineExpose({
saveExcelDs,
submitForm,
sheetFile,
appendReplaceExcel,
uploadStatus
})
</script>
<template>
<div class="excel-detail">
<div class="detail-inner">
<el-form
ref="excelForm"
require-asterisk-position="right"
:model="param"
label-position="top"
v-loading="loading"
>
<el-form-item
v-if="sheetFile.name"
prop="id"
:label="t('data_source.document')"
key="sheetFile"
:rules="[
{
required: true
}
]"
>
<ExcelInfo
@del="handleExcelDel"
show-del
:name="sheetFile.name"
:size="sheetFile.size"
></ExcelInfo>
<el-upload
action=""
:multiple="false"
ref="uploadAgain"
:show-file-list="false"
accept=".xls,.xlsx,.csv"
:before-upload="beforeUpload"
:on-change="onChange"
:http-request="uploadExcel"
:on-error="uploadFail"
name="file"
>
<template #trigger>
<el-button text>{{ t('data_source.reupload') }}</el-button>
</template>
</el-upload>
</el-form-item>
<el-form-item
v-else
prop="id"
key="sheetId"
:label="t('data_source.document')"
:rules="[
{
required: true
}
]"
>
<el-upload
:multiple="false"
action=""
ref="upload"
:show-file-list="false"
accept=".xls,.xlsx,.csv"
:before-upload="beforeUpload"
:on-change="onChange"
:http-request="uploadExcel"
:on-error="uploadFail"
name="file"
>
<template #trigger>
<el-button secondary>
<template #icon>
<Icon name="icon_upload_outlined"><icon_upload_outlined class="svg-icon" /></Icon>
</template>
{{ t('dataset.upload_file') }}
</el-button>
</template>
</el-upload>
<p class="upload-tip" style="width: 100%">{{ t('data_source.and_csv_formats') }}</p>
<div class="ed-form-item__error" v-if="status">
{{ t('data_source.please_upload_files') }}
</div>
</el-form-item>
<el-form-item
:class="status && !sheetFile.name && 'error-status'"
prop="name"
key="name"
v-if="showName"
:rules="[
{
required: true,
message: t('common.please_input') + t('datasource.datasource') + t('common.name')
}
]"
:label="t('visualization.custom') + t('datasource.datasource') + t('common.name')"
>
<el-input
v-model="param.name"
:placeholder="t('common.please_input') + t('datasource.datasource') + t('common.name')"
/>
</el-form-item>
</el-form>
<template v-if="activeTab">
<div class="title-form_primary">
{{ t('chart.data_preview') }}
</div>
<SheetTabs
:activeTab="activeTab"
@tab-click="handleTabClick"
:tab-list="tabList"
></SheetTabs>
<div class="table-select_mode" v-if="param.editType === 0">
<div class="btn-select">
<el-button
@click="changeCurrentMode('preview')"
:class="[currentMode === 'preview' && 'is-active']"
text
>
{{ t('chart.data_preview') }}
</el-button>
<el-button
@click="changeCurrentMode('select')"
:class="[currentMode === 'select' && 'is-active']"
text
>
{{ t('data_set.field_selection') }}
</el-button>
</div>
</div>
<div
class="info-table"
:class="param.editType === 0 && 'info-table_height'"
v-if="isResize"
>
<el-auto-resizer v-if="currentMode === 'preview'">
<template #default="{ height, width }">
<el-table-v2
:columns="multipleSelection"
header-class="excel-header-cell"
:data="sheetObj.jsonArray"
:width="width"
:height="height"
fixed
/>
</template>
</el-auto-resizer>
<el-table
header-class="header-cell"
v-else
ref="multipleTable"
:data="columns"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column :label="t('data_set.field_name')">
<template #default="scope">{{ scope.row.title }}</template>
</el-table-column>
<el-table-column :label="t('data_set.field_type')">
<template #default="scope">
<div class="flex-align-center">
<el-icon>
<Icon>
<component
:class="`svg-icon field-icon-${fieldType[scope.row.fieldType]}`"
:is="iconFieldMap[fieldType[scope.row.fieldType]]"
></component>
</Icon>
</el-icon>
{{ t(`dataset.${fieldType[scope.row.fieldType]}`) }}
</div>
</template>
</el-table-column>
<el-table-column
prop="length"
:label="t('datasource.length')"
v-if="param.editType === 0"
>
<template #default="scope">
<el-input-number
:disabled="disabledFieldLength(scope.row)"
v-model="scope.row.length"
autocomplete="off"
step-strictly
class="text-left edit-all-line"
:min="1"
:max="512"
:placeholder="t('common.inputText')"
controls-position="right"
type="number"
@change="lengthChange(scope.row)"
/>
</template>
</el-table-column>
<el-table-column
prop="primaryKey"
class-name="checkbox-table"
:label="t('datasource.set_key')"
width="100"
v-if="param.editType === 0 && isSupportSetKey"
>
<template #default="scope">
<el-checkbox
:key="scope.row.dataKey"
v-model="scope.row.primaryKey"
:disabled="!scope.row.checked"
@change="primaryKeyChange(scope.row)"
>
</el-checkbox>
</template>
</el-table-column>
</el-table>
</div>
</template>
</div>
</div>
</template>
<style lang="less">
.excel-detail {
display: flex;
justify-content: center;
width: calc(100% + 48px);
margin: -8px -24px 0 -24px;
.ed-form-item {
margin-bottom: 16px;
}
.table-select_mode {
display: flex;
align-items: center;
justify-content: space-between;
background: #f5f6f7;
padding: 16px;
.btn-select {
width: 164px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
border: 1px solid #bbbfc4;
border-radius: 4px;
.is-active {
background: var(--ed-color-primary-1a, rgba(51, 112, 255, 0.1));
}
.ed-button:not(.is-active) {
color: #1f2329;
}
.ed-button.is-text {
height: 24px;
width: 74px;
line-height: 24px;
}
.ed-button + .ed-button {
margin-left: 4px;
}
}
}
.detail-operate {
height: 56px;
padding: 16px 24px;
font-size: 16px;
font-weight: 500;
width: 100%;
border-bottom: 1px solid rgba(31, 35, 41, 0.15);
}
.detail-inner {
width: 800px;
padding-top: 16px;
height: calc(100vh - 280px);
min-height: 700px;
.dropdown-icon {
.down-outlined {
transform: rotate(180deg);
}
&[aria-expanded='true'] {
.down-outlined {
transform: rotate(0);
}
}
cursor: pointer;
}
.error-status {
margin-top: 32px;
}
.upload-tip {
color: #8f959e;
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.title-form_primary {
margin: 16px 0;
margin-top: 32px;
}
.info-table {
width: 100%;
height: calc(100% - 200px);
&.info-table_height {
height: calc(100% - 379px);
}
}
}
}
</style>

View File

@ -0,0 +1,229 @@
<script lang="ts" setup>
import { onBeforeMount, PropType, ref, toRefs } from 'vue'
import { cloneDeep } from 'lodash-es'
import { useI18n } from '@/hooks/web/useI18n'
export interface PageSetting {
pageType: string
requestData: requestItem[]
responseData: responseItem[]
}
export interface requestItem {
parameterName: string
builtInParameterName: string
requestParameterName: string
parameterDefaultValue: string
}
export interface responseItem {
parameterName: string
resolutionPath: string
resolutionPathType: string
}
const props = defineProps({
page: {
type: Object as PropType<PageSetting>,
default: () => ({
pageType: '',
requestData: [],
responseData: []
})
}
})
const { page } = toRefs(props)
const { t } = useI18n()
const options = [
{
value: 'pageNumber',
label: t('api_pagination.number__size')
},
{
value: 'cursor',
label: t('api_pagination.cursor__size')
},
{
value: 'empty',
label: t('chart.line_symbol_none')
}
]
const requestData = ref([
{
parameterName: t('api_pagination.page_number'),
builtInParameterName: '${pageToken}',
requestParameterName: '',
parameterDefaultValue: ''
},
{
parameterName: t('api_pagination.pagination_size'),
builtInParameterName: '${pageSize}',
requestParameterName: '',
parameterDefaultValue: ''
}
])
const defaultPathArr = [
{
value: 'totalNumber',
label: t('api_pagination.total_number_de')
},
{
value: 'totalPage',
label: t('api_pagination.number_of_pages')
}
]
const cursorPathArr = [
{
value: 'cursor',
label: t('api_pagination.cursor')
}
]
const resolutionPathOptions = ref(cloneDeep(defaultPathArr))
const responseData = ref([
{
parameterName: t('api_pagination.total_number'),
resolutionPath: '',
resolutionPathType: 'number'
}
])
onBeforeMount(() => {
if (!page.value.requestData || page.value.requestData.length === 0) {
page.value.requestData = requestData.value
}
if (!page.value.responseData || page.value.responseData.length === 0) {
page.value.responseData = responseData.value
}
if (page.value.pageType === '' || !page.value.pageType) {
page.value.pageType = 'empty'
}
handleNumberSizeChange()
})
const handleNumberSizeChange = () => {
if (page.value.pageType === 'pageNumber') {
page.value.responseData[0].resolutionPathType = 'totalNumber'
page.value.responseData[0].parameterName = t('api_pagination.total_number')
resolutionPathOptions.value = cloneDeep(defaultPathArr)
page.value.requestData[0].parameterName = t('api_pagination.page_number')
page.value.requestData[0].builtInParameterName = '${pageNumber}'
}
if (page.value.pageType === 'cursor') {
page.value.responseData[0].resolutionPathType = 'cursor'
page.value.responseData[0].parameterName = t('api_pagination.cursor')
resolutionPathOptions.value = cloneDeep(cursorPathArr)
page.value.requestData[0].parameterName = t('api_pagination.cursor')
page.value.requestData[0].builtInParameterName = '${pageToken}'
}
}
</script>
<template>
<div class="api-pagination">
<span class="type">{{ t('api_pagination.pagination_method') }}</span>
<el-select
v-model="page.pageType"
@change="handleNumberSizeChange"
style="width: 100%; margin-top: 8px"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<template v-if="page.pageType !== 'empty'">
<div class="table-title request">{{ t('datasource.request') }}</div>
<el-table header-cell-class-name="header-cell" :data="page.requestData" style="width: 100%">
<el-table-column prop="parameterName" :label="t('api_pagination.parameter_name')" />
<el-table-column
prop="builtInParameterName"
:label="t('api_pagination.built_in_parameter_name')"
/>
<el-table-column :label="t('api_pagination.parameter_default_value')" width="220">
<template #default="scope">
<el-input
v-model="scope.row.parameterDefaultValue"
style="width: 100%"
:placeholder="
scope.row.builtInParameterName === '${pageNumber}'
? t('api_pagination.enter_first_page')
: t('api_pagination.enter_default_value')
"
/>
</template>
</el-table-column>
</el-table>
<div class="table-title response">{{ t('api_pagination.response') }}</div>
<el-table header-cell-class-name="header-cell" :data="page.responseData" style="width: 100%">
<el-table-column
prop="parameterName"
:label="t('api_pagination.parameter_name')"
width="160"
/>
<el-table-column prop="resolutionPath" :label="t('api_pagination.parsing_path')">
<template #default="scope">
<el-input
v-model="scope.row.resolutionPath"
style="width: 100%"
:placeholder="t('api_pagination.please_enter_jsonpath')"
><template #prepend>
<el-select
class="bg-white"
v-model="scope.row.resolutionPathType"
style="width: 89px"
>
<el-option
v-for="item in resolutionPathOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select> </template
></el-input>
</template>
</el-table-column>
</el-table>
</template>
</div>
</template>
<style lang="less" scoped>
.api-pagination {
.type {
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
.table-title {
width: 100%;
height: 30px;
padding-left: 12px;
display: flex;
align-items: center;
&.request {
background: #ebf1ff;
margin-top: 16px;
border-top: 1px solid #dddedf;
}
&.response {
background: #e6f7f5;
}
}
.bg-white {
:deep(.ed-input__wrapper) {
background: white;
}
}
}
</style>

View File

@ -0,0 +1,15 @@
import ace from 'ace-builds'
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url'
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl)
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url'
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl)
import modeXmlUrl from 'ace-builds/src-noconflict/mode-xml?url'
ace.config.setModuleUrl('ace/mode/xml', modeXmlUrl)
import modeTextUrl from 'ace-builds/src-noconflict/mode-text?url'
ace.config.setModuleUrl('ace/mode/text', modeTextUrl)
import 'ace-builds/src-noconflict/ext-language_tools'
ace.require('ace/ext/language_tools')

View File

@ -0,0 +1,184 @@
import { isString, isObject, isNumber, isNull, isInteger, isEmpty, isBoolean } from 'lodash-es'
const isArray = Array.isArray
class Convert {
constructor() {
this._option = {
$id: 'http://example.com/root.json',
$schema: 'http://json-schema.org/draft-07/schema#'
}
this._object = null
}
/**
* 转换函数
* @param {*} object 需要转换的对象
* @param {*} ?option 可选参数目前只有能设置 root 节点的 $id $schema
*/
format(object, option = {}) {
// 数据校验确保传入的的object只能是对象或数组
if (!isObject(object)) {
throw new TypeError('传入参数只能是对象或数组')
}
// 合并属性
this._option = Object.assign(this._option, option)
// 需要转换的对象
this._object = object
let convertRes
// 数组类型和对象类型结构不一样
if (isArray(object)) {
convertRes = this._arrayToSchema()
} else {
convertRes = this._objectToSchema()
}
// 释放
this._object = null
return convertRes
}
/**
* 数组类型转换成JSONSCHEMA
*/
_arrayToSchema() {
// root节点基本信息
const result = this._value2object(this._object, this._option.$id, '', true)
if (this._object.length > 0) {
const itemArr = []
for (let index = 0; index < this._object.length; index++) {
// 创建items对象的基本信息
const objectItem = this._object[index]
let item = this._value2object(objectItem, `#/items`, 'items')
if (isObject(objectItem) && !isEmpty(objectItem)) {
// 递归遍历
const objectItemSchema = this._json2schema(objectItem, `#/items`)
// 合并对象
item = Object.assign(item, objectItemSchema)
}
itemArr.push(item)
}
result['items'] = itemArr
}
return result
}
/**
* 对象类型转换成JSONSCHEMA
*/
_objectToSchema() {
let baseResult = this._value2object(this._object, this._option.$id, '', true)
const objectSchema = this._json2schema(this._object)
baseResult = Object.assign(baseResult, objectSchema)
return baseResult
}
_json2schema(object, name = '') {
// 如果递归值不是对象那么return掉
if (!isObject(object)) {
return
}
// 处理当前路径$id
if (name === '' || name === undefined) {
name = '#'
}
const result = {}
// 判断传入object是对象还是数组
if (isArray(object)) {
result.items = {}
} else {
result.properties = {}
}
// 遍历传入的对象
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
const element = object[key]
// 如果只是undefined跳过
if (element === undefined) {
continue
}
const $id = `${name}/properties/${key}`
// 判断当前 element 的值 是否也是对象如果是就继续递归不是就赋值给result
if (!result['properties']) {
continue
}
if (isObject(element)) {
// 创建当前属性的基本信息
result['properties'][key] = this._value2object(element, $id, key)
if (isArray(element)) {
// 针对空数组和有值的数组做不同处理
if (element.length > 0) {
// 是数组
const itemArr = []
for (let index = 0; index < element.length; index++) {
const elementItem = element[index]
// 创建items对象的基本信息
let item = this._value2object(elementItem, `${$id}/items`, key + 'items')
// 判断第一项是否是对象,且对象属性不为空
if (isObject(elementItem) && !isEmpty(elementItem)) {
// 新增的properties才合并进来
item = Object.assign(item, this._json2schema(elementItem, `${$id}/items`))
}
itemArr.push(item)
}
result['properties'][key]['items'] = itemArr
}
} else {
// 不是数组递归遍历获取然后合并对象属性
result['properties'][key] = Object.assign(
result['properties'][key],
this._json2schema(element, $id)
)
}
} else {
// 一般属性直接获取基本信息
if (result['properties']) {
result['properties'][key] = this._value2object(element, $id, key)
}
}
}
}
return result
}
/**
* 把json的值转换成对象类型
* @param {*} value
* @param {*} $id
* @param {*} key
*/
_value2object(value, $id, key = '', root = false) {
const objectTemplate = {
$id: $id,
title: `The ${key} Schema`,
mock: {
mock: value
}
}
// 判断是否为初始化root数据
if (root) {
objectTemplate['$schema'] = this._option.$schema
objectTemplate['title'] = `The Root Schema`
objectTemplate['mock'] = undefined
}
if (isBoolean(value)) {
objectTemplate.type = 'boolean'
} else if (isInteger(value)) {
objectTemplate.type = 'integer'
} else if (isNumber(value)) {
objectTemplate.type = 'number'
} else if (isString(value)) {
objectTemplate.type = 'string'
} else if (isNull(value)) {
objectTemplate.type = 'null'
} else if (isArray(value)) {
objectTemplate.type = 'array'
objectTemplate['mock'] = undefined
} else if (isObject(value)) {
objectTemplate.type = 'object'
objectTemplate['mock'] = undefined
}
return objectTemplate
}
}
export default Convert

View File

@ -0,0 +1,185 @@
export function formatJson(json) {
let i = 0
let il = 0
const tab = ' '
let newJson = ''
let indentLevel = 0
let inString = false
let currentChar = null
let flag = false
for (i = 0, il = json.length; i < il; i += 1) {
currentChar = json.charAt(i)
switch (currentChar) {
case '{':
if (i !== 0 && json.charAt(i - 1) === '$') {
newJson += currentChar
flag = true
} else if (!inString) {
newJson += currentChar + '\n' + repeat(tab, indentLevel + 1)
indentLevel += 1
} else {
newJson += currentChar
}
break
case '[':
if (!inString) {
newJson += currentChar + '\n' + repeat(tab, indentLevel + 1)
indentLevel += 1
} else {
newJson += currentChar
}
break
case '}':
if (flag) {
newJson += currentChar
flag = false
} else if (!inString) {
indentLevel -= 1
newJson += '\n' + repeat(tab, indentLevel) + currentChar
} else {
newJson += currentChar
}
break
case ']':
if (!inString) {
indentLevel -= 1
newJson += '\n' + repeat(tab, indentLevel) + currentChar
} else {
newJson += currentChar
}
break
case ',':
if (!inString) {
newJson += ',\n' + repeat(tab, indentLevel)
} else {
newJson += currentChar
}
break
case ':':
if (!inString) {
newJson += ': '
} else {
newJson += currentChar
}
break
case ' ':
case '\n':
case '\t':
if (inString) {
newJson += currentChar
}
break
case '"':
if (i > 0 && json.charAt(i - 1) !== '\\') {
inString = !inString
}
newJson += currentChar
break
default:
newJson += currentChar
break
}
}
return newJson
}
function repeat(s, count) {
return new Array(count + 1).join(s)
}
export function formatXml(text) {
// 去掉多余的空格
text =
'\n' +
text.replace(/(<\w+)(\s.*?>)/g, function ($0, name, props) {
return name + ' ' + props.replace(/\s+(\w+=)/g, ' $1')
})
// 把注释编码
text = text.replace(/<!--(.+?)-->/g, function ($0, text) {
var ret = '<!--' + escape(text) + '-->'
return ret
})
// 调整格式
var rgx = /\n(<(([^\?]).+?)(?:\s|\s*?>|\s*?(\/)>)(?:.*?(?:(?:(\/)>)|(?:<(\/)\2>)))?)/gm
var nodeStack = []
var output = text.replace(
rgx,
function ($0, all, name, isBegin, isCloseFull1, isCloseFull2, isFull1, isFull2) {
var isClosed =
isCloseFull1 === '/' || isCloseFull2 === '/' || isFull1 === '/' || isFull2 === '/'
var prefix = ''
if (isBegin === '!') {
prefix = getPrefix(nodeStack.length)
} else {
if (isBegin !== '/') {
prefix = getPrefix(nodeStack.length)
if (!isClosed) {
nodeStack.push(name)
}
} else {
nodeStack.pop()
prefix = getPrefix(nodeStack.length)
}
}
var ret = '\n' + prefix + all
return ret
}
)
var outputText = output.substring(1)
// 把注释还原并解码调格式
outputText = outputText.replace(/(\s*)<!--(.+?)-->/g, function ($0, prefix, text) {
if (prefix.charAt(0) === '\r') {
prefix = prefix.substring(1)
}
text = unescape(text).replace(/\r/g, '\n')
var ret = '\n' + prefix + '<!--' + text.replace(/^\s*/gm, prefix) + '-->'
return ret
})
return outputText.replace(/\s+$/g, '').replace(/\r/g, '\r\n')
}
/**
* @param time 时间
* @param cFormat 格式
* @returns {string|null} 字符串
* @example formatTime('2018-1-29', '{y}/{m}/{d} {h}:{i}:{s}') // -> 2018/01/29 00:00:00
*/
export function formatTime(time, cFormat) {
if (arguments.length === 0) return null
if ((time + '').length === 10) {
time = +time * 1000
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
return format.replace(/{([ymdhisa])+}/g, (result, key) => {
let value = formatObj[key]
if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
if (result.length > 0 && value < 10) {
value = '0' + value
}
return value || 0
})
}
function getPrefix(prefixIndex) {
var span = ' '
var output = []
for (var i = 0; i < prefixIndex; ++i) {
output.push(span)
}
return output.join('')
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,197 @@
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
export const dsTypes = [
{
type: 'db2',
name: 'Db2',
catalog: 'OLTP',
extraParams: ''
},
{
type: 'mysql',
name: 'MySQL',
catalog: 'OLTP',
extraParams:
'characterEncoding=UTF-8&connectTimeout=5000&useSSL=false&allowPublicKeyRetrieval=true'
},
{
type: 'TiDB',
name: 'TiDB',
catalog: 'OLTP',
extraParams:
'characterEncoding=UTF-8&connectTimeout=5000&useSSL=false&allowPublicKeyRetrieval=true'
},
{
type: 'impala',
name: 'Apache Impala',
catalog: 'OLAP',
extraParams: 'AuthMech=0'
},
{
type: 'mariadb',
name: 'MariaDB',
catalog: 'OLTP',
extraParams:
'characterEncoding=UTF-8&connectTimeout=5000&useSSL=false&allowPublicKeyRetrieval=true'
},
{
type: 'doris',
name: 'Apache Doris',
catalog: 'OLAP',
extraParams:
'characterEncoding=UTF-8&connectTimeout=5000&useSSL=false&allowPublicKeyRetrieval=true'
},
{
type: 'es',
name: 'Elasticsearch',
catalog: 'OLAP',
extraParams: ''
},
{
type: 'StarRocks',
name: 'StarRocks',
catalog: 'OLAP',
extraParams:
'characterEncoding=UTF-8&connectTimeout=5000&useSSL=false&allowPublicKeyRetrieval=true'
},
{
type: 'pg',
name: 'PostgreSQL',
catalog: 'OLTP',
extraParams: ''
},
{
type: 'sqlServer',
name: 'SQL Server',
catalog: 'OLTP',
extraParams: ''
},
{
type: 'oracle',
name: 'Oracle',
catalog: 'OLTP',
extraParams: '',
charset: [
'Default',
'GBK',
'BIG5',
'ISO-8859-1',
'UTF-8',
'UTF-16',
'CP850',
'EUC_JP',
'EUC_KR'
],
targetCharset: ['Default', 'GBK', 'UTF-8']
},
{
type: 'mongo',
name: 'Mongodb-BI',
catalog: 'OLTP',
extraParams: 'rebuildschema=true&authSource=admin'
},
{
type: 'ck',
name: 'ClickHouse',
catalog: 'OLAP',
extraParams: ''
},
{
type: 'redshift',
name: 'AWS Redshift',
catalog: 'DL',
extraParams: ''
},
{
type: 'API',
name: 'API',
catalog: 'OTHER',
extraParams: ''
},
{
type: 'Excel',
name: 'Excel',
catalog: 'LOCAL',
extraParams: ''
}
]
export const typeList = ['OLTP', 'OLAP', 'DL', 'OTHER', 'LOCAL']
export const nameMap = {
OLTP: 'OLTP',
OLAP: 'OLAP',
DL: t('datasource.dl'),
OTHER: t('data_source.api_data'),
LOCAL: t('datasource.local_file')
}
export interface Configuration {
dataBase: string
jdbcUrl: string
urlType: string
connectionType: string
schema: string
extraParams: string
username: string
password: string
host: string
authMethod: string
port: string
initialPoolSize: string
minPoolSize: string
maxPoolSize: string
queryTimeout: string
useSSH: boolean
sshHost: string
sshPort: string
sshUserName: string
sshType: string
sshPassword: string
}
export interface ApiConfiguration {
id: string
name: string
type: string
deTableName: string
method: string
copy: boolean
url: string
status: string
useJsonPath: boolean
serialNumber: number
}
export interface SyncSetting {
id: string
updateType: string
syncRate: string
simpleCronValue: number
simpleCronType: string
startTime: number
endTime: number
endLimit: string
cron: string
}
export interface Node {
name: string
createBy: string
creator: string
copy: boolean
createTime: string
id: number | string
size: number
description: string
type: string
nodeType: string
fileName: string
syncSetting?: SyncSetting
editType?: number
configuration?: Configuration
apiConfiguration?: ApiConfiguration[]
paramsConfiguration?: ApiConfiguration[]
weight?: number
lastSyncTime?: number | string
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
<script lang="ts" setup>
import {ref, onMounted,watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const props = defineProps({
projectInfo: {
type: String,
default: ''
}
})
const projectInfo:any=ref({
})
watch(() => props.projectInfo, val => {
projectInfo.value = props.projectInfo
})
onMounted(()=>{
projectInfo.value = props.projectInfo
})
</script>
<template>
<div class="project-header-box">
<div class="project-header-left">
<div class="return-box" @click="$router.go(-1)">
<img src="@/assets/newimg/u594.png" alt="">
</div>
<img src="@/assets/newimg/logosmall.png" alt="" style="margin-left: 10px;">
<div class="header-title">{{projectInfo.name }}</div>
</div>
</div>
</template>
<style lang="less" scoped>
.project-header-box{
width: 100%;
height: 60px;
background-color: rgba(37, 38, 38, 1);
box-sizing: border-box;
border-bottom: 1px solid rgba(79, 80, 82, 1);
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: 0 10px;
.project-header-left{
display: flex;
align-items: center;
}
.project-header-right{
display: flex;
align-items: center;
}
.return-box{
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
cursor: pointer;
}
.return-box:hover{
background-color: rgba(255, 255, 255, 0.1);
}
.header-title{
font-family: 'Arial Negreta', 'Arial Normal', 'Arial';
font-weight: 700;
font-style: normal;
font-size: 16px;
color: #F2F4F5;
padding-left: 14px;
}
.preview-button{
width: 60px;
height: 30px;
line-height: 30px;
background-color: rgba(54, 55, 56, 1);
border: none;
border-radius: 4px;
font-family: 'Arial Normal', 'Arial';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #F2F4F5;
text-align: center;
cursor: pointer;
}
.design-button{
width: 60px;
height: 30px;
line-height: 30px;
background-color: rgba(0, 137, 255, 1);
border: none;
border-radius: 4px;
font-family: 'Arial Normal', 'Arial';
font-weight: 400;
font-style: normal;
font-size: 14px;
color: #F2F4F5;
text-align: center;
margin-left: 10px;
cursor: pointer;
}
}
</style>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { findComponentAttr } from '@/utils/components'
import { storeToRefs } from 'pinia'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import ViewEditor from '@/views/chart/components/editor/index.vue'
import { computed } from 'vue'
const dvMainStore = dvMainStoreWithOut()
const { curComponent, batchOptStatus } = storeToRefs(dvMainStore)
defineProps({
canvasViewInfoMobile: {
type: Object,
required: true
}
})
const otherEditorShow = computed(() => {
return Boolean(
curComponent.value &&
(!['UserView', 'VQuery'].includes(curComponent.value?.component) ||
(curComponent.value?.component === 'UserView' &&
curComponent.value?.innerType === 'picture-group')) &&
!batchOptStatus.value
)
})
const viewEditorShow = computed(() => {
return Boolean(
curComponent.value &&
['UserView', 'VQuery'].includes(curComponent.value.component) &&
curComponent.value.innerType !== 'picture-group' &&
!batchOptStatus.value
)
})
</script>
<template>
<div class="mobile_content">
<template v-if="otherEditorShow">
<component :is="findComponentAttr(curComponent)" :themes="'light'" />
</template>
<template v-if="viewEditorShow">
<view-editor
:themes="'light'"
:view="canvasViewInfoMobile[curComponent ? curComponent.id : 'default']"
></view-editor>
</template>
</div>
</template>
<style scoped lang="less">
.mobile_content {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,430 @@
<template>
<el-drawer
:title="t('visualization.save_app')"
v-model="state.appApplyDrawer"
custom-class="de-app-drawer"
:show-close="false"
size="500px"
direction="rtl"
z-index="1000"
>
<div class="app-export">
<el-form
ref="appSaveForm"
:model="state.form"
:rules="state.rule"
class="de-form-item app-form"
size="middle"
label-width="180px"
label-position="top"
>
<div class="de-row-rules" style="margin: 0 0 16px">
<span>{{ t('visualization.base_info') }}</span>
</div>
<el-form-item :label="dvPreName + t('visualization.name')" prop="name">
<el-input
v-model="state.form.name"
autocomplete="off"
:placeholder="t('visualization.input_tips')"
/>
</el-form-item>
<el-form-item :label="dvPreName + t('visualization.position')" prop="pid">
<el-tree-select
style="width: 100%"
@keydown.stop
@keyup.stop
v-model="state.form.pid"
:data="state.dvTree"
:props="state.propsTree"
@node-click="dvTreeSelect"
:render-after-expand="false"
filterable
>
<template #default="{ data: { name } }">
<span class="custom-tree-node">
<el-icon>
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<span :title="name">{{ name }}</span>
</span>
</template>
</el-tree-select>
</el-form-item>
<el-form-item :label="t('visualization.ds_group_name')" prop="datasetFolderName">
<el-input
v-model="state.form.datasetFolderName"
autocomplete="off"
:placeholder="t('visualization.input_tips')"
/>
</el-form-item>
<el-form-item :label="t('visualization.ds_group_position')" prop="datasetFolderPid">
<el-tree-select
style="width: 100%"
@keydown.stop
@keyup.stop
v-model="state.form.datasetFolderPid"
:data="state.dsTree"
:props="state.propsTree"
@node-click="dsTreeSelect"
:filter-method="dsTreeFilterMethod"
:render-after-expand="false"
filterable
>
<template #default="{ data: { name } }">
<span class="custom-tree-node">
<el-icon>
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<span :title="name">{{ name }}</span>
</span>
</template>
</el-tree-select>
</el-form-item>
<div class="de-row-rules" style="margin: 0 0 16px">
<span>{{ t('visualization.datasource_info') }}</span>
</div>
<el-row class="datasource-link">
<el-row class="head">
<el-col :span="11">{{ t('visualization.app_datasource') }}</el-col
><el-col :span="2"></el-col
><el-col :span="11">{{ t('visualization.sys_datasource') }}</el-col>
</el-row>
<el-row
:key="index"
class="content"
v-for="(appDatasource, index) in state.appData.datasourceInfo"
>
<el-col :span="11">
<el-select style="width: 100%" v-model="appDatasource.name" disabled>
<el-option
:key="appDatasource.name"
:label="appDatasource.name"
:value="appDatasource.name"
>
</el-option>
</el-select> </el-col
><el-col :span="2" class="icon-center">
<Icon name="dv-link-target"
><dvLinkTarget class="svg-icon" style="width: 20px; height: 20px" /></Icon></el-col
><el-col :span="11">
<dataset-select
ref="datasetSelector"
v-model="appDatasource.systemDatasourceId"
style="flex: 1"
:state-obj="state"
themes="light"
source-type="datasource"
@add-ds-window="addDsWindow"
view-id="0"
/>
</el-col>
</el-row>
</el-row>
</el-form>
</div>
<template #footer>
<div class="apply" style="width: 100%">
<el-button v-if="isDesktop() || openType === '_self'" @click="goBack">{{
t('visualization.back')
}}</el-button>
<el-button type="primary" @click="saveApp">{{ t('visualization.save') }}</el-button>
</div>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import dvFolder from '@/assets/svg/dv-folder.svg'
import dvLinkTarget from '@/assets/svg/dv-link-target.svg'
import {
ElButton,
ElDrawer,
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElTreeSelect
} from 'element-plus-secondary'
import { computed, PropType, reactive, ref, toRefs } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { queryTreeApi } from '@/api/visualization/dataVisualization'
import { BusiTreeNode, BusiTreeRequest } from '@/models/tree/TreeNode'
import { getDatasetTree } from '@/api/dataset'
import DatasetSelect from '@/views/chart/components/editor/dataset-select/DatasetSelect.vue'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
import { deepCopy } from '@/utils/utils'
import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot'
import { useCache } from '@/hooks/web/useCache'
import { isDesktop } from '@/utils/ModelUtil'
import { filterFreeFolder } from '@/utils/utils'
const desktop = isDesktop()
const { wsCache } = useCache('localStorage')
const { t } = useI18n()
const emits = defineEmits(['closeDraw', 'saveAppCanvas'])
const appSaveForm = ref(null)
const dvMainStore = dvMainStoreWithOut()
const { dvInfo, appData } = storeToRefs(dvMainStore)
const snapshotStore = snapshotStoreWithOut()
const props = defineProps({
componentData: {
type: Object,
required: true
},
canvasViewInfo: {
type: Object,
required: true
},
curCanvasType: {
type: String,
required: true
},
themes: {
type: String as PropType<EditorTheme>,
default: 'dark'
}
})
const { componentData, canvasViewInfo, curCanvasType, themes } = toRefs(props)
const openType = wsCache.get('open-backend') === '1' ? '_self' : '_blank'
const dvPreName = computed(() =>
curCanvasType.value === 'dashboard'
? t('work_branch.dashboard')
: t('work_branch.big_data_screen')
)
const addDsWindow = () => {
// do addDsWindow
const url = '#/data/datasource?opt=create'
window.open(url, openType)
}
const state = reactive({
appApplyDrawer: false,
dvTree: [],
dsTree: [],
propsTree: {
label: 'name',
children: 'children',
isLeaf: node => !node.children?.length
},
appData: {
datasourceInfo: []
},
form: {
pid: '',
name: t('visualization.new'),
datasetFolderPid: null,
datasetFolderName: null
},
rule: {
name: [
{
required: true,
min: 2,
max: 25,
message: t('datasource.input_limit_2_25', [2, 25]),
trigger: 'blur'
}
],
pid: [
{
required: true,
message: t('visualization.select_folder'),
trigger: 'blur'
}
],
datasetFolderName: [
{
required: true,
min: 2,
max: 25,
message: t('datasource.input_limit_2_25', [2, 25]),
trigger: 'blur'
}
],
datasetFolderPid: [
{
required: true,
message: t('visualization.select_ds_group_folder'),
trigger: 'blur'
}
]
}
})
const goBack = () => {
window.history.back()
}
const initData = () => {
const request = { busiFlag: curCanvasType.value, leaf: false, weight: 7 }
queryTreeApi(request).then(res => {
filterFreeFolder(res, curCanvasType.value)
const resultTree = res || []
dfs(resultTree as unknown as BusiTreeNode[])
state.dvTree = (resultTree as unknown as BusiTreeNode[]) || []
if (state.dvTree.length && state.dvTree[0].name === 'root' && state.dvTree[0].id === '0') {
state.dvTree[0].name =
curCanvasType.value === 'dataV'
? t('work_branch.big_data_screen')
: t('work_branch.dashboard')
}
})
const requestDs = { leaf: false, weight: 7 } as BusiTreeRequest
getDatasetTree(requestDs).then(res => {
filterFreeFolder(res, 'dataset')
dfs(res as unknown as BusiTreeNode[])
state.dsTree = (res as unknown as BusiTreeNode[]) || []
if (state.dsTree.length && state.dsTree[0].name === 'root' && state.dsTree[0].id === '0') {
state.dsTree[0].name = t('visualization.dataset')
}
})
}
const dfs = (arr: BusiTreeNode[]) => {
arr.forEach(ele => {
ele['value'] = ele.id
if (ele.children?.length) {
dfs(ele.children)
}
})
}
const init = params => {
state.appApplyDrawer = true
state.form = params.base
state.appData.datasourceInfo = deepCopy(appData.value?.datasourceInfo)
initData()
}
const dvTreeSelect = element => {
state.form.pid = element.id
}
const dsTreeSelect = element => {
state.form.datasetFolderPid = element.id
}
const close = () => {
emits('closeDraw')
snapshotStore.recordSnapshotCache('renderChart')
state.appApplyDrawer = false
}
const saveApp = () => {
let datasourceMatchReady = true
state.appData.datasourceInfo.forEach(datasource => {
if (!datasource.systemDatasourceId) {
datasourceMatchReady = false
}
})
if (!datasourceMatchReady) {
ElMessage.error(t('visualization.app_no_datasource_tips'))
return
}
appSaveForm.value?.validate(valid => {
if (valid) {
// datasource
appData.value['datasourceInfo'] = state.appData.datasourceInfo
dvInfo.value['pid'] = state.form.pid
dvInfo.value['name'] = state.form.name
dvInfo.value['datasetFolderPid'] = state.form.datasetFolderPid
dvInfo.value['datasetFolderName'] = state.form.datasetFolderName
dvInfo.value['dataState'] = 'ready'
snapshotStore.recordSnapshotCache('renderChart')
emits('saveAppCanvas')
} else {
return false
}
})
}
defineExpose({
init,
close
})
</script>
<style lang="less" scoped>
.app-export {
width: 100%;
height: calc(100% - 56px);
}
.app-export-bottom {
width: 100%;
height: 56px;
text-align: right;
}
:deep(.ed-drawer__body) {
padding-bottom: 0 !important;
}
.de-row-rules {
display: flex;
align-items: center;
position: relative;
font-size: 14px;
font-weight: 500;
line-height: 22px;
padding-left: 10px;
margin: 24px 0 16px 0;
color: var(--ed-text-color-regular);
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
height: 14px;
width: 2px;
background: #3370ff;
}
}
.custom-tree-node {
display: flex;
align-items: center;
span {
margin-left: 8.75px;
width: 120px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
.datasource-link {
color: var(--ed-text-color-regular);
font-size: 12px;
font-weight: 500;
width: 100%;
.head {
width: 100%;
}
.content {
width: 100%;
margin-top: 8px;
}
}
.icon-center {
padding: 0 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.app-form {
padding-bottom: 95px;
}
</style>
<style lang="less">
.de-app-drawer {
z-index: 1000;
}
</style>

View File

@ -0,0 +1,83 @@
<script lang="ts" setup>
import icon_left_outlined from '@/assets/svg/icon_left_outlined.svg'
import icon_right_outlined from '@/assets/svg/icon_right_outlined.svg'
import { useAppStoreWithOut } from '@/store/modules/app'
const appStore = useAppStoreWithOut()
defineProps({
isInside: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['changeSideTreeStatus'])
const handleClick = val => {
appStore.setArrowSide(val)
emits('changeSideTreeStatus', val)
}
</script>
<template>
<div
@click="handleClick(false)"
v-if="appStore.getArrowSide && !isInside"
class="arrow-side-tree arrow-side-tree-left"
>
<el-icon>
<Icon name="icon_left_outlined"><icon_left_outlined class="svg-icon" /></Icon>
</el-icon>
</div>
<div
@click="handleClick(true)"
v-else-if="!appStore.getArrowSide && isInside"
class="arrow-side-tree arrow-side-tree-right"
>
<el-icon>
<Icon name="icon_right_outlined"><icon_right_outlined class="svg-icon" /></Icon>
</el-icon>
</div>
</template>
<style lang="less" scoped>
.arrow-side-tree-left {
top: 44px;
height: 24px;
width: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0px 5px 10px 0px #1f23291a;
}
.arrow-side-tree-right {
box-shadow: 0px 4px 8px 0px #0000001a;
top: 44px;
height: 24px;
width: 20px;
display: flex;
align-items: center;
padding-left: 2px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
&:hover {
padding-left: 4px;
width: 24px;
}
}
.arrow-side-tree {
position: absolute;
border: 1px solid #dee0e3;
background: #fff;
cursor: pointer;
z-index: 10;
&:hover {
.ed-icon {
color: var(--ed-color-primary);
}
}
.ed-icon {
font-size: 12px;
}
}
</style>

View File

@ -0,0 +1,260 @@
<template>
<el-dialog
class="create-dialog"
:title="t('visualization.new_from_template')"
v-model="state.dialogShow"
width="700"
:before-close="close"
@submit.prevent
>
<el-row class="create-main" v-loading="state.loading">
<el-row>
<el-col :span="18" style="height: 40px">
<el-radio v-model="state.inputType" label="new_outer_template"
>{{ t('visualization.import_template') }}
</el-radio>
<el-radio v-model="state.inputType" label="new_inner_template" @click="getTree"
>{{ t('visualization.copy_template') }}
</el-radio>
</el-col>
<el-col v-if="state.inputType === 'new_outer_template'" :span="6" class="button-main">
<el-button class="el-icon-upload" size="small" type="primary" @click="goFile"
>{{ t('visualization.upload_template') }}
</el-button>
<input
id="input"
ref="files"
type="file"
accept=".DET2"
hidden
@change="handleFileChange"
/>
</el-col>
</el-row>
<el-row style="margin-top: 5px">
<el-col :span="4" class="name-area">{{ t('visualization.name') }}</el-col>
<el-col :span="20">
<el-input v-model="state.dvCreateInfo.name" clearable size="small" />
</el-col>
</el-row>
<el-row v-if="state.inputType === 'new_inner_template'" class="preview">
<el-col :span="8" style="height: 100%; overflow-y: auto">
<de-template-preview-list
:template-list="state.templateList"
@showCurrentTemplateInfo="showCurrentTemplateInfo"
/>
</el-col>
<el-col :span="16" :style="classBackground" class="preview-show" />
</el-row>
<el-row
v-if="state.inputType === 'new_outer_template'"
class="preview"
:style="classBackground"
/>
<el-row class="root-class">
<el-button size="small" @click="cancel()">{{ t('commons.cancel') }} </el-button>
<el-button type="primary" size="small" :disabled="!saveStatus" @click="save()"
>{{ t('commons.confirm') }}
</el-button>
</el-row>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import { showTemplateList } from '@/api/template'
import { useI18n } from '@/hooks/web/useI18n'
import { computed, reactive, ref, watch } from 'vue'
import { imgUrlTrans } from '@/utils/imgUtils'
import { ElMessage } from 'element-plus-secondary'
import { decompression } from '@/api/visualization/dataVisualization'
import DeTemplatePreviewList from '@/views/common/DeTemplatePreviewList.vue'
const { t } = useI18n()
const emits = defineEmits(['finish'])
const files = ref(null)
const props = defineProps({
curCanvasType: {
type: String,
required: true
}
})
const state = reactive({
dialogShow: false,
loading: false,
inputType: 'new_outer_template',
fieldName: 'name',
tableRadio: null,
keyWordSearch: '',
columnLabel: t('visualization.belong_to_category'),
templateList: [],
importTemplateInfo: {
snapshot: ''
},
dvCreateInfo: {
pid: -1,
name: null,
canvasStyleData: null,
componentData: null,
templateId: null,
dynamicData: null,
staticResource: null
},
templateSelected: false
})
const saveStatus = computed(() => {
return state.dvCreateInfo.name && state.templateSelected
})
const classBackground = computed(() => {
if (state.importTemplateInfo.snapshot) {
return {
background: `url(${imgUrlTrans(state.importTemplateInfo.snapshot)}) no-repeat`
}
} else {
return {}
}
})
watch(
() => state.inputType,
() => {
createInit()
}
)
const createInit = () => {
state.templateSelected = false
state.dvCreateInfo.name = null
state.dvCreateInfo.canvasStyleData = null
state.dvCreateInfo.componentData = null
state.importTemplateInfo.snapshot = null
state.dvCreateInfo.templateId = null
}
const showCurrentTemplateInfo = data => {
state.dvCreateInfo.templateId = data.id
if (data.nodeType === 'folder') {
state.dvCreateInfo.name = null
state.importTemplateInfo.snapshot = null
state.templateSelected = false
} else {
state.dvCreateInfo.name = data.name
state.importTemplateInfo.snapshot = data.snapshot
state.templateSelected = true
}
}
const getTree = () => {
const request = {
level: '0',
leafDvType: props.curCanvasType,
withChildren: true
}
state.loading = true
showTemplateList(request).then(res => {
state.templateList = res.data
state.loading = false
})
}
const cancel = () => {
emits('finish')
}
const save = () => {
if (!state.dvCreateInfo.name) {
ElMessage.warning(t('common.save_success'))
return false
}
if (state.dvCreateInfo.name.length > 50) {
ElMessage.warning(t('common.char_can_not_more_50'))
return false
}
if (!state.dvCreateInfo.templateId && state.inputType === 'new_inner_template') {
ElMessage.warning('chart.template_can_not_empty')
return false
}
state.dvCreateInfo['newFrom'] = state.inputType
state.loading = true
decompression(state.dvCreateInfo)
.then(response => {
state.loading = false
emits('finish', response.data)
})
.catch(() => {
state.loading = false
})
}
const handleFileChange = e => {
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = res => {
state.templateSelected = true
const result = res.target.result
state.importTemplateInfo = JSON.parse(result)
state.dvCreateInfo.name = state.importTemplateInfo['name'].name
state.dvCreateInfo.canvasStyleData = state.importTemplateInfo['canvasStyleData']
state.dvCreateInfo.componentData = state.importTemplateInfo['componentData']
state.dvCreateInfo.dynamicData = state.importTemplateInfo['dynamicData']
state.dvCreateInfo.staticResource = state.importTemplateInfo['staticResource']
}
reader.readAsText(file)
}
const goFile = () => {
files.value.click()
}
const close = () => {
state.dialogShow = false
}
const optInit = () => {
state.dialogShow = true
createInit()
}
defineExpose({
optInit
})
</script>
<style scoped lang="less">
.create-main {
display: inherit;
}
.name-area {
display: flex;
align-items: center;
justify-content: left;
}
.button-main {
display: flex;
align-items: center;
justify-content: right;
}
.root-class {
display: flex;
align-items: center;
justify-content: right;
margin: 15px 0px 5px;
}
.preview {
margin-top: 5px;
border: 1px solid #e6e6e6;
height: 310px !important;
overflow: hidden;
background-size: 100% 100% !important;
}
.preview-show {
border-left: 1px solid #e6e6e6;
height: 310px;
background-size: 100% 100% !important;
}
</style>

View File

@ -0,0 +1,49 @@
<template>
<el-dialog
class="market-create-dialog"
v-model="state.dialogShow"
width="80vw"
height="90vh"
:before-close="close"
@submit.prevent
>
<template-market ref="templateMarketCreateRef" @close="close"></template-market>
</el-dialog>
</template>
<script setup lang="ts">
import TemplateMarket from '@/views/template-market/index.vue'
import { nextTick, reactive, ref } from 'vue'
const templateMarketCreateRef = ref(null)
const state = reactive({
dialogShow: false
})
const close = () => {
state.dialogShow = false
}
const optInit = param => {
state.dialogShow = true
nextTick(() => {
templateMarketCreateRef.value.optInit(param)
})
}
defineExpose({
optInit
})
</script>
<style lang="less">
.market-create-dialog {
border-radius: 4px !important;
overflow: hidden;
.ed-dialog__body {
padding: 0 !important;
}
.ed-dialog__header {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,455 @@
<script lang="ts" setup>
import dvFolder from '@/assets/svg/dv-folder.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import { ref, reactive, computed, watch, toRefs, nextTick } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useCache } from '@/hooks/web/useCache'
import nothingTree from '@/assets/img/nothing-tree.png'
import { BusiTreeNode } from '@/models/tree/TreeNode'
import {
copyResource,
dvNameCheck,
moveResource,
queryTreeApi,
ResourceOrFolder,
updateBase,
saveCanvas
} from '@/api/visualization/dataVisualization'
import { ElMessage } from 'element-plus-secondary'
import { cutTargetTree, filterFreeFolder, nameTrim } from '@/utils/utils'
const props = defineProps({
curCanvasType: {
type: String,
required: true
}
})
const { curCanvasType } = toRefs(props)
const { wsCache } = useCache('localStorage')
const { t } = useI18n()
const state = reactive({
tData: [],
nameList: []
})
const showParentSelected = ref(false)
const loading = ref(false)
const nodeType = ref()
const pid = ref()
const id = ref()
const cmd = ref('')
const treeRef = ref()
const filterText = ref('')
const resourceFormNameLabel = ref('')
const resourceForm = reactive({
pid: '',
pName: null,
name: '新建'
})
const sourceLabel = computed(() =>
curCanvasType.value === 'dataV' ? t('work_branch.big_data_screen') : t('work_branch.dashboard')
)
const methodMap = {
move: moveResource,
copy: copyResource,
newFolder: saveCanvas
}
const searchEmpty = ref(false)
const filterNode = (value: string, data: BusiTreeNode) => {
nextTick(() => {
searchEmpty.value = treeRef.value.isEmpty
})
if (!value) return true
return data.name.includes(value)
}
watch(filterText, val => {
treeRef.value.filter(val)
})
const nameRepeat = value => {
if (!nameList || nameList.length === 0) {
return false
}
return nameList.some(name => name === value)
}
const nameValidator = (_, value, callback) => {
if (nameRepeat(value)) {
callback(new Error(t('visualization.name_repeat')))
} else {
callback()
}
}
const showPid = computed(() => {
return ['newLeaf', 'copy', 'newLeafAfter'].includes(cmd.value) && showParentSelected.value
})
const showName = computed(() => {
return !['newLeafAfter', 'move'].includes(cmd.value)
})
let nameList = []
const resourceFormRules = ref()
const resource = ref()
const resourceDialogShow = ref(false)
const dialogTitle = ref('')
let tData = []
const filterMethod = value => {
state.tData = [...tData].filter(item => item.name.includes(value))
}
const resetForm = () => {
dialogTitle.value = null
resourceFormNameLabel.value = ''
resourceForm.name = t('visualization.new')
resourceForm.pid = ''
resourceDialogShow.value = false
}
const dfs = (arr: BusiTreeNode[]) => {
arr.forEach(ele => {
ele['value'] = ele.id
if (ele.children?.length) {
dfs(ele.children)
}
})
}
const getDialogTitle = exec => {
return {
newFolder: t('visualization.new_folder'),
newLeaf:
props.curCanvasType === 'dataV'
? t('visualization.new_screen')
: t('visualization.new_dashboard'),
move: t('visualization.move_to'),
copy: t('visualization.copy') + sourceLabel.value,
rename: t('visualization.rename'),
newLeafAfter: t('visualization.belong_folder')
}[exec]
}
const placeholder = ref('')
const optInit = (type, data: BusiTreeNode, exec, parentSelect = false) => {
showParentSelected.value = parentSelect
nodeType.value = type
const optSource = data.leaf || type === 'leaf' ? sourceLabel.value : t('visualization.folder')
const placeholderLabel =
data.leaf || type === 'leaf'
? props.curCanvasType === 'dataV'
? t('work_branch.big_data_screen')
: t('work_branch.dashboard')
: t('visualization.folder')
placeholder.value = t('visualization.input_name_tips', [placeholderLabel])
filterText.value = ''
dialogTitle.value = getDialogTitle(exec) + ('rename' === exec ? optSource : '')
resourceFormNameLabel.value = (exec === 'move' ? '' : optSource) + t('visualization.name')
const request = { busiFlag: curCanvasType.value, leaf: false, weight: 7 }
if (['newFolder'].includes(exec)) {
resourceForm.name = ''
} else if ('copy' === exec) {
resourceForm.name = data.name + '_copy'
} else {
resourceForm.name = data.name
}
queryTreeApi(request).then(res => {
filterFreeFolder(res, curCanvasType.value)
const resultTree = res || []
dfs(resultTree as unknown as BusiTreeNode[])
state.tData = (resultTree as unknown as BusiTreeNode[]) || []
if (state.tData.length && state.tData[0].name === 'root' && state.tData[0].id === '0') {
state.tData[0].name =
curCanvasType.value === 'dataV'
? t('work_branch.big_data_screen')
: t('work_branch.dashboard')
}
tData = [...state.tData]
if ('move' === exec) {
cutTargetTree(state.tData, data.id)
}
if (['newLeaf', 'newFolder'].includes(exec)) {
resourceForm.pid = data.id as string
pid.value = data.id
} else {
id.value = data.id
}
})
cmd.value = exec
resourceDialogShow.value = true
resourceFormRules.value = {
name: [
{
required: true,
message: placeholder.value,
trigger: 'change'
},
{
required: true,
message: placeholder.value,
trigger: 'blur'
},
{
min: 1,
max: 64,
message: t('commons.char_1_64'),
trigger: 'change'
},
{ required: true, trigger: 'blur', validator: nameValidator }
],
pid: [
{
required: true,
message: t('common.please_select'),
trigger: 'blur'
}
]
}
setTimeout(() => {
resource.value.clearValidate()
}, 50)
}
const editeInit = (param: BusiTreeNode) => {
pid.value = param['pid']
id.value = param.id
}
const propsTree = {
label: 'name',
children: 'children',
isLeaf: node => !node.children?.length
}
const nodeClick = (data: BusiTreeNode) => {
resourceForm.pid = data.id as string
resourceForm.pName = data.name as string
}
const checkParent = params => {
if (params.pid !== 0 && !params.pid) {
ElMessage.error(t('visualization.select_target_folder'))
return false
}
// pName
if (filterText.value && !resourceForm.pName.includes(filterText.value)) {
ElMessage.error(t('visualization.select_target_folder'))
return false
}
// ID
if (params.pid === params.id) {
ElMessage.warning(t('visualization.select_target_tips'))
return
}
return true
}
const saveResource = () => {
resource.value.validate(async result => {
if (result) {
const params: ResourceOrFolder = {
nodeType: nodeType.value as 'folder' | 'leaf',
name: resourceForm.name,
type: curCanvasType.value
}
switch (cmd.value) {
case 'move':
params.pid = resourceForm.pid as string
params.id = id.value
break
case 'copy':
params.id = id.value
params.pid = resourceForm.pid || pid.value || '0'
break
case 'rename':
params.pid = pid.value as string
params.id = id.value
break
default:
params.pid = resourceForm.pid || pid.value || '0'
break
}
nameTrim(params, t('components.length_1_64_characters'))
if (cmd.value === 'move' && !checkParent(params)) {
return
}
if (['newLeaf', 'newLeafAfter', 'newFolder', 'rename', 'move', 'copy'].includes(cmd.value)) {
await dvNameCheck({ opt: cmd.value, ...params })
}
if (cmd.value === 'newLeaf') {
resourceDialogShow.value = false
emits('finish', { opt: 'newLeaf', ...params })
} else {
loading.value = true
const method = methodMap[cmd.value] ? methodMap[cmd.value] : updateBase
method(params)
.then(data => {
loading.value = false
resourceDialogShow.value = false
emits('finish')
ElMessage.success(t('visualization.save_success'))
if (cmd.value === 'copy') {
const openType = wsCache.get('open-backend') === '1' ? '_self' : '_blank'
const baseUrl =
curCanvasType.value === 'dataV'
? '#/dvCanvas?opt=copy&dvId='
: '#/dashboard?opt=copy&resourceId='
window.open(baseUrl + data.data, openType)
}
})
.finally(() => {
loading.value = false
})
}
}
})
}
defineExpose({
optInit,
editeInit
})
const emits = defineEmits(['finish'])
</script>
<template>
<el-dialog
class="create-dialog"
:title="dialogTitle"
v-model="resourceDialogShow"
:width="cmd === 'move' ? '600px' : '420px'"
:before-close="resetForm"
@submit.prevent
>
<el-form
v-loading="loading"
label-position="top"
require-asterisk-position="right"
ref="resource"
:model="resourceForm"
:rules="resourceFormRules"
>
<el-form-item v-if="showName" :label="resourceFormNameLabel" prop="name">
<el-input
@keydown.stop
@keyup.stop
:placeholder="placeholder"
v-model="resourceForm.name"
/>
</el-form-item>
<el-form-item v-if="showPid" :label="t('visualization.belong_folder')" prop="pid">
<el-tree-select
style="width: 100%"
@keydown.stop
@keyup.stop
v-model="resourceForm.pid"
:data="state.tData"
:props="propsTree"
@node-click="nodeClick"
:filter-method="filterMethod"
:render-after-expand="false"
filterable
>
<template #default="{ data: { name } }">
<span class="custom-tree-node">
<el-icon>
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<span :title="name">{{ name }}</span>
</span>
</template>
</el-tree-select>
</el-form-item>
<div v-if="cmd === 'move'">
<el-input style="margin-bottom: 12px" v-model="filterText" clearable>
<template #prefix>
<el-icon>
<Icon name="icon_search-outline_outlined"
><icon_searchOutline_outlined class="svg-icon"
/></Icon>
</el-icon>
</template>
</el-input>
<div class="tree-content">
<el-tree
ref="treeRef"
:filter-node-method="filterNode"
filterable
v-model="resourceForm.pid"
empty-text=""
menu
:data="state.tData"
:props="propsTree"
@node-click="nodeClick"
>
<template #default="{ data }">
<span class="custom-tree-node">
<el-icon style="font-size: 18px">
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<span :title="data.name">{{ data.name }}</span>
</span>
</template>
</el-tree>
<div v-if="searchEmpty" class="empty-search">
<img :src="nothingTree" />
<span>{{ t('visualization.no_content') }}</span>
</div>
</div>
</div>
</el-form>
<template #footer>
<el-button secondary @click="resetForm()">{{ t('visualization.cancel') }} </el-button>
<el-button type="primary" @click="saveResource()"
>{{ t('visualization.confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<style lang="less" scoped>
.tree-content {
width: 552px;
height: 380px;
border: 1px solid #dee0e3;
border-radius: 4px;
padding: 8px;
overflow-y: auto;
.empty-search {
width: 100%;
margin-top: 57px;
display: flex;
flex-direction: column;
align-items: center;
img {
width: 100px;
height: 100px;
margin-bottom: 8px;
}
span {
font-family: var(--de-custom_font, 'PingFang');
font-size: 14px;
font-weight: 400;
line-height: 22px;
color: #646a73;
}
}
}
.custom-tree-node {
display: flex;
align-items: center;
span {
margin-left: 8.75px;
width: 120px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
</style>

View File

@ -0,0 +1,925 @@
<script setup lang="ts">
import dvDashboardSpineMobile from '@/assets/svg/dv-dashboard-spine-mobile.svg'
import icon_add_outlined from '@/assets/svg/icon_add_outlined.svg'
import dvCopyDark from '@/assets/svg/dv-copy-dark.svg'
import dvDelete from '@/assets/svg/dv-delete.svg'
import dvMove from '@/assets/svg/dv-move.svg'
import { treeDraggbleChart } from '@/utils/treeDraggbleChart'
import { debounce } from 'lodash-es'
import dvRename from '@/assets/svg/dv-rename.svg'
import dvDashboardSpine from '@/assets/svg/dv-dashboard-spine.svg'
import dvScreenSpine from '@/assets/svg/dv-screen-spine.svg'
import dvNewFolder from '@/assets/svg/dv-new-folder.svg'
import icon_fileAdd_outlined from '@/assets/svg/icon_file-add_outlined.svg'
import dvUseTemplate from '@/assets/svg/dv-use-template.svg'
import icon_searchOutline_outlined from '@/assets/svg/icon_search-outline_outlined.svg'
import dvSortAsc from '@/assets/svg/dv-sort-asc.svg'
import dvSortDesc from '@/assets/svg/dv-sort-desc.svg'
import dvFolder from '@/assets/svg/dv-folder.svg'
import icon_operationAnalysis_outlined from '@/assets/svg/icon_operation-analysis_outlined.svg'
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
import { onMounted, reactive, ref, toRefs, watch, nextTick, computed } from 'vue'
import {
copyResource,
deleteLogic,
ResourceOrFolder,
queryShareBaseApi
} from '@/api/visualization/dataVisualization'
import { ElIcon, ElMessage, ElMessageBox, ElScrollbar } from 'element-plus-secondary'
import { Icon } from '@/components/icon-custom'
import { useEmitt } from '@/hooks/web/useEmitt'
import { HandleMore } from '@/components/handle-more'
import DeResourceGroupOpt from '@/views/common/DeResourceGroupOpt.vue'
import { useEmbedded } from '@/store/modules/embedded'
import { BusiTreeNode, BusiTreeRequest } from '@/models/tree/TreeNode'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { useAppStoreWithOut } from '@/store/modules/app'
import { storeToRefs } from 'pinia'
import DvHandleMore from '@/components/handle-more/src/DvHandleMore.vue'
import { interactiveStoreWithOut } from '@/store/modules/interactive'
import { useShareStoreWithOut } from '@/store/modules/share'
const shareStore = useShareStoreWithOut()
const interactiveStore = interactiveStoreWithOut()
import { useI18n } from '@/hooks/web/useI18n'
import _ from 'lodash'
import DeResourceCreateOptV2 from '@/views/common/DeResourceCreateOptV2.vue'
import { useCache } from '@/hooks/web/useCache'
import { findParentIdByChildIdRecursive } from '@/utils/canvasUtils'
import { XpackComponent } from '@/components/plugin'
import treeSort, { treeParentWeight } from '@/utils/treeSortUtils'
import router from '@/router'
import { cancelRequestBatch } from '@/config/axios/service'
import { isFreeFolder } from '@/utils/utils'
const { wsCache } = useCache()
const dvMainStore = dvMainStoreWithOut()
const appStore = useAppStoreWithOut()
const embeddedStore = useEmbedded()
const { dvInfo } = storeToRefs(dvMainStore)
const { t } = useI18n()
const props = defineProps({
curCanvasType: {
type: String,
required: true
},
showPosition: {
required: false,
type: String,
default: 'preview'
}
})
const defaultProps = {
children: 'children',
label: 'name'
}
const mounted = ref(false)
const rootManage = ref(false)
const anyManage = ref(false)
const { curCanvasType, showPosition } = toRefs(props)
const resourceLabel =
curCanvasType.value === 'dataV' ? t('work_branch.big_data_screen') : t('work_branch.dashboard')
const newResourceLabel =
curCanvasType.value === 'dataV' ? t('visualization.new_screen') : t('visualization.new_dashboard')
const selectedNodeKey = ref(null)
const filterText = ref(null)
const expandedArray = ref([])
const resourceListTree = ref()
const resourceGroupOpt = ref()
const resourceCreateOpt = ref()
const returnMounted = ref(false)
const state = reactive({
pWeightMap: {},
curSortType: 'time_desc',
resourceTree: [] as BusiTreeNode[],
originResourceTree: [] as BusiTreeNode[],
folderMenuList: [
{
label: t('visualization.move_to'), //''
command: 'move',
svgName: dvMove
},
{
label: t('visualization.rename'), //''
command: 'rename',
svgName: dvRename
},
{
label: t('visualization.delete'), //
command: 'delete',
svgName: dvDelete,
divided: true
}
],
sortType: [
{
label: t('visualization.time_asc'), //''
value: 'time_asc'
},
{
label: t('visualization.time_desc'), //''
value: 'time_desc'
},
{
label: t('visualization.name_asc'), //''
value: 'name_asc'
},
{
label: t('visualization.name_desc'), //''
value: 'name_desc'
}
],
templateCreatePid: 0
})
const dvSvgType = computed(() =>
curCanvasType.value === 'dashboard' ? dvDashboardSpine : dvScreenSpine
)
const isEmbedded = computed(() => appStore.getIsDataEaseBi || appStore.getIsIframe)
const resourceTypeList = computed(() => {
const list = [
{
label: t('work_branch.new_empty'), //'',
svgName: dvSvgType.value,
command: 'newLeaf'
},
{
label: t('work_branch.new_using_template'),
svgName: dvUseTemplate,
command: 'newFromTemplate'
},
{
label: t('work_branch.new_folder'), //''
divided: true,
svgName: dvFolder,
command: 'newFolder'
}
]
return list
})
const { handleDrop, allowDrop, handleDragStart } = treeDraggbleChart(
state,
'resourceTree',
curCanvasType.value
)
const menuListWeight = id => {
const pWeight = state.pWeightMap[id]
return pWeight < 7 ? menuList : menuListWithCopy
}
const menuListWithCopy = [
{
label: t('visualization.copy'), //'',
command: 'copy',
svgName: dvCopyDark
},
{
label: t('visualization.move_to'), //'',
command: 'move',
svgName: dvMove
},
{
label: t('visualization.rename'), //'',
command: 'rename',
svgName: dvRename
},
{
label: t('visualization.delete'), //'',
command: 'delete',
svgName: dvDelete,
divided: true
}
]
const menuList = [
{
label: t('visualization.move_to'), //'',
command: 'move',
svgName: dvMove
},
{
label: t('visualization.rename'), //'',
command: 'rename',
svgName: dvRename
},
{
label: t('visualization.delete'), //'',
command: 'delete',
svgName: dvDelete,
divided: true
}
]
const infoId = wsCache.get(curCanvasType.value === 'dashboard' ? 'db-info-id' : 'dv-info-id')
const routerDvId = router.currentRoute.value.query.dvId
const dvId = embeddedStore.dvId || infoId || routerDvId
wsCache.delete(curCanvasType.value === 'dashboard' ? 'db-info-id' : 'dv-info-id')
if (dvId && showPosition.value === 'preview') {
selectedNodeKey.value = dvId
returnMounted.value = true
}
const nodeExpand = data => {
if (data.id) {
expandedArray.value.push(data.id)
}
}
const nodeCollapse = data => {
if (data.id) {
expandedArray.value.splice(expandedArray.value.indexOf(data.id), 1)
}
}
const filterNode = (value: string, data: BusiTreeNode) => {
if (!value) return true
return data.name?.toLocaleLowerCase().includes(value.toLocaleLowerCase())
}
//
const cancelPreRequest = () => {
cancelRequestBatch('/dataVisualization/findById')
cancelRequestBatch('/chartData/getData')
cancelRequestBatch('/linkage/getVisualizationAllLinkageInfo/**')
cancelRequestBatch('/linkJump/queryVisualizationJumpInfo/**')
}
const nodeClick = (data: BusiTreeNode) => {
cancelPreRequest()
selectedNodeKey.value = data.id
if (data.leaf) {
emit('nodeClick', data)
} else {
resourceListTree.value.setCurrentKey(null)
}
}
const getTree = async () => {
const request = { busiFlag: curCanvasType.value } as BusiTreeRequest
const isDashboard = curCanvasType.value == 'dashboard'
await interactiveStore.setInteractive(request)
const interactiveData = isDashboard ? interactiveStore.getPanel : interactiveStore.getScreen
const nodeData = interactiveData.treeNodes
rootManage.value = interactiveData.rootManage
anyManage.value = interactiveData.anyManage
if (
dvInfo.value &&
dvInfo.value.id &&
!JSON.stringify(nodeData).includes(dvInfo.value.id) &&
showPosition.value !== 'multiplexing'
) {
dvMainStore.resetDvInfo()
}
let curSortType = sortList[Number(wsCache.get('TreeSort-backend')) ?? 1].value
curSortType = wsCache.get(`TreeSort-${curCanvasType.value}`) ?? curSortType
if (nodeData.length && nodeData[0]['id'] === '0' && nodeData[0]['name'] === 'root') {
state.originResourceTree = nodeData[0]['children'] || []
sortTypeChange(curSortType)
afterTreeInit()
return
}
state.originResourceTree = nodeData
sortTypeChange(curSortType)
afterTreeInit()
}
const flattedTree = computed<BusiTreeNode[]>(() => {
return _.filter(flatTree(state.resourceTree), node => node.leaf)
})
const hasData = computed<boolean>(() => flattedTree.value.length > 0)
function flatTree(tree: BusiTreeNode[]) {
let result = _.cloneDeep(tree)
_.forEach(tree, node => {
if (node.children && node.children.length > 0) {
result = _.union(result, flatTree(node.children))
}
})
return result
}
const afterTreeInit = () => {
state.pWeightMap = treeParentWeight(state.originResourceTree, rootManage.value ? 9 : 0)
mounted.value = true
if (selectedNodeKey.value && returnMounted.value) {
expandedArray.value = getDefaultExpandedKeys()
returnMounted.value = false
}
nextTick(() => {
resourceListTree.value.setCurrentKey(selectedNodeKey.value)
nextTick(() => {
if (selectedNodeKey.value) {
const nodeDom = document.querySelector('.is-current')
nodeDom && nodeDom.click()
}
})
resourceListTree.value.filter(filterText.value)
})
}
const copyLoading = ref(false)
const openType = wsCache.get('open-backend') === '1' ? '_self' : '_blank'
const emit = defineEmits(['nodeClick'])
const operation = (cmd: string, data: BusiTreeNode, nodeType: string) => {
if (cmd === 'delete') {
const msg = data.leaf ? '' : t('visualization.delete_tips')
const tips_label = data.leaf ? resourceLabel : t('visualization.folder')
ElMessageBox.confirm(t('visualization.delete_warn', [tips_label]), {
confirmButtonType: 'danger',
type: 'warning',
tip: msg,
autofocus: false,
showClose: false
}).then(() => {
deleteLogic(data.id, curCanvasType.value).then(() => {
ElMessage.success(t('visualization.delete_success'))
getTree()
})
})
} else if (cmd === 'edit') {
resourceEdit(data.id)
} else if (cmd === 'copy') {
const targetPid = findParentIdByChildIdRecursive(state.resourceTree, data.id)
const params: ResourceOrFolder = {
nodeType: nodeType as 'folder' | 'leaf',
name: data.name + '-copy',
type: curCanvasType.value,
id: data.id,
pid: targetPid || '0'
}
copyLoading.value = true
copyResource(params)
.then(data => {
const baseUrl =
curCanvasType.value === 'dataV'
? `#/dvCanvas?opt=copy&pid=${params.pid}&dvId=${data.data}`
: `#/dashboard?opt=copy&pid=${params.pid}&resourceId=${data.data}`
if (isEmbedded.value) {
embeddedStore.clearState()
embeddedStore.setPid(params.pid as string)
embeddedStore.setOpt('copy')
if (curCanvasType.value === 'dataV') {
embeddedStore.setDvId(data.data)
} else {
embeddedStore.setResourceId(data.data)
}
useEmitt().emitter.emit(
'changeCurrentComponent',
curCanvasType.value === 'dataV' ? 'VisualizationEditor' : 'DashboardEditor'
)
return
}
const newWindow = window.open(baseUrl, openType)
initOpenHandler(newWindow)
})
.finally(() => {
copyLoading.value = false
})
} else {
resourceGroupOpt.value.optInit(nodeType, data, cmd, ['copy'].includes(cmd))
}
}
const addOperation = (
cmd: string,
data?: BusiTreeNode,
nodeType?: string,
parentSelect?: boolean
) => {
//
if (cmd === 'newLeaf') {
const baseUrl =
curCanvasType.value === 'dataV' ? '#/dvCanvas?opt=create' : '#/dashboard?opt=create'
let newWindow = null
if (isEmbedded.value) {
embeddedStore.clearState()
embeddedStore.setOpt('create')
if (data?.id) {
embeddedStore.setPid(data?.id as string)
}
useEmitt().emitter.emit(
'changeCurrentComponent',
curCanvasType.value === 'dataV' ? 'VisualizationEditor' : 'DashboardEditor'
)
return
}
if (data?.id) {
newWindow = window.open(baseUrl + `&pid=${data.id}`, openType)
} else {
newWindow = window.open(baseUrl, openType)
}
initOpenHandler(newWindow)
} else if (cmd === 'newFromTemplate') {
const params = {
curPosition: 'create',
pid: data?.id,
templateType: curCanvasType.value === 'dataV' ? 'SCREEN' : 'PANEL'
}
resourceCreateOpt.value.optInit(params)
} else {
resourceGroupOpt.value.optInit(nodeType, data || {}, cmd, parentSelect)
}
}
function createNewObject() {
return addOperation('newLeaf', null, 'leaf', true)
}
const resourceEdit = resourceId => {
const baseUrl = curCanvasType.value === 'dataV' ? '#/dvCanvas?dvId=' : '#/dashboard?resourceId='
if (isEmbedded.value) {
embeddedStore.clearState()
if (curCanvasType.value === 'dataV') {
embeddedStore.setDvId(resourceId)
} else {
embeddedStore.setResourceId(resourceId)
}
useEmitt().emitter.emit(
'changeCurrentComponent',
curCanvasType.value === 'dataV' ? 'VisualizationEditor' : 'DashboardEditor'
)
return
}
const newWindow = window.open(baseUrl + resourceId, openType)
initOpenHandler(newWindow)
}
const resourceOptFinish = () => {
getTree()
}
const resourceCreateFinish = templateData => {
// do create
wsCache.set(`de-template-data`, JSON.stringify(templateData))
const baseUrl =
curCanvasType.value === 'dataV'
? '#/dvCanvas?opt=create&createType=template'
: '#/dashboard?opt=create&createType=template'
let newWindow = null
if (isEmbedded.value) {
embeddedStore.clearState()
embeddedStore.setOpt('create')
embeddedStore.setCreateType('template')
if (state.templateCreatePid) {
embeddedStore.setPid(state.templateCreatePid as unknown as string)
}
useEmitt().emitter.emit(
'changeCurrentComponent',
curCanvasType.value === 'dataV' ? 'VisualizationEditor' : 'DashboardEditor'
)
return
}
if (state.templateCreatePid) {
newWindow = window.open(baseUrl + `&pid=${state.templateCreatePid}`, openType)
} else {
newWindow = window.open(baseUrl, openType)
}
initOpenHandler(newWindow)
}
const getParentKeys = (tree, targetKey, parentKeys = []) => {
for (const node of tree) {
if (node.id === targetKey) {
return parentKeys
}
if (node.children) {
const newParentKeys = [...parentKeys, node.id]
const result = getParentKeys(node.children, targetKey, newParentKeys)
if (result) {
return result
}
}
}
return null
}
const getDefaultExpandedKeys = () => {
const parentKeys = getParentKeys(state.resourceTree, selectedNodeKey.value)
if (parentKeys) {
return [selectedNodeKey.value, ...parentKeys]
} else {
return []
}
}
const sortList = [
{
name: t('visualization.time_asc'),
value: 'time_asc'
},
{
name: t('visualization.time_desc'),
value: 'time_desc',
divided: true
},
{
name: t('visualization.name_asc'),
value: 'name_asc'
},
{
name: t('visualization.name_desc'),
value: 'name_desc'
}
]
const sortTypeTip = computed(() => {
return sortList.find(ele => ele.value === state.curSortType).name
})
const handleSortTypeChange = sortType => {
state.resourceTree = treeSort(state.originResourceTree, sortType)
state.curSortType = sortType
wsCache.set('TreeSort-' + curCanvasType.value, state.curSortType)
}
const sortTypeChange = sortType => {
state.resourceTree = treeSort(state.originResourceTree, sortType)
state.curSortType = sortType
}
const proxyAllowDrop = debounce((arg1, arg2) => {
const flagArray = ['dashboard', 'dataV', 'dataset', 'datasource']
const flag = flagArray.findIndex(item => item === curCanvasType.value)
if (flag < 0 || !isFreeFolder(arg2, flag + 1)) {
return allowDrop(arg1, arg2)
}
ElMessage.warning(t('free.save_error'))
return false
}, 300)
watch(filterText, val => {
resourceListTree.value.filter(val)
})
const openHandler = ref(null)
const initOpenHandler = newWindow => {
if (openHandler?.value) {
const pm = {
methodName: 'initOpenHandler',
args: newWindow
}
openHandler.value.invokeMethod(pm)
}
}
const loadInit = () => {
const historyTreeSort = wsCache.get('TreeSort-' + curCanvasType.value)
if (historyTreeSort) {
state.curSortType = historyTreeSort
}
}
const loadShareBase = () => {
queryShareBaseApi().then(res => {
const param = {
shareDisable: res.data?.disable,
sharePeRequire: res.data?.peRequire
}
shareStore.setData(param)
})
}
onMounted(() => {
loadInit()
getTree()
loadShareBase()
})
defineExpose({
rootManage,
hasData,
createNewObject,
mounted
})
</script>
<template>
<div class="resource-tree">
<div class="tree-header">
<div class="icon-methods" v-show="showPosition === 'preview'">
<span class="title"> {{ resourceLabel }} </span>
<div v-if="rootManage" class="flex-align-center">
<el-tooltip :content="t('work_branch.new_folder')" placement="top" effect="dark">
<el-icon
class="custom-icon btn"
style="margin-right: 20px"
@click="addOperation('newFolder', null, 'folder')"
>
<Icon name="dv-new-folder"><dvNewFolder class="svg-icon" /></Icon>
</el-icon>
</el-tooltip>
<el-tooltip :content="newResourceLabel" placement="top" effect="dark">
<el-dropdown popper-class="menu-outer-dv_popper" trigger="hover">
<el-icon class="custom-icon btn" @click="addOperation('newLeaf', null, 'leaf', true)">
<Icon name="icon_file-add_outlined"
><icon_fileAdd_outlined class="svg-icon"
/></Icon>
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="addOperation('newLeaf', null, 'leaf', true)">
<el-icon :class="`handle-icon color-${curCanvasType}`">
<Icon><component class="svg-icon" :is="dvSvgType"></component></Icon>
</el-icon>
{{ t('work_branch.new_empty') }}
</el-dropdown-item>
<el-dropdown-item @click="addOperation('newFromTemplate', null, 'leaf', true)">
<el-icon class="handle-icon">
<Icon name="dv-use-template"><dvUseTemplate class="svg-icon" /></Icon>
</el-icon>
{{ t('work_branch.new_using_template') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</div>
</div>
<el-input
:placeholder="t('commons.search')"
v-model="filterText"
clearable
class="search-bar"
>
<template #prefix>
<el-icon>
<Icon name="icon_search-outline_outlined"
><icon_searchOutline_outlined class="svg-icon"
/></Icon>
</el-icon>
</template>
</el-input>
<el-dropdown @command="handleSortTypeChange" trigger="click">
<el-icon class="filter-icon-span">
<el-tooltip :offset="16" effect="dark" :content="sortTypeTip" placement="top">
<Icon v-if="state.curSortType.includes('asc')" name="dv-sort-asc" class="opt-icon"
><dvSortAsc class="svg-icon opt-icon"
/></Icon>
</el-tooltip>
<el-tooltip :offset="16" effect="dark" :content="sortTypeTip" placement="top">
<Icon v-if="state.curSortType.includes('desc')" name="dv-sort-desc" class="opt-icon"
><dvSortDesc class="svg-icon opt-icon"
/></Icon>
</el-tooltip>
</el-icon>
<template #dropdown>
<el-dropdown-menu style="width: 246px">
<template :key="ele.value" v-for="ele in sortList">
<el-dropdown-item
class="ed-select-dropdown__item"
:class="ele.value === state.curSortType && 'selected'"
:command="ele.value"
>
{{ ele.name }}
</el-dropdown-item>
<li v-if="ele.divided" class="ed-dropdown-menu__item--divided"></li>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-scrollbar class="custom-tree" v-loading="copyLoading">
<el-tree
menu
ref="resourceListTree"
:default-expanded-keys="expandedArray"
:data="state.resourceTree"
:props="defaultProps"
node-key="id"
highlight-current
:expand-on-click-node="true"
:filter-node-method="filterNode"
@node-expand="nodeExpand"
@node-collapse="nodeCollapse"
@node-click="nodeClick"
@node-drag-start="handleDragStart"
:allow-drop="proxyAllowDrop"
@node-drop="handleDrop"
draggable
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<el-icon style="font-size: 18px" v-if="!data.leaf">
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<el-icon style="font-size: 18px" v-else-if="curCanvasType === 'dashboard'">
<Icon
><component
:is="data.extraFlag ? dvDashboardSpineMobile : dvDashboardSpine"
></component
></Icon>
</el-icon>
<el-icon class="icon-screen-new color-dataV" style="font-size: 18px" v-else>
<Icon name="icon_operation-analysis_outlined"
><icon_operationAnalysis_outlined class="svg-icon"
/></Icon>
</el-icon>
<span :title="node.label" class="label-tooltip">{{ node.label }}</span>
<div class="icon-more" v-if="data.weight >= 7 && showPosition === 'preview'">
<el-icon
v-on:click.stop
v-if="data.leaf"
class="hover-icon"
@click="resourceEdit(data.id)"
>
<Icon><icon_edit_outlined class="svg-icon" /></Icon>
</el-icon>
<handle-more
@handle-command="
cmd => addOperation(cmd, data, cmd === 'newFolder' ? 'folder' : 'leaf')
"
:menu-list="resourceTypeList"
:icon-name="icon_add_outlined"
placement="bottom-start"
v-if="!data.leaf"
></handle-more>
<dv-handle-more
@handle-command="cmd => operation(cmd, data, data.leaf ? 'leaf' : 'folder')"
:node="data"
:any-manage="anyManage"
:resource-type="curCanvasType"
:menu-list="data.leaf ? menuListWeight(data.id) : state.folderMenuList"
></dv-handle-more>
</div>
</span>
</template>
</el-tree>
<de-resource-group-opt
:cur-canvas-type="curCanvasType"
@finish="resourceOptFinish"
ref="resourceGroupOpt"
/>
<de-resource-create-opt-v2
:cur-canvas-type="curCanvasType"
ref="resourceCreateOpt"
@finish="resourceCreateFinish"
></de-resource-create-opt-v2>
</el-scrollbar>
</div>
<XpackComponent ref="openHandler" jsname="L2NvbXBvbmVudC9lbWJlZGRlZC1pZnJhbWUvT3BlbkhhbmRsZXI=" />
</template>
<style lang="less" scoped>
.filter-icon-span {
border: 1px solid #bbbfc4;
width: 32px;
height: 32px;
border-radius: 4px;
color: #1f2329;
padding: 8px;
margin-left: 8px;
font-size: 16px;
cursor: pointer;
.opt-icon:focus {
outline: none !important;
}
&:hover {
background: #f5f6f7;
}
&:active {
background: #eff0f1;
}
}
.resource-tree {
padding: 16px 0 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.tree-header {
padding: 0 16px;
}
.icon-methods {
display: flex;
align-items: center;
justify-content: flex-end;
font-size: 20px;
font-weight: 500;
color: var(--TextPrimary, #1f2329);
padding-bottom: 16px;
.title {
margin-right: auto;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
}
.custom-icon {
font-size: 20px;
&.btn {
color: var(--ed-color-primary);
}
&:hover {
cursor: pointer;
}
}
}
.search-bar {
padding-bottom: 10px;
width: calc(100% - 40px);
}
}
.title-area {
margin-left: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.title-area-outer {
display: flex;
flex: 1 1 0%;
width: 0px;
}
.custom-tree-node-list {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding: 0 8px;
}
.father .child {
visibility: hidden;
}
.father:hover .child {
visibility: visible;
}
:deep(.ed-input__wrapper) {
width: 80px;
}
.custom-tree {
height: calc(100vh - 148px);
padding: 0 8px;
}
.custom-tree-node {
width: calc(100% - 30px);
display: flex;
align-items: center;
box-sizing: content-box;
padding-right: 4px;
.label-tooltip {
width: 100%;
margin-left: 8.75px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.icon-more {
margin-left: auto;
display: none;
}
&:hover {
.label-tooltip {
width: calc(100% - 78px);
}
.icon-more {
display: inline-flex;
}
}
.icon-screen-new {
border-radius: 4px;
color: #fff;
padding: 3px;
}
}
</style>
<style lang="less">
.menu-outer-dv_popper {
width: 140px;
margin-top: -2px !important;
.ed-icon {
border-radius: 4px;
}
}
.sort-type-normal {
i {
display: none;
}
}
.sort-type-checked {
color: var(--ed-color-primary);
i {
display: block;
}
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<el-col>
<el-row style="display: inherit; margin-top: 5px">
<el-row>
<el-input
v-model="state.templateFilterText"
:placeholder="t('visualization.filter_keywords')"
size="small"
clearable
prefix-icon="el-icon-search"
/>
</el-row>
<el-row style="display: inherit; margin-top: 5px">
<el-tree
ref="templateTree"
:default-expanded-keys="state.defaultExpandedKeys"
:data="templateList"
node-key="id"
:expand-on-click-node="true"
:filter-node-method="filterNode"
:highlight-current="true"
@node-click="nodeClick"
>
<template #default="{ data }">
<span class="custom-tree-node">
<span class="custom-label">
<el-icon style="font-size: 18px" v-if="data.nodeType === 'folder'">
<Icon name="dv-folder"><dvFolder class="svg-icon" /></Icon>
</el-icon>
<el-icon style="font-size: 18px" v-else-if="data.dvType === 'dashboard'">
<Icon name="dv-dashboard-spine"><dvDashboardSpine class="svg-icon" /></Icon>
</el-icon>
<el-icon class="icon-screen-new" style="font-size: 18px" v-else>
<Icon name="icon_operation-analysis_outlined"
><icon_operationAnalysis_outlined class="svg-icon"
/></Icon>
</el-icon>
<span :title="data.name" class="custom-name">{{ data.name }}</span>
</span>
</span>
</template>
</el-tree>
</el-row>
</el-row>
</el-col>
</template>
<script setup lang="ts">
import dvFolder from '@/assets/svg/dv-folder.svg'
import dvDashboardSpine from '@/assets/svg/dv-dashboard-spine.svg'
import icon_operationAnalysis_outlined from '@/assets/svg/icon_operation-analysis_outlined.svg'
import { findOne } from '@/api/template'
import { useI18n } from '@/hooks/web/useI18n'
import { reactive } from 'vue'
const { t } = useI18n()
const emits = defineEmits(['showCurrentTemplateInfo'])
defineProps({
curCanvasType: {
type: String,
required: true
},
templateList: {
type: Array,
default: function () {
return []
}
}
})
const state = reactive({
templateFilterText: '',
defaultExpandedKeys: [],
currentTemplateShowList: []
})
const filterNode = (value, data) => {
if (!value) return true
return data.label.indexOf(value) !== -1
}
const nodeClick = data => {
if (data.nodeType === 'template') {
findOne(data.id).then(res => {
emits('showCurrentTemplateInfo', res.data)
})
}
}
</script>
<style scoped lang="less">
.custom-label {
display: flex;
flex: 1 1 0%;
width: 0px;
}
.custom-name {
margin-left: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<div class="info-card">
<div class="info-title">
{{
`${
dvInfo.type === 'dashboard'
? t('work_branch.dashboard')
: t('work_branch.big_data_screen')
}ID`
}}
</div>
<div class="info-content">{{ dvInfo.id }}</div>
<div v-if="dvInfo.creatorName" class="info-title">{{ t('visualization.create_by') }}</div>
<div v-if="dvInfo.creatorName" class="info-content">{{ dvInfo.creatorName }}</div>
<div class="info-title">{{ t('visualization.create_time') }}</div>
<div class="info-content">{{ timestampFormatDate(dvInfo.createTime) }}</div>
<div v-if="dvInfo.updateName" class="info-title">{{ t('visualization.update_by') }}</div>
<div v-if="dvInfo.updateName" class="info-content">{{ dvInfo.updateName }}</div>
<div class="info-title">{{ t('visualization.update_time') }}</div>
<div v-if="dvInfo.updateTime" class="info-content">
{{ timestampFormatDate(dvInfo.updateTime) }}
</div>
<div v-if="!dvInfo.updateTime" class="info-content">N/A</div>
</div>
</template>
<script lang="ts" setup>
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const dvMainStore = dvMainStoreWithOut()
const { dvInfo } = storeToRefs(dvMainStore)
const timestampFormatDate = value => {
if (!value) {
return '-'
}
return new Date(value).toLocaleString()
}
</script>
<style lang="less" scoped>
.info-card {
font-family: var(--de-custom_font, 'PingFang');
font-style: normal;
padding-left: 4px;
font-weight: 400;
line-height: 22px;
.info-title {
color: #646a73;
font-size: 14px;
margin-bottom: 4px;
}
.info-content {
color: #1f2329;
font-size: 14px;
margin-bottom: 12px;
}
:last-child {
margin-bottom: 0;
}
}
</style>

View File

@ -0,0 +1,159 @@
<template>
<el-drawer
direction="btt"
size="90%"
v-model="dialogShow"
trigger="click"
:title="t('visualization.multiplexing')"
custom-class="custom-drawer"
>
<dashboard-preview-show
v-if="dialogShow && curDvType === 'dashboard'"
ref="multiplexingPreviewShowRef"
class="multiplexing-area"
no-close
show-position="multiplexing"
></dashboard-preview-show>
<preview-show
v-if="dialogShow && curDvType === 'dataV'"
ref="multiplexingPreviewShowRef"
class="multiplexing-area"
no-close
show-position="multiplexing"
></preview-show>
<template #footer>
<el-row class="multiplexing-footer">
<el-col class="adapt-count">
<span>{{ t('visualization.multi_selected', [selectComponentCount]) }} </span>
</el-col>
<el-col class="adapt-select">
<span class="adapt-text">{{ t('visualization.component_style') }} </span>
<el-select
style="width: 120px"
v-model="multiplexingStyleAdapt"
placeholder="Select"
placement="top-start"
>
<el-option
v-for="item in state.copyOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-col>
<el-button class="close-button" @click="dialogShow = false">{{
t('visualization.close')
}}</el-button>
<el-button
type="primary"
:disabled="!selectComponentCount"
class="confirm-button"
@click="saveMultiplexing"
>{{ t('visualization.multiplexing') }}</el-button
>
</el-row>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { computed, reactive, ref, nextTick } from 'vue'
import DashboardPreviewShow from '@/views/dashboard/DashboardPreviewShow.vue'
import { copyStoreWithOut } from '@/store/modules/data-visualization/copy'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia'
import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot'
import PreviewShow from '@/views/data-visualization/PreviewShow.vue'
import { useI18n } from '@/hooks/web/useI18n'
const dvMainStore = dvMainStoreWithOut()
const snapshotStore = snapshotStoreWithOut()
const dialogShow = ref(false)
const copyStore = copyStoreWithOut()
const multiplexingPreviewShowRef = ref(null)
const { multiplexingStyleAdapt, curMultiplexingComponents } = storeToRefs(dvMainStore)
const curDvType = ref('dashboard')
const { t } = useI18n()
const selectComponentCount = computed(() => Object.keys(curMultiplexingComponents.value).length)
const state = reactive({
copyOptions: [
{ label: t('visualization.adapt_new_subject'), value: true },
{ label: t('visualization.keep_subject'), value: false }
]
})
const dialogInit = (dvType = 'dashboard') => {
curDvType.value = dvType
dialogShow.value = true
dvMainStore.initCurMultiplexingComponents()
}
const saveMultiplexing = () => {
dialogShow.value = false
const previewStateInfo = multiplexingPreviewShowRef.value.getPreviewStateInfo()
const canvasViewInfoPreview = previewStateInfo.canvasViewInfoPreview
nextTick(() => {
copyStore.copyMultiplexingComponents(canvasViewInfoPreview)
snapshotStore.recordSnapshotCache('saveMultiplexing')
})
}
defineExpose({
dialogInit
})
</script>
<style lang="less" scoped>
.close-button {
position: absolute;
top: 18px;
right: 120px;
}
.confirm-button {
position: absolute;
top: 18px;
right: 20px;
}
.multiplexing-area {
width: 100%;
height: 100%;
}
.multiplexing-footer {
position: relative;
}
.adapt-count {
position: absolute;
top: 18px;
left: 20px;
color: #646a73;
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
.adapt-select {
position: absolute;
top: 18px;
right: 220px;
}
.adapt-text {
font-size: 14px;
font-weight: 400;
color: #1f2329;
line-height: 22px;
}
</style>
<style lang="less">
.custom-drawer {
.ed-drawer__footer {
height: 64px !important;
padding: 0px !important;
box-shadow: 0 -1px 0px #d7d7d7 !important;
}
.ed-drawer__body {
padding: 0 0 64px 0 !important;
}
}
</style>

View File

@ -23,6 +23,8 @@ public class DatasetGroupInfoDTO extends DatasetNodeDTO {
private String sql; private String sql;
private String appId;
private Long total; private Long total;
private String creator; private String creator;