Compare commits
14 Commits
614a144f60
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5598ddf93c | ||
|
|
3017460cc7 | ||
|
|
4836a770c4 | ||
|
|
e6c89fad1b | ||
|
|
dba19b1e66 | ||
|
|
4e30bdb7cc | ||
|
|
4ac57cd140 | ||
|
|
c4d52ce47a | ||
|
|
54d0688571 | ||
|
|
66d5fd6ca4 | ||
|
|
25195b6360 | ||
|
|
e02ecf053f | ||
|
|
c86f2ad412 | ||
|
|
82fd97e06a |
32
README.md
@@ -51,42 +51,36 @@ http://go.mayfly.run
|
|||||||
|
|
||||||
#### 首页
|
#### 首页
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 机器操作
|
#### 资源管理
|
||||||
|
|
||||||
##### 状态查看
|

|
||||||
|
|
||||||

|
#### 资源操作
|
||||||
|
|
||||||
##### ssh 终端
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### 文件操作
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 数据库操作
|
|
||||||
|
|
||||||
##### sql 编辑器
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
##### 在线增删改查数据
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Redis 操作
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Mongo 操作
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
#### 工单流程审批
|
#### 工单流程审批
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
31
README_EN.md
@@ -46,40 +46,35 @@ account/password:test/test123.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Machine Operation
|
#### Resource Manage
|
||||||
|
|
||||||
##### Status
|

|
||||||
|
|
||||||

|
#### Resource Operation
|
||||||
|
|
||||||
##### SSH Terminal
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
##### File Operation
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Database Operation
|
|
||||||
|
|
||||||
##### SQL Editor
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
##### Add, delete, update and check data online
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Redis Operation
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### Mongo Operation
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
#### Work order process approval
|
#### Work order process approval
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -10,41 +10,41 @@
|
|||||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@logicflow/core": "^2.0.16",
|
"@logicflow/core": "^2.1.4",
|
||||||
"@logicflow/extension": "^2.0.21",
|
"@logicflow/extension": "^2.1.6",
|
||||||
"@vueuse/core": "^13.6.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.15.0",
|
||||||
"@xterm/addon-web-links": "^0.11.0",
|
"@xterm/addon-web-links": "^0.11.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"asciinema-player": "^3.10.0",
|
"asciinema-player": "^3.12.1",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.19",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"element-plus": "^2.10.5",
|
"element-plus": "^2.12.0",
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.8",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.5.4",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.55.1",
|
||||||
"monaco-sql-languages": "^0.15.1",
|
"monaco-sql-languages": "^0.15.1",
|
||||||
"monaco-themes": "^0.4.6",
|
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.4",
|
||||||
"qrcode.vue": "^3.6.0",
|
"qrcode.vue": "^3.6.0",
|
||||||
"screenfull": "^6.0.2",
|
"screenfull": "^6.0.2",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"sql-formatter": "^15.6.5",
|
"sql-formatter": "^15.6.10",
|
||||||
"trzsz": "^1.1.5",
|
"trzsz": "^1.1.5",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^13.0.0",
|
||||||
"vue": "^v3.6.0-alpha.2",
|
"vue": "^v3.6.0-alpha.4",
|
||||||
"vue-i18n": "^11.1.11",
|
"vue-i18n": "^11.2.2",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.6.3",
|
||||||
"vuedraggable": "^4.1.0"
|
"vuedraggable": "^4.1.0",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/node": "^22.13.14",
|
"@types/node": "^22.13.14",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
@@ -52,16 +52,16 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
"@typescript-eslint/eslint-plugin": "^8.35.0",
|
||||||
"@typescript-eslint/parser": "^8.35.0",
|
"@typescript-eslint/parser": "^8.35.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/compiler-sfc": "^3.5.18",
|
"@vue/compiler-sfc": "^3.5.22",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"code-inspector-plugin": "^1.0.4",
|
"code-inspector-plugin": "^1.0.4",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
"eslint-plugin-vue": "^10.4.0",
|
"eslint-plugin-vue": "^10.5.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"prettier": "^3.6.1",
|
"prettier": "^3.6.1",
|
||||||
"sass": "^1.89.2",
|
"sass": "^1.94.1",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.17",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"vite": "npm:rolldown-vite@latest",
|
||||||
"vite-plugin-progress": "0.0.7",
|
"vite-plugin-progress": "0.0.7",
|
||||||
"vue-eslint-parser": "^10.2.0"
|
"vue-eslint-parser": "^10.2.0"
|
||||||
|
|||||||
1
frontend/src/assets/icon/db/db.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1756305127175" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22356" width="48" height="48"><path d="M959.718832 123.963683C872.444401 50.185297 704.593576 0.299912 511.850044 0.299912S151.255687 50.185297 63.981255 123.963683C23.193205 158.453578 0 198.04198 0 240.22962v543.840672c0 132.461193 229.132871 239.929708 511.850044 239.929708s511.850044-107.468515 511.850044-239.929708v-543.840672c0-42.18764-23.193205-81.776042-63.981256-116.265937zM87.774285 189.64444c19.794201-21.893586 50.685151-43.087377 89.373816-61.182075 42.287611-19.794201 92.073025-35.489603 147.956653-46.586352C384.087474 70.17944 446.869081 64.281168 511.850044 64.281168s127.76257 5.898272 186.745289 17.594845c55.883628 11.096749 105.669042 26.792151 147.956654 46.586352 38.688665 18.094699 69.579615 39.28849 89.373816 61.182075 15.795372 17.494875 23.793029 34.489896 23.793029 50.48521 0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 273.319926 63.981255 256.324905 63.981255 240.22962s7.997657-33.090306 23.79303-50.58518zM63.981255 356.495558c87.274431 73.778385 255.125256 123.66377 447.868789 123.66377s360.594357-49.885385 447.868788-123.66377v155.254515c0 16.095285-7.997657 33.090306-23.793029 50.48521-19.794201 21.893586-50.685151 43.087377-89.373816 61.182075-42.287611 19.794201-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182075C71.978912 544.740408 63.981255 527.745387 63.981255 511.750073V356.495558z m895.737577 427.574734c0 16.095285-7.997657 33.090306-23.793029 50.485209-19.794201 21.893586-50.685151 43.087377-89.373816 61.182076-42.287611 19.894172-92.073025 35.489603-147.956654 46.586352-58.98272 11.696573-121.864298 17.594845-186.745289 17.594845s-127.76257-5.898272-186.74529-17.594845c-55.883628-11.096749-105.669042-26.792151-147.956653-46.586352-38.688665-18.094699-69.579615-39.28849-89.373816-61.182076C71.978912 817.160597 63.981255 800.165576 63.981255 784.070292V627.91604c87.274431 73.778385 255.125256 123.66377 447.868789 123.663771s360.594357-49.885385 447.868788-123.663771v156.154252z" p-id="22357"></path><path d="M167.950796 519.847701m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22358"></path><path d="M167.950796 791.768037m-39.988285 0a39.988285 39.988285 0 1 0 79.976569 0 39.988285 39.988285 0 1 0-79.976569 0Z" p-id="22359"></path></svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
1
frontend/src/assets/icon/db/table.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1756305474315" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24277" width="48" height="48"><path d="M960 0H0v1024h1024V0.146286h-64V0z m-640 960.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m320 256.146286h-256v-192h256v192z m0-256.146286h-256V512.146286h256v191.853714z m0-256h-256V256.146286H640v192h-256V256.146286h-64v192h-256V256.146286h896v191.853714z" p-id="24278"></path></svg>
|
||||||
|
After Width: | Height: | Size: 547 B |
1
frontend/src/assets/icon/docker/docker.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1756107672203" class="icon" viewBox="0 0 1472 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5144" width="48" height="48"><path d="M1449.66628 358.737a233.848 233.848 0 0 0-166.348-35.445 268.717 268.717 0 0 0-108.127-152.273l-31.158-20.026-22.265 30.455a258.736 258.736 0 0 0-22.01 265.39 177.353 177.353 0 0 1-74.28 21.241h-24.953V309.536H830.08228V0H624.44928v154.768H287.27328v154.704H118.68528V468.08H8.44728L3.26528 504.42a493.032 493.032 0 0 0 95.97 353.3c90.149 110.11 234.232 165.964 428.284 165.964a749.848 749.848 0 0 0 585.42-255.025 804.871 804.871 0 0 0 139.86-226.874c187.718-3.391 213.246-134.359 214.27-139.99l4.863-27.447-22.01-15.61z m-766.291-49.84v-92.068h87.717v92.068h-87.717z m-337.176 154.64v-92.068h87.59v92.068h-87.59z m168.588 0v-92.068h87.589v92.068h-87.589z m168.588 0v-92.068h87.717v92.068h-87.717z m170.38-92.068h87.524v92.068h-87.525v-92.068zM683.37428 62.125h87.717v92.003h-87.717V62.125zM514.78728 216.829h87.589v92.068h-87.525v-92.068z m-168.588 0h87.59v92.068h-87.59v-92.068zM177.61228 371.47h87.525v92.068H177.61228v-92.068zM527.19928 938.4a609.348 609.348 0 0 1-235-40.564 399.493 399.493 0 0 0 151.058-66.092 44.018 44.018 0 0 0 7.87-57.582 39.54 39.54 0 0 0-54.575-11.9 375.18 375.18 0 0 1-215.998 62.508 262.639 262.639 0 0 1-19.194-21.433 392.455 392.455 0 0 1-79.591-249.523h943.9a250.035 250.035 0 0 0 155.216-62.06l4.99-4.671a682.157 682.157 0 0 1-658.42 451.636z m699.432-482.412l-25.144-1.215-15.163-21.178a186.566 186.566 0 0 1-21.626-161.358 145.619 145.619 0 0 1 42.483 100.769l-1.663 60.525 54.83-18.682a205.505 205.505 0 0 1 111.07-1.664 170.123 170.123 0 0 1-144.787 42.803zM544.41028 629.31a69.738 69.738 0 1 1-66.412 69.674 68.139 68.139 0 0 1 66.412-69.674z m0 85.413a15.74 15.74 0 1 0-14.971-15.675 15.291 15.291 0 0 0 14.97 15.675z m0 0" p-id="5145"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
1
frontend/src/assets/icon/machine/machine.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1756286353957" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19008" width="48" height="48"><path d="M853.333333 554.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128v-170.666666a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 682.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 896h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 853.333333v-170.666666a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 725.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM853.333333 42.666667a128 128 0 0 1 128 128v170.666666a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128V170.666667a128 128 0 0 1 128-128h682.666666z m0 85.333333H170.666667a42.666667 42.666667 0 0 0-42.368 37.674667L128 170.666667v170.666666a42.666667 42.666667 0 0 0 37.674667 42.368L170.666667 384h682.666666a42.666667 42.666667 0 0 0 42.368-37.674667L896 341.333333V170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667zM256 213.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" p-id="19009"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M475.19999999 84.5568c202.7008 0 362.6496 71.0912 373.50400001 163.6608l0.40959999 4.5568h0.5632v232.2432H795.19999999V364.288c-63.1552 48.3328-175.5648 80.5888-307.5584 82.5088l-12.4416 0.0768c-133.1968 0-247.7312-30.8224-313.93279999-78.08L155.2 364.288v136.7552c0 63.5136 128.6144 126.208 319.99999999 126.208 63.1808 0 119.5264-6.8352 166.656-18.2784-4.9408 23.552-6.4 43.5968-4.4032 60.2112-48.7936 10.6752-103.7056 16.6144-162.2528 16.6144-133.1968 0-247.7312-30.7968-313.93279999-78.08l-6.0672-4.5056v125.824c0 63.5136 128.6144 126.2336 319.99999999 126.2336 74.3168 0 139.1616-9.4464 190.6688-24.7296l15.18080001 55.5008a631.04 631.04 0 0 1-89.6256 19.584 803.8656 803.8656 0 0 1-116.22400001 8.192c-206.7456 0-369.3312-73.984-374.3488-169.1392l-0.128-4.5568V252.7744h0.56320001C107.32799999 158.0032 269.1712 84.5824 475.19999999 84.5824z m335.18080001 637.696c12.3648 0 22.4 10.0608 22.39999999 22.4256l-0.0768 74.112a22.3744 22.3744 0 0 1 8.96-9.3184c15.4112-8.704 27.0336-24.6528 33.408-46.592a22.4 22.4 0 1 1 43.008 12.4928c-9.6 33.024-28.416 58.4704-54.39999999 73.1136a22.4 22.4 0 0 1-30.92480001-9.216v40.7296a22.4 22.4 0 0 1-44.79999999 0V744.704c0-12.3648 10.0608-22.4 22.4256-22.4z m-15.6672-184.7808a22.784 22.784 0 0 1 31.51359999 0.256c9.8816 9.8816 24.6528 26.624 40.06400001 47.36 25.3184 34.048 44.2624 68.4544 53.24799999 101.9136a22.4256 22.4256 0 0 1-43.3664 11.3408c-9.8816-36.6848-35.584-76.3392-65.8432-111.488-39.7824 46.1824-69.76 97.152-69.75999999 138.5984 0 36.992 13.056 67.4048 33.89439999 81.5616l5.632 5.3248a22.4 22.4 0 0 1-30.77119999 31.7696c-33.8432-22.9376-53.5552-67.3792-53.55520001-118.656 0-39.1424 18.1248-81.8944 48.2816-125.8752a461.312 461.312 0 0 1 50.688-62.1056zM475.19999999 143.0016c-187.7504 0.0512-314.7776 60.416-319.53919999 122.7264 4.8128 62.2336 131.7888 122.5984 319.53919999 122.5984s314.7776-60.3648 319.5392-122.6496C789.92639999 203.4176 662.95039999 143.0016 475.19999999 143.0016z" ></path></svg>
|
<svg t="1756389060526" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="29147" width="48" height="48"><path d="M465.454545 9.402182c245.697939 0 439.575273 86.171152 452.732122 198.376727l0.496485 5.523394h0.682666v281.506909H853.333333V348.470303c-76.551758 58.585212-212.805818 97.683394-372.79806 100.010667l-15.080728 0.093091c-161.450667 0-300.280242-37.360485-380.524606-94.642425L77.575758 348.470303v165.763879c0 76.986182 155.896242 152.979394 387.878787 152.979394 76.582788 0 144.880485-8.285091 202.007273-22.155637-5.988848 28.547879-7.757576 52.844606-5.337212 72.983273-59.143758 12.939636-125.703758 20.138667-196.670061 20.138667-161.450667 0-300.280242-37.329455-380.524606-94.642424l-7.354181-5.461334v152.51394c0 76.986182 155.896242 153.010424 387.878787 153.010424 90.08097 0 168.680727-11.450182 231.113697-29.975273l18.40097 67.273697a764.89697 764.89697 0 0 1-108.637091 23.738182 974.382545 974.382545 0 0 1-140.877576 9.929697c-250.600727 0-447.674182-89.677576-453.756121-205.017212l-0.155151-5.523394V213.302303h0.682666C19.549091 98.428121 215.722667 9.433212 465.454545 9.433212z m406.279758 772.964848c14.987636 0 27.151515 12.194909 27.151515 27.182546l-0.093091 89.832727a27.120485 27.120485 0 0 1 10.860606-11.29503c18.680242-10.550303 32.768-29.882182 40.494546-56.475152a27.151515 27.151515 0 1 1 52.130909 15.142788c-11.636364 40.029091-34.443636 70.873212-65.939394 88.622546a27.151515 27.151515 0 0 1-37.484606-11.17091v49.369213a27.151515 27.151515 0 0 1-54.30303 0V809.580606c0-14.987636 12.194909-27.151515 27.182545-27.151515z m-18.990545-223.976727a27.61697 27.61697 0 0 1 38.198303 0.310303c11.977697 11.977697 29.882182 32.271515 48.562424 57.406061 30.68897 41.270303 53.651394 82.97503 64.54303 123.531636a27.182545 27.182545 0 0 1-52.565333 13.746424c-11.977697-44.466424-43.132121-92.532364-79.80994-135.136969-48.221091 55.978667-84.557576 117.76-84.557575 167.99806 0 44.838788 15.825455 81.702788 41.084121 98.862546l6.826667 6.454303a27.151515 27.151515 0 0 1-37.298425 38.508606c-41.022061-27.803152-64.915394-81.671758-64.915394-143.825455 0-47.445333 21.969455-99.265939 58.523152-152.576a559.166061 559.166061 0 0 1 61.44-75.279515zM465.454545 80.244364C237.878303 80.306424 83.905939 153.475879 78.134303 229.003636 83.968 304.407273 237.878303 377.607758 465.454545 377.607758S847.003152 304.407273 852.774788 228.941576C846.941091 153.475879 693.030788 80.244364 465.454545 80.244364z" p-id="29148"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M897.8125003 599.75c-0.37500029 8.58750029-11.73750029 18.18749971-35.06250058 30.375-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.95000029-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.3125-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.7375-23.81249971-11.40000029-35.96249971-20.99999971-36.37499942-30.07500029v90.97499971c0 9.07499971 12.52500029 18.71250029 36.37499942 30.11250058 47.7 22.79999971 312.48749971 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.35000029 52.94999971-27.60000029 301.76250029-129.89999971 349.79999942-154.95000029 24.4125-12.7125 35.25000029-22.6125 35.25000029-31.57499971v-89.70000029l-0.18749971-0.07499971z" fill="" ></path><path d="M897.77500001 451.43749971c-0.37500029 8.58750029-11.73750029 18.15000029-35.02500029 30.33750058-47.99999971 25.01250029-296.84999971 127.35-349.79999942 154.94999942-52.9875 27.60000029-82.38750029 27.3375-124.23750029 7.35000029-41.85-19.98749971-306.60000029-126.97499971-354.30000029-149.77500029-23.81249971-11.3625-35.96249971-20.99999971-36.37499942-30.0375v90.97500058c0 9.07499971 12.52500029 18.675 36.37499942 30.07499942 47.7 22.79999971 312.45000029 129.75000029 354.30000029 149.7375 41.85 20.025 71.25000029 20.28750029 124.23750029-7.3125 52.94999971-27.60000029 301.76250029-129.9375 349.79999942-154.94999942 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125v-89.70000029l-0.225-0.03750029z" fill="" ></path><path d="M897.77500001 297.61250029c0.45-9.15000029-11.51250029-17.17499971-35.58750029-26.02500029-46.8-17.13750029-294.11250029-115.57500029-341.47499942-132.93749971-47.3625-17.325-66.63750029-16.61249971-122.25000058 3.375C342.7375003 161.93750029 79.41249972 265.24999971 32.5750003 283.55000029c-23.43750029 9.225-34.875 17.73749971-34.50000058 26.81249942V401.37499971c0 9.07499971 12.52500029 18.675 36.37500029 30.07500029 47.7 22.79999971 312.45000029 129.78749971 354.30000029 149.77500029 41.85 19.98749971 71.25000029 20.25 124.23749942-7.35000029 52.94999971-27.60000029 301.76250029-129.9375 349.80000029-154.95000029 24.4125-12.75000029 35.25000029-22.65000029 35.25000029-31.6125V297.61250029h-0.30000058zM320.31250001 383.75l208.53749971-32.02499971-63 92.3625-145.49999942-60.33750029z m461.25-83.17500029l-123.33750029 48.75000029-13.3875 5.24999971-123.26249971-48.74999942 136.575-54 123.37499971 48.74999942z m-362.09999971-89.36249942l-20.17500029-37.20000058 62.92500029 24.60000058 59.32499942-19.42500058-16.04999971 38.43750058 60.45000029 22.64999942-77.9625 8.1-17.47500029 42.00000029-28.19999971-46.83750029-90-8.1 67.1625-24.22499942z m-155.3625 52.49999971c61.57500029 0 111.44999971 19.31249971 111.44999971 43.16249971s-49.87500029 43.2-111.44999971 43.2-111.4875-19.38750029-111.4875-43.2c0-23.85 49.91249971-43.2 111.4875-43.2z" fill="" ></path></svg>
|
<svg t="1756388835244" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25729" width="48" height="48"><path d="M1023.786667 611.84c-0.426667 9.770667-13.354667 20.693333-39.893334 34.56-54.613333 28.458667-337.749333 144.896-397.994666 176.298667-60.288 31.402667-93.738667 31.104-141.354667 8.32-47.616-22.741333-348.842667-144.469333-403.114667-170.368-27.093333-12.970667-40.917333-23.893333-41.386666-34.218667v103.509333c0 10.325333 14.250667 21.290667 41.386666 34.261334 54.272 25.941333 355.541333 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.362667 60.245333-31.402667 343.338667-147.797333 397.994666-176.298667 27.776-14.464 40.106667-25.728 40.106667-35.925333v-102.058667l-0.213333-0.085333z m0-168.746667c-0.512 9.770667-13.397333 20.650667-39.893334 34.517334-54.613333 28.458667-337.749333 144.896-397.994666 176.298666-60.288 31.402667-93.738667 31.104-141.354667 8.362667-47.616-22.741333-348.842667-144.469333-403.114667-170.410667-27.093333-12.928-40.917333-23.893333-41.386666-34.176v103.509334c0 10.325333 14.250667 21.248 41.386666 34.218666 54.272 25.941333 355.498667 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.32 60.245333-31.402667 343.338667-147.84 397.994666-176.298666 27.776-14.506667 40.106667-25.770667 40.106667-35.968v-102.058667l-0.256-0.042667z m0-175.018666c0.469333-10.410667-13.141333-19.541333-40.533334-29.610667-53.248-19.498667-334.634667-131.498667-388.522666-151.253333-53.888-19.712-75.818667-18.901333-139.093334 3.84C392.234667 113.706667 92.629333 231.253333 39.338667 252.074667c-26.666667 10.496-39.68 20.181333-39.253334 30.506666V386.133333c0 10.325333 14.250667 21.248 41.386667 34.218667 54.272 25.941333 355.498667 147.669333 403.114667 170.410667 47.616 22.741333 81.066667 23.04 141.354666-8.362667 60.245333-31.402667 343.338667-147.84 397.994667-176.298667 27.776-14.506667 40.106667-25.770667 40.106667-35.968V268.074667h-0.341334zM366.677333 366.08l237.269334-36.437333-71.68 105.088-165.546667-68.650667z m524.8-94.634667l-140.330666 55.466667-15.232 5.973333-140.245334-55.466666 155.392-61.44 140.373334 55.466666z m-411.989333-101.674666l-22.954667-42.325334 71.594667 27.989334 67.498667-22.101334-18.261334 43.733334 68.778667 25.770666-88.704 9.216-19.882667 47.786667-32.085333-53.290667-102.4-9.216 76.416-27.562666z m-176.768 59.733333c70.058667 0 126.805333 21.973333 126.805333 49.109333s-56.746667 49.152-126.805333 49.152-126.848-22.058667-126.848-49.152c0-27.136 56.789333-49.152 126.848-49.152z" p-id="25730"></path></svg>
|
||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -16,12 +16,13 @@ export const LinkTypeEnum = {
|
|||||||
|
|
||||||
// 资源类型
|
// 资源类型
|
||||||
export const ResourceTypeEnum = {
|
export const ResourceTypeEnum = {
|
||||||
Machine: EnumValue.of(1, '机器').setExtra({ icon: 'Monitor', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
Machine: EnumValue.of(1, 'tag.machine').setExtra({ icon: 'icon machine/machine', iconColor: 'var(--el-color-primary)' }).tagTypeSuccess(),
|
||||||
Db: EnumValue.of(2, '数据库实例').setExtra({ icon: 'Coin', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
Db: EnumValue.of(2, 'tag.db').setExtra({ icon: 'icon db/db', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||||
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'icon redis/redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
Redis: EnumValue.of(3, 'redis').setExtra({ icon: 'icon redis/redis', iconColor: 'var(--el-color-danger)' }).tagTypeInfo(),
|
||||||
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
Mongo: EnumValue.of(4, 'mongo').setExtra({ icon: 'icon mongo/mongo', iconColor: 'var(--el-color-success)' }).tagTypeDanger(),
|
||||||
AuthCert: EnumValue.of(5, '授权凭证').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
AuthCert: EnumValue.of(5, 'ac.ac').setExtra({ icon: 'Ticket', iconColor: 'var(--el-color-success)' }),
|
||||||
Es: EnumValue.of(6, 'ES实例').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
Es: EnumValue.of(6, 'tag.es').setExtra({ icon: 'icon es/es-color', iconColor: 'var(--el-color-warning)' }).tagTypeWarning(),
|
||||||
|
Container: EnumValue.of(7, 'tag.container').setExtra({ icon: 'icon docker/docker', iconColor: 'var(--el-color-primary)' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标签关联的资源类型
|
// 标签关联的资源类型
|
||||||
@@ -35,8 +36,9 @@ export const TagResourceTypeEnum = {
|
|||||||
Redis: ResourceTypeEnum.Redis,
|
Redis: ResourceTypeEnum.Redis,
|
||||||
Mongo: ResourceTypeEnum.Mongo,
|
Mongo: ResourceTypeEnum.Mongo,
|
||||||
AuthCert: ResourceTypeEnum.AuthCert,
|
AuthCert: ResourceTypeEnum.AuthCert,
|
||||||
|
Container: ResourceTypeEnum.Container,
|
||||||
|
|
||||||
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'Coin' }),
|
Db: EnumValue.of(22, '数据库').setExtra({ icon: 'icon db/db' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 标签关联的资源类型路径
|
// 标签关联的资源类型路径
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const config = {
|
|||||||
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
|
||||||
|
|
||||||
// 系统版本
|
// 系统版本
|
||||||
version: 'v1.10.2',
|
version: 'v1.10.5',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出CSV文件
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param columns 列信息
|
||||||
|
* @param datas 数据
|
||||||
|
*/
|
||||||
export function exportCsv(filename: string, columns: string[], datas: []) {
|
export function exportCsv(filename: string, columns: string[], datas: []) {
|
||||||
// 二维数组
|
// 二维数组
|
||||||
const cvsData = [columns];
|
const cvsData = [columns];
|
||||||
@@ -30,6 +38,11 @@ export function exportCsv(filename: string, columns: string[], datas: []) {
|
|||||||
exportFile(`${filename}.csv`, csvString);
|
exportFile(`${filename}.csv`, csvString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出文件
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param content 文件内容
|
||||||
|
*/
|
||||||
export function exportFile(filename: string, content: string) {
|
export function exportFile(filename: string, content: string) {
|
||||||
// 导出
|
// 导出
|
||||||
let link = document.createElement('a');
|
let link = document.createElement('a');
|
||||||
@@ -44,3 +57,77 @@ export function exportFile(filename: string, content: string) {
|
|||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link); // 下载完成后移除元素
|
document.body.removeChild(link); // 下载完成后移除元素
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算字符串显示宽度(考虑中英文字符差异)
|
||||||
|
* @param str 要计算的字符串
|
||||||
|
* @returns 计算后的宽度值
|
||||||
|
*/
|
||||||
|
function getStringWidth(str: string): number {
|
||||||
|
if (!str) return 0;
|
||||||
|
|
||||||
|
// 统计中文字符数量(包括中文标点)
|
||||||
|
const chineseChars = str.match(/[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g);
|
||||||
|
const chineseCount = chineseChars ? chineseChars.length : 0;
|
||||||
|
|
||||||
|
// 英文字符数量
|
||||||
|
const englishCount = str.length - chineseCount;
|
||||||
|
|
||||||
|
// 中文字符按2个单位宽度计算,英文字符按1个单位宽度计算
|
||||||
|
return chineseCount * 2 + englishCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出Excel文件
|
||||||
|
* @param filename 文件名
|
||||||
|
* @param sheets 多个工作表数据,每个工作表包含名称、列信息和数据
|
||||||
|
* 示例: [{name: 'Sheet1', columns: ['列1', '列2'], datas: [{col1: '值1', col2: '值2'}]}]
|
||||||
|
*/
|
||||||
|
export function exportExcel(filename: string, sheets: { name: string; columns: string[]; datas: any[] }[]) {
|
||||||
|
// 创建工作簿
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
|
||||||
|
// 处理每个工作表
|
||||||
|
sheets.forEach((sheet) => {
|
||||||
|
// 准备表头
|
||||||
|
const headers: any = {};
|
||||||
|
sheet.columns.forEach((col) => {
|
||||||
|
headers[col] = col;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
|
const data = [headers, ...sheet.datas];
|
||||||
|
|
||||||
|
// 创建工作表
|
||||||
|
const ws = XLSX.utils.json_to_sheet(data, { skipHeader: true });
|
||||||
|
|
||||||
|
// 设置列宽自适应
|
||||||
|
const colWidths: { wch: number }[] = [];
|
||||||
|
sheet.columns.forEach((col, index) => {
|
||||||
|
// 计算列宽:取表头和前几行数据的最大宽度
|
||||||
|
let maxWidth = getStringWidth(col); // 表头宽度
|
||||||
|
const checkCount = Math.min(sheet.datas.length, 10); // 只检查前10行数据
|
||||||
|
|
||||||
|
for (let i = 0; i < checkCount; i++) {
|
||||||
|
const cellData = sheet.datas[i][col];
|
||||||
|
const cellStr = cellData ? String(cellData) : '';
|
||||||
|
const cellWidth = getStringWidth(cellStr);
|
||||||
|
if (cellWidth > maxWidth) {
|
||||||
|
maxWidth = cellWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置最小宽度为8,最大宽度为80
|
||||||
|
colWidths.push({ wch: Math.min(Math.max(maxWidth + 2, 8), 80) });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应用列宽设置
|
||||||
|
ws['!cols'] = colWidths;
|
||||||
|
|
||||||
|
// 添加工作表到工作簿
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, sheet.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 导出文件
|
||||||
|
XLSX.writeFile(wb, `${filename}.xlsx`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ import { ElMessage } from 'element-plus';
|
|||||||
export function templateResolve(template: string, param: any) {
|
export function templateResolve(template: string, param: any) {
|
||||||
return template.replace(/\{\w+\}/g, (word) => {
|
return template.replace(/\{\w+\}/g, (word) => {
|
||||||
const key = word.substring(1, word.length - 1);
|
const key = word.substring(1, word.length - 1);
|
||||||
const value = param[key];
|
let value;
|
||||||
|
// 兼容FormData类型的参数
|
||||||
|
if (param instanceof FormData) {
|
||||||
|
value = param.get(key);
|
||||||
|
} else {
|
||||||
|
value = param[key];
|
||||||
|
}
|
||||||
if (value != null || value != undefined) {
|
if (value != null || value != undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useUserInfo } from '@/store/userInfo';
|
|||||||
* @param code 权限code
|
* @param code 权限code
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function hasPerm(code: string) {
|
export function hasPerm(code: string): boolean {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export function hasPerm(code: string) {
|
|||||||
* @returns {"xxx:save": true} key->permission code
|
* @returns {"xxx:save": true} key->permission code
|
||||||
* @param permCodes
|
* @param permCodes
|
||||||
*/
|
*/
|
||||||
export function hasPerms(permCodes: any[]) {
|
export function hasPerms(permCodes: any[]): Record<string, boolean> {
|
||||||
const res = {} as { [key: string]: boolean };
|
const res = {} as { [key: string]: boolean };
|
||||||
for (let permCode of permCodes) {
|
for (let permCode of permCodes) {
|
||||||
if (hasPerm(permCode)) {
|
if (hasPerm(permCode)) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<transition @enter="onEnter" name="el-zoom-in-center">
|
<transition @enter="onEnter" name="el-zoom-in-center">
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
:aria-hidden="state.isShow ? 'false' : 'true'"
|
||||||
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
data-popper-placement="bottom"
|
data-popper-placement="bottom"
|
||||||
@@ -126,7 +126,7 @@ const onCurrentContextmenuClick = (ci: ContextmenuItem) => {
|
|||||||
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
emit('currentContextmenuClick', { id: ci.clickId, item: state.item });
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerContextmenuClick = (event: any, data: any) => {
|
const headerContextmenuClick = (event: any) => {
|
||||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: [Object, String, Number, null],
|
type: [Object, String, Number, null, Boolean],
|
||||||
required: true,
|
required: true,
|
||||||
default: () => null,
|
default: () => null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-tooltip :content="formatByteSize(fileDetail?.size)" placement="left">
|
<el-button v-if="loading" :loading="loading" name="loading" link type="primary" />
|
||||||
<el-link v-if="props.canDownload" target="_blank" rel="noopener noreferrer" icon="Download" type="primary" :href="getFileUrl(props.fileKey)"></el-link>
|
|
||||||
</el-tooltip>
|
|
||||||
|
|
||||||
{{ fileDetail?.filename }}
|
<template v-else>
|
||||||
|
<el-tooltip :content="fileSize" placement="left">
|
||||||
|
<el-link
|
||||||
|
v-if="props.canDownload"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
icon="Download"
|
||||||
|
type="primary"
|
||||||
|
:href="getFileUrl(props.fileKey)"
|
||||||
|
></el-link>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
{{ fileDetail?.filename }}
|
||||||
|
<!-- 文件大小显示 -->
|
||||||
|
<span v-if="props.showFileSize && fileDetail?.size" class="file-size">({{ fileSize }})</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, Ref, ref, watch } from 'vue';
|
||||||
import openApi from '@/common/openApi';
|
import openApi from '@/common/openApi';
|
||||||
import { getFileUrl } from '@/common/request';
|
import { getFileUrl } from '@/common/request';
|
||||||
import { formatByteSize } from '@/common/utils/format';
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fileKey: {
|
fileKey: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -23,8 +37,14 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showFileSize: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loading: Ref<boolean> = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
setFileInfo();
|
setFileInfo();
|
||||||
});
|
});
|
||||||
@@ -38,23 +58,38 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fileSize = computed(() => {
|
||||||
|
return fileDetail.value.size ? formatByteSize(fileDetail.value.size) : '';
|
||||||
|
});
|
||||||
|
|
||||||
const fileDetail: any = ref({});
|
const fileDetail: any = ref({});
|
||||||
|
|
||||||
const setFileInfo = async () => {
|
const setFileInfo = async () => {
|
||||||
if (!props.fileKey) {
|
try {
|
||||||
return;
|
if (!props.fileKey) {
|
||||||
}
|
return;
|
||||||
if (props.files && props.files.length > 0) {
|
}
|
||||||
const file: any = props.files.find((file: any) => {
|
loading.value = true;
|
||||||
return file.fileKey === props.fileKey;
|
if (props.files && props.files.length > 0) {
|
||||||
});
|
const file: any = props.files.find((file: any) => {
|
||||||
fileDetail.value = file;
|
return file.fileKey === props.fileKey;
|
||||||
return;
|
});
|
||||||
}
|
fileDetail.value = file;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const files = await openApi.getFileDetail([props.fileKey]);
|
const files = await openApi.getFileDetail([props.fileKey]);
|
||||||
fileDetail.value = files?.[0];
|
fileDetail.value = files?.[0];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss"></style>
|
<style lang="scss" scoped>
|
||||||
|
.file-size {
|
||||||
|
margin-left: 1px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form-item v-bind="$attrs">
|
<el-form-item v-bind="$attrs">
|
||||||
<template #label>
|
<template #label>
|
||||||
{{ props.label }}
|
<div class="flex items-center">
|
||||||
|
{{ props.label }}
|
||||||
|
|
||||||
<el-tooltip :placement="props.placement">
|
<el-tooltip :placement="props.placement">
|
||||||
<template #content>
|
<template #content>
|
||||||
<span v-html="props.tooltip"></span>
|
<span v-html="props.tooltip"></span>
|
||||||
</template>
|
</template>
|
||||||
<SvgIcon name="QuestionFilled" />
|
<SvgIcon name="QuestionFilled" class="ml-1" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
<!-- 遍历父组件传入的 solts 透传给子组件 -->
|
||||||
@@ -24,11 +26,11 @@ import { useSlots } from 'vue';
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
require: true,
|
required: true,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
type: String,
|
type: String,
|
||||||
require: true,
|
required: true,
|
||||||
},
|
},
|
||||||
placement: {
|
placement: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -34,15 +34,8 @@ import 'monaco-editor/esm/vs/editor/contrib/suggest/browser/suggestInlineComplet
|
|||||||
import { editor, languages } from 'monaco-editor';
|
import { editor, languages } from 'monaco-editor';
|
||||||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js?worker';
|
||||||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||||||
// 主题仓库 https://github.com/brijeshb42/monaco-themes
|
import SolarizedLight from './themes/Solarized-light.json';
|
||||||
// 主题例子 https://editor.bitwiser.in/
|
import SolarizedDark from './themes/Solarized-dark.json';
|
||||||
// import Monokai from 'monaco-themes/themes/Monokai.json'
|
|
||||||
// import Active4D from 'monaco-themes/themes/Active4D.json'
|
|
||||||
// import ahe from 'monaco-themes/themes/All Hallows Eve.json'
|
|
||||||
// import bop from 'monaco-themes/themes/Birds of Paradise.json'
|
|
||||||
// import krTheme from 'monaco-themes/themes/krTheme.json'
|
|
||||||
// import Dracula from 'monaco-themes/themes/Dracula.json'
|
|
||||||
import SolarizedLight from 'monaco-themes/themes/Solarized-light.json';
|
|
||||||
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
import { language as shellLan } from 'monaco-editor/esm/vs/basic-languages/shell/shell.js';
|
||||||
|
|
||||||
import { ElOption, ElSelect } from 'element-plus';
|
import { ElOption, ElSelect } from 'element-plus';
|
||||||
@@ -155,6 +148,7 @@ const defaultOptions = {
|
|||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
lineNumbers: 'on',
|
lineNumbers: 'on',
|
||||||
lineNumbersMinChars: 3,
|
lineNumbersMinChars: 3,
|
||||||
|
fixedOverflowWidgets: true, // 使弹出层不被容器限制
|
||||||
} as editor.IStandaloneEditorConstructionOptions;
|
} as editor.IStandaloneEditorConstructionOptions;
|
||||||
|
|
||||||
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
const monacoTextareaRef: Ref<any> = useTemplateRef('monacoTextareaRef');
|
||||||
@@ -225,6 +219,7 @@ const initMonacoEditorIns = () => {
|
|||||||
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
// options参数参考 https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html#language
|
||||||
// 初始化一些主题
|
// 初始化一些主题
|
||||||
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
monaco.editor.defineTheme('SolarizedLight', SolarizedLight);
|
||||||
|
monaco.editor.defineTheme('SolarizedDark', SolarizedDark);
|
||||||
defaultOptions.language = state.languageMode;
|
defaultOptions.language = state.languageMode;
|
||||||
defaultOptions.theme = themeConfig.value.editorTheme;
|
defaultOptions.theme = themeConfig.value.editorTheme;
|
||||||
let options = Object.assign(defaultOptions, props.options as any);
|
let options = Object.assign(defaultOptions, props.options as any);
|
||||||
|
|||||||
73
frontend/src/components/monaco/RealLogViewer.vue
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full">
|
||||||
|
<monaco-editor
|
||||||
|
ref="editorRef"
|
||||||
|
:height="props.height"
|
||||||
|
class="editor"
|
||||||
|
language="text"
|
||||||
|
v-model="modelValue"
|
||||||
|
:options="{
|
||||||
|
readOnly: true,
|
||||||
|
}"
|
||||||
|
:can-change-mode="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, useTemplateRef, watch } from 'vue';
|
||||||
|
import { useWebSocket } from '@vueuse/core';
|
||||||
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '100%',
|
||||||
|
},
|
||||||
|
wsUrl: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const websocketUrl = ref(props.wsUrl);
|
||||||
|
|
||||||
|
const { data } = useWebSocket(websocketUrl);
|
||||||
|
|
||||||
|
const editorRef: any = useTemplateRef('editorRef');
|
||||||
|
|
||||||
|
const modelValue = defineModel<string>('modelValue', {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(data, (value) => {
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
modelValue.value = modelValue.value + value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
||||||
|
setTimeout(() => {
|
||||||
|
revealLastLine();
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
const reload = (wsUrl: string) => {
|
||||||
|
modelValue.value = '';
|
||||||
|
websocketUrl.value = wsUrl;
|
||||||
|
revealLastLine();
|
||||||
|
};
|
||||||
|
|
||||||
|
const revealLastLine = () => {
|
||||||
|
const editor = editorRef.value.getEditor();
|
||||||
|
const lineCount = editor?.getModel().getLineCount();
|
||||||
|
editor.revealLine(lineCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
reload,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.editor {
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1086
frontend/src/components/monaco/themes/Solarized-dark.json
Normal file
1077
frontend/src/components/monaco/themes/Solarized-light.json
Normal file
@@ -146,7 +146,7 @@ const initSocket = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
socket = await createWebSocket(`${props.socketUrl}?rows=${term?.rows}&cols=${term?.cols}`);
|
socket = await createWebSocket(`${props.socketUrl}${props.socketUrl.includes('?') ? '&' : '?'}rows=${term?.rows}&cols=${term?.cols}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
|
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
|
||||||
state.status = TerminalStatus.Error;
|
state.status = TerminalStatus.Error;
|
||||||
|
|||||||
20
frontend/src/hooks/useDataState.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export function useDataState<KeyType, ValueType extends number | boolean | string>() {
|
||||||
|
const dataState = ref(new Map<KeyType, ValueType>());
|
||||||
|
|
||||||
|
const setState = (key: KeyType, value: ValueType) => {
|
||||||
|
dataState.value.set(key, value as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getState = (key: KeyType): ValueType => {
|
||||||
|
const result = dataState.value.get(key);
|
||||||
|
return result as ValueType;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataState,
|
||||||
|
setState,
|
||||||
|
getState,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -191,145 +191,6 @@ export default {
|
|||||||
btnTwo: 'Update now',
|
btnTwo: 'Update now',
|
||||||
btnTwoLoading: 'Updating',
|
btnTwoLoading: 'Updating',
|
||||||
},
|
},
|
||||||
menu: {
|
|
||||||
index: 'Home Page',
|
|
||||||
personalCenter: 'Personal Center',
|
|
||||||
|
|
||||||
tag: 'Tag',
|
|
||||||
tagTree: 'Tag Tree',
|
|
||||||
tagSave: 'Save Tag',
|
|
||||||
tagDelete: 'Delete Tag',
|
|
||||||
authorization: 'Authorization',
|
|
||||||
authorizationBase: 'Base Permission',
|
|
||||||
authorizationSave: 'Save Authorization',
|
|
||||||
authorizationDelete: 'Delete Authorization',
|
|
||||||
team: 'Team',
|
|
||||||
teamSave: 'Save Team',
|
|
||||||
teamDelete: 'Delete Team',
|
|
||||||
teamMemberAdd: 'Add Member',
|
|
||||||
teamMemberDelete: 'Delete Member',
|
|
||||||
teamTagSave: 'Save Team Tag',
|
|
||||||
|
|
||||||
machine: 'Machine',
|
|
||||||
machineOp: 'Machine Operation',
|
|
||||||
machineOpBase: 'Base Permission',
|
|
||||||
machineList: 'Machine List',
|
|
||||||
machineBase: 'Base Permission',
|
|
||||||
machineCreate: 'Create Machine',
|
|
||||||
machineEdit: 'Edit Machine',
|
|
||||||
machineDelete: 'Delete Machine',
|
|
||||||
machineTerminal: 'Machine Terminal',
|
|
||||||
machineFileConf: 'File',
|
|
||||||
machineFileConfCreate: 'File-Add Config',
|
|
||||||
machineFileConfDelete: 'File-Delete Config',
|
|
||||||
machineFileCreate: 'File-Create',
|
|
||||||
machineFileDelete: 'File-Delete',
|
|
||||||
machineFileWrite: 'File-Write',
|
|
||||||
machineFileUpload: 'File-Upload',
|
|
||||||
machineScript: 'Script',
|
|
||||||
machineScriptSave: 'Script-Save',
|
|
||||||
machineScriptDelete: 'Script-Delete',
|
|
||||||
machineScriptRun: 'Script-Run',
|
|
||||||
machineKillprocess: 'Kill Process',
|
|
||||||
machineCronJob: 'Cron Job',
|
|
||||||
machineCronJobSvae: 'Cron Job-Save',
|
|
||||||
machineCronJobDelete: 'Cron Job-Delete',
|
|
||||||
machineSecurityConfig: 'Security Config',
|
|
||||||
machineSecurityCmdSvae: 'Cmd Config-Save',
|
|
||||||
machineSecurityCmdDelete: 'Cmd Config-Delete',
|
|
||||||
|
|
||||||
dbms: 'DBMS',
|
|
||||||
dbDataOp: 'Data Operation',
|
|
||||||
dbDataOpBase: 'Base Permission',
|
|
||||||
dbDataOpSqlScriptRun: 'SQL Script Run',
|
|
||||||
dbInstance: 'DB Instance',
|
|
||||||
dbInstanceBase: 'Base Permission',
|
|
||||||
dbInstanceSave: 'Save Instance',
|
|
||||||
dbInstanceDelete: 'Delete Instance',
|
|
||||||
dbBase: 'Db Base Permission',
|
|
||||||
dbSave: 'Save Db',
|
|
||||||
dbDelete: 'Delete Db',
|
|
||||||
dbDataSync: 'Data Sync',
|
|
||||||
dbDataSyncBase: 'Base Permission',
|
|
||||||
dbDataSyncSave: 'Save Sync Task',
|
|
||||||
dbDataSyncDelete: 'Delete Sync Task',
|
|
||||||
dbDataSyncChangeStatus: 'Enable/Disable Sync Task',
|
|
||||||
dbDataSyncLog: 'Sync Log',
|
|
||||||
dbTransfer: 'DB Transfer',
|
|
||||||
dbTransferBase: 'Base Permission',
|
|
||||||
dbTransferSave: 'Save Transfer Task',
|
|
||||||
dbTransferDelete: 'Delete Transfer Task',
|
|
||||||
dbTransferChangeStatus: 'Enable/Disable Transfer Task',
|
|
||||||
dbTransferRun: 'Run Transfer Task',
|
|
||||||
dbTransferRunLog: 'Transfer Log',
|
|
||||||
dbTransferFileShow: 'ransfer File-Show',
|
|
||||||
dbTransferFileDelete: 'Transfer File-Delete',
|
|
||||||
dbTransferFileDownload: 'Transfer File-Download',
|
|
||||||
dbTransferFileRun: 'Transfer File-Run',
|
|
||||||
|
|
||||||
redis: 'Redis',
|
|
||||||
redisDataOp: 'Data Operation',
|
|
||||||
redisDataOpBase: 'Base Permission',
|
|
||||||
redisDataOpSave: 'Save Data',
|
|
||||||
redisDataOpDelete: 'Delete Data',
|
|
||||||
redisManage: 'Redis Manage',
|
|
||||||
redisManageBase: 'Base Permission',
|
|
||||||
|
|
||||||
mongo: 'Mongo',
|
|
||||||
mongoDataOp: 'Data Operation',
|
|
||||||
mongoDataOpBase: 'Base Permission',
|
|
||||||
mongoDataOpSave: 'Save Data',
|
|
||||||
mongoDataOpDelete: 'Delete Data',
|
|
||||||
mongoManage: 'Mongo Manage',
|
|
||||||
mongoManageBase: 'Base Permission',
|
|
||||||
|
|
||||||
flow: 'Flow',
|
|
||||||
myTask: 'My Task',
|
|
||||||
myFlow: 'My Flow',
|
|
||||||
flowProcDef: 'Process Define',
|
|
||||||
flowProcDefSave: 'Save Process Define',
|
|
||||||
flowProcDefDelete: 'Delete Process Define',
|
|
||||||
|
|
||||||
msgManage: 'Message',
|
|
||||||
channel: 'Message Channel',
|
|
||||||
msgChannelBase: 'Base Permission',
|
|
||||||
saveMsgChannel: 'Save Message Channel',
|
|
||||||
delMsgChannel: 'Delete Message Channel',
|
|
||||||
msgTmpl: 'Message Template',
|
|
||||||
msgTmplBase: 'Base Permission',
|
|
||||||
saveMsgTmpl: 'Save Message Template',
|
|
||||||
delMsgTmpl: 'Delete Message Template',
|
|
||||||
sendMsg: 'Send Message',
|
|
||||||
|
|
||||||
system: 'System',
|
|
||||||
menuPermission: 'Menu & Permission',
|
|
||||||
menuPermissionBase: 'Base Permission',
|
|
||||||
menuPermissionAdd: 'Add Menu Permission',
|
|
||||||
menuPermissionEdit: 'Edit Menu Permission',
|
|
||||||
menuPermissionDelete: 'Delete Menu Permission',
|
|
||||||
menuPermissionEnableDisable: 'Enable/Disable Menu Permission',
|
|
||||||
account: 'Account',
|
|
||||||
accountBase: 'Base Permission',
|
|
||||||
accountAdd: 'Add Account',
|
|
||||||
accountEdit: 'Edit Account',
|
|
||||||
accountDelete: 'Delete Account',
|
|
||||||
accountEnableDisable: 'Enable/Disable Account',
|
|
||||||
accountRoleAllocation: 'Role Allocation',
|
|
||||||
role: 'Role',
|
|
||||||
roleBase: 'Base Permission',
|
|
||||||
roleAdd: 'Add Role',
|
|
||||||
roleEdit: 'Edit Role',
|
|
||||||
roleDelete: 'Delete Role',
|
|
||||||
roleMenuPermissionAllocation: 'Menu & Permission Allocation',
|
|
||||||
sysConf: 'System Config',
|
|
||||||
sysConfBase: 'Base Permission',
|
|
||||||
sysConfSave: 'Save System Config',
|
|
||||||
opLog: 'Operation Log',
|
|
||||||
opLogBase: 'Base Permission',
|
|
||||||
|
|
||||||
noPagePermission: 'No Page Permission',
|
|
||||||
authcertShowciphertext: 'Show Ciphertext',
|
|
||||||
},
|
|
||||||
home: {
|
home: {
|
||||||
personalInfo: 'Personal Information',
|
personalInfo: 'Personal Information',
|
||||||
welcomeMsg: `Hello, {name}, no matter how bad life gets, it doesn't prevent me from getting better!`,
|
welcomeMsg: `Hello, {name}, no matter how bad life gets, it doesn't prevent me from getting better!`,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { exportExcel } from '@/common/utils/export';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
db: {
|
db: {
|
||||||
// db instance
|
// db instance
|
||||||
@@ -65,7 +67,7 @@ export default {
|
|||||||
resultSet: 'Result Set',
|
resultSet: 'Result Set',
|
||||||
tableDataEmptyTextTips:
|
tableDataEmptyTextTips:
|
||||||
'tips: Single table query at the beginning of select * or click the default query data of the table name, double-click the data online modification',
|
'tips: Single table query at the beginning of select * or click the default query data of the table name, double-click the data online modification',
|
||||||
noSelctRunSqlMsg: 'Select the sql you want to execute',
|
noSelectRunSqlMsg: 'Select the sql you want to execute or move the cursor near the sql you want to execute',
|
||||||
enterExecRemarkTips: 'Please enter remark',
|
enterExecRemarkTips: 'Please enter remark',
|
||||||
execRemarkPlaceholder: 'Enter the remark to execute the sql',
|
execRemarkPlaceholder: 'Enter the remark to execute the sql',
|
||||||
currentSqlTabIsRunning: 'The current result set tab is being executed, please use the new TAB to execute',
|
currentSqlTabIsRunning: 'The current result set tab is being executed, please use the new TAB to execute',
|
||||||
@@ -99,6 +101,7 @@ export default {
|
|||||||
cancelFiexd: 'Cancel Fixed',
|
cancelFiexd: 'Cancel Fixed',
|
||||||
formView: 'Form View',
|
formView: 'Form View',
|
||||||
genJson: 'Generating JSON',
|
genJson: 'Generating JSON',
|
||||||
|
exportExcel: 'Export Excel',
|
||||||
exportCsv: 'Export CSV',
|
exportCsv: 'Export CSV',
|
||||||
exportSql: 'Export SQL',
|
exportSql: 'Export SQL',
|
||||||
onlySelectOneData: 'Only one row can be selected',
|
onlySelectOneData: 'Only one row can be selected',
|
||||||
|
|||||||
83
frontend/src/i18n/en/docker.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
export default {
|
||||||
|
docker: {
|
||||||
|
containerConf: 'Container Config',
|
||||||
|
addr: 'Address',
|
||||||
|
addrTips: 'eg: unix:///var/run/docker.sock 、tcp://192.168.1.1',
|
||||||
|
|
||||||
|
container: 'Container',
|
||||||
|
containerName: 'Container Name',
|
||||||
|
running: 'Running',
|
||||||
|
stopped: 'Stopped',
|
||||||
|
name: 'Container Name',
|
||||||
|
ip: 'IP Address',
|
||||||
|
status: 'Status',
|
||||||
|
stats: 'Stats',
|
||||||
|
memory: 'Memory',
|
||||||
|
stop: 'Stop',
|
||||||
|
stopContainerConfirm: 'Are you sure to stop container [{name}] ?',
|
||||||
|
removeContainerConfirm: 'Are you sure to remove container [{name}] ?',
|
||||||
|
restart: 'Restart',
|
||||||
|
createContainer: 'Create Container',
|
||||||
|
mount: 'Mount',
|
||||||
|
hostDir: 'Host Directory',
|
||||||
|
containerDir: 'Container Directory',
|
||||||
|
permission: 'Permission',
|
||||||
|
rw: 'RW',
|
||||||
|
ro: 'RO',
|
||||||
|
port: 'Port',
|
||||||
|
|
||||||
|
image: 'Image',
|
||||||
|
tag: 'Tag',
|
||||||
|
size: 'Size',
|
||||||
|
used: 'Used',
|
||||||
|
unUsed: 'UnUsed',
|
||||||
|
imageName: 'Image Name',
|
||||||
|
log: 'Log',
|
||||||
|
lines: 'Lines',
|
||||||
|
follow: 'Follow',
|
||||||
|
stopImageConfirm: 'Are you sure to stop image [{name}] ?',
|
||||||
|
export: 'Export',
|
||||||
|
imageUploading: 'Image uploading, please wait...',
|
||||||
|
imageTips: 'Support manual input and select',
|
||||||
|
forcePull: 'Force Pull Image',
|
||||||
|
hostPortPlaceholder: '80',
|
||||||
|
forcePullTips: 'Ignore the server existing image, pull again',
|
||||||
|
server: 'Server',
|
||||||
|
protocol: 'Protocol',
|
||||||
|
networkMode: 'Network Mode',
|
||||||
|
consoleTerminal: 'Console Terminal',
|
||||||
|
otherOption: 'Other Option',
|
||||||
|
tty: 'tty',
|
||||||
|
openStdin: 'stdin (-i)',
|
||||||
|
privileged: 'Privileged',
|
||||||
|
restartPolicy: 'Restart Policy',
|
||||||
|
noRestart: 'No Restart',
|
||||||
|
alwaysRestart: 'Always Restart',
|
||||||
|
onFailure: 'On Failure',
|
||||||
|
unlessStopped: 'Unless Stopped',
|
||||||
|
cpuShare: 'CPU Share',
|
||||||
|
cpuShareTips: 'The default container share is 1024 cpus, and increasing it will give the current container more CPU time',
|
||||||
|
cpuQuota: 'CPU Quota',
|
||||||
|
cpuLimitTips: 'A CPU limit of 0 turns off the limit',
|
||||||
|
cpuCanUseTips: 'The maximum available is {cpuTotal} cores',
|
||||||
|
core: 'Core',
|
||||||
|
memoryLimit: 'Memory Limit',
|
||||||
|
memoryLimitTips: 'A memory limit of 0 turns off the limit',
|
||||||
|
shmSize: 'Shm Size',
|
||||||
|
memoryCanUseTips: 'Maximum available {memTotal}',
|
||||||
|
tagTips: `One in a row, for example:
|
||||||
|
tag1=value1
|
||||||
|
tag2=value2`,
|
||||||
|
envParam: 'Env Param',
|
||||||
|
envParamTips: `One in a row, for example:
|
||||||
|
env1=value1
|
||||||
|
env2=value2`,
|
||||||
|
device: 'Device',
|
||||||
|
driver: 'Driver',
|
||||||
|
driverTips: 'Device drivers to be used by the container, e.g. : nvidia, etc',
|
||||||
|
count: 'Count',
|
||||||
|
capabilitie: 'Capabilitie',
|
||||||
|
deviceId: 'Device ID',
|
||||||
|
capabilitiePlaceholder: 'eg: gpu',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
es: {
|
es: {
|
||||||
keywordPlaceholder: 'host / name / code',
|
keywordPlaceholder: 'host / name / code',
|
||||||
|
protocol: 'Protocol',
|
||||||
port: 'Port',
|
port: 'Port',
|
||||||
size: 'size',
|
size: 'size',
|
||||||
docs: 'docs',
|
docs: 'docs',
|
||||||
@@ -16,11 +17,11 @@ export default {
|
|||||||
connSuccess: 'be connected successfully',
|
connSuccess: 'be connected successfully',
|
||||||
shouldTestConn: 'please test connection first',
|
shouldTestConn: 'please test connection first',
|
||||||
instance: 'ES Instance',
|
instance: 'ES Instance',
|
||||||
instanceSave: 'Save Instance',
|
instanceSave: 'ES-Save Instance',
|
||||||
instanceDel: 'Delete Instance',
|
instanceDel: 'Es-Delete Instance',
|
||||||
operation: 'Data Operation',
|
operation: 'Es-Data Operation',
|
||||||
dataSave: 'Data Save',
|
dataSave: 'Es-Data Save',
|
||||||
dataDel: 'Data Del',
|
dataDel: 'Es-Data Del',
|
||||||
indexName: 'Index Name',
|
indexName: 'Index Name',
|
||||||
requireIndexName: 'Index Name Is Required',
|
requireIndexName: 'Index Name Is Required',
|
||||||
indexDetail: 'Index Detail',
|
indexDetail: 'Index Detail',
|
||||||
|
|||||||
@@ -113,5 +113,9 @@ export default {
|
|||||||
taskBeginTime: 'Start Time',
|
taskBeginTime: 'Start Time',
|
||||||
flowAudit: 'Process Audit',
|
flowAudit: 'Process Audit',
|
||||||
notify: 'Notify',
|
notify: 'Notify',
|
||||||
|
|
||||||
|
aitask: 'AI Task',
|
||||||
|
aiAuditRule: 'Audit Rule',
|
||||||
|
aiAuditRuleTip: 'Please input the audit rule',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
144
frontend/src/i18n/en/menu.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
export default {
|
||||||
|
menu: {
|
||||||
|
index: 'Home',
|
||||||
|
personalCenter: 'Personal Center',
|
||||||
|
myResource: 'Resource',
|
||||||
|
|
||||||
|
tag: 'Resource',
|
||||||
|
tagTree: 'Resource Tree',
|
||||||
|
tagSave: 'Save Tag',
|
||||||
|
tagDelete: 'Delete Tag',
|
||||||
|
authorization: 'Authorization',
|
||||||
|
authorizationBase: 'Base Permission',
|
||||||
|
authorizationSave: 'Save Authorization',
|
||||||
|
authorizationDelete: 'Delete Authorization',
|
||||||
|
team: 'Team',
|
||||||
|
teamSave: 'Save Team',
|
||||||
|
teamDelete: 'Delete Team',
|
||||||
|
teamMemberAdd: 'Add Member',
|
||||||
|
teamMemberDelete: 'Delete Member',
|
||||||
|
teamTagSave: 'Save Team Tag',
|
||||||
|
|
||||||
|
machine: 'Machine',
|
||||||
|
machineOp: 'Machine Operation',
|
||||||
|
machineOpBase: 'Base Permission',
|
||||||
|
machineList: 'Machine List',
|
||||||
|
machineBase: 'Base Permission',
|
||||||
|
machineCreate: 'Create Machine',
|
||||||
|
machineEdit: 'Edit Machine',
|
||||||
|
machineDelete: 'Delete Machine',
|
||||||
|
machineTerminal: 'Machine Terminal',
|
||||||
|
machineFileConf: 'File',
|
||||||
|
machineFileConfCreate: 'File-Add Config',
|
||||||
|
machineFileConfDelete: 'File-Delete Config',
|
||||||
|
machineFileCreate: 'File-Create',
|
||||||
|
machineFileDelete: 'File-Delete',
|
||||||
|
machineFileWrite: 'File-Write',
|
||||||
|
machineFileUpload: 'File-Upload',
|
||||||
|
machineScript: 'Script',
|
||||||
|
machineScriptSave: 'Script-Save',
|
||||||
|
machineScriptDelete: 'Script-Delete',
|
||||||
|
machineScriptRun: 'Script-Run',
|
||||||
|
machineKillprocess: 'Kill Process',
|
||||||
|
machineCronJob: 'Cron Job',
|
||||||
|
machineCronJobSvae: 'Cron Job-Save',
|
||||||
|
machineCronJobDelete: 'Cron Job-Delete',
|
||||||
|
machineSecurityConfig: 'Security Config',
|
||||||
|
machineSecurityCmdSvae: 'Cmd Config-Save',
|
||||||
|
machineSecurityCmdDelete: 'Cmd Config-Delete',
|
||||||
|
|
||||||
|
dbms: 'DBMS',
|
||||||
|
dbDataOp: 'Data Operation',
|
||||||
|
dbDataOpBase: 'Base Permission',
|
||||||
|
dbDataOpSqlScriptRun: 'SQL Script Run',
|
||||||
|
dbInstance: 'DB Instance',
|
||||||
|
dbInstanceBase: 'Base Permission',
|
||||||
|
dbInstanceSave: 'Save Instance',
|
||||||
|
dbInstanceDelete: 'Delete Instance',
|
||||||
|
dbBase: 'Db Base Permission',
|
||||||
|
dbSave: 'Save Db',
|
||||||
|
dbDelete: 'Delete Db',
|
||||||
|
dbDataSync: 'Data Sync',
|
||||||
|
dbDataSyncBase: 'Base Permission',
|
||||||
|
dbDataSyncSave: 'Save Sync Task',
|
||||||
|
dbDataSyncDelete: 'Delete Sync Task',
|
||||||
|
dbDataSyncChangeStatus: 'Enable/Disable Sync Task',
|
||||||
|
dbDataSyncLog: 'Sync Log',
|
||||||
|
dbTransfer: 'DB Transfer',
|
||||||
|
dbTransferBase: 'Base Permission',
|
||||||
|
dbTransferSave: 'Save Transfer Task',
|
||||||
|
dbTransferDelete: 'Delete Transfer Task',
|
||||||
|
dbTransferChangeStatus: 'Enable/Disable Transfer Task',
|
||||||
|
dbTransferRun: 'Run Transfer Task',
|
||||||
|
dbTransferRunLog: 'Transfer Log',
|
||||||
|
dbTransferFileShow: 'ransfer File-Show',
|
||||||
|
dbTransferFileDelete: 'Transfer File-Delete',
|
||||||
|
dbTransferFileDownload: 'Transfer File-Download',
|
||||||
|
dbTransferFileRun: 'Transfer File-Run',
|
||||||
|
|
||||||
|
redis: 'Redis',
|
||||||
|
redisDataOp: 'Data Operation',
|
||||||
|
redisDataOpBase: 'Base Permission',
|
||||||
|
redisDataOpSave: 'Save Data',
|
||||||
|
redisDataOpDelete: 'Delete Data',
|
||||||
|
redisManage: 'Redis Manage',
|
||||||
|
redisManageBase: 'Base Permission',
|
||||||
|
|
||||||
|
mongo: 'Mongo',
|
||||||
|
mongoDataOp: 'Data Operation',
|
||||||
|
mongoDataOpBase: 'Base Permission',
|
||||||
|
mongoDataOpSave: 'Save Data',
|
||||||
|
mongoDataOpDelete: 'Delete Data',
|
||||||
|
mongoManage: 'Mongo Manage',
|
||||||
|
mongoManageBase: 'Base Permission',
|
||||||
|
|
||||||
|
containerManageBase: 'Container Manage - Base Permission',
|
||||||
|
|
||||||
|
flow: 'Flow',
|
||||||
|
myTask: 'My Task',
|
||||||
|
myFlow: 'My Flow',
|
||||||
|
flowProcDef: 'Process Define',
|
||||||
|
flowProcDefSave: 'Save Process Define',
|
||||||
|
flowProcDefDelete: 'Delete Process Define',
|
||||||
|
|
||||||
|
msgManage: 'Message',
|
||||||
|
channel: 'Message Channel',
|
||||||
|
msgChannelBase: 'Base Permission',
|
||||||
|
saveMsgChannel: 'Save Message Channel',
|
||||||
|
delMsgChannel: 'Delete Message Channel',
|
||||||
|
msgTmpl: 'Message Template',
|
||||||
|
msgTmplBase: 'Base Permission',
|
||||||
|
saveMsgTmpl: 'Save Message Template',
|
||||||
|
delMsgTmpl: 'Delete Message Template',
|
||||||
|
sendMsg: 'Send Message',
|
||||||
|
|
||||||
|
system: 'System',
|
||||||
|
menuPermission: 'Menu & Permission',
|
||||||
|
menuPermissionBase: 'Base Permission',
|
||||||
|
menuPermissionAdd: 'Add Menu Permission',
|
||||||
|
menuPermissionEdit: 'Edit Menu Permission',
|
||||||
|
menuPermissionDelete: 'Delete Menu Permission',
|
||||||
|
menuPermissionEnableDisable: 'Enable/Disable Menu Permission',
|
||||||
|
account: 'Account',
|
||||||
|
accountBase: 'Base Permission',
|
||||||
|
accountAdd: 'Add Account',
|
||||||
|
accountEdit: 'Edit Account',
|
||||||
|
accountDelete: 'Delete Account',
|
||||||
|
accountEnableDisable: 'Enable/Disable Account',
|
||||||
|
accountRoleAllocation: 'Role Allocation',
|
||||||
|
role: 'Role',
|
||||||
|
roleBase: 'Base Permission',
|
||||||
|
roleAdd: 'Add Role',
|
||||||
|
roleEdit: 'Edit Role',
|
||||||
|
roleDelete: 'Delete Role',
|
||||||
|
roleMenuPermissionAllocation: 'Menu & Permission Allocation',
|
||||||
|
sysConf: 'System Config',
|
||||||
|
sysConfBase: 'Base Permission',
|
||||||
|
sysConfSave: 'Save System Config',
|
||||||
|
opLog: 'Operation Log',
|
||||||
|
opLogBase: 'Base Permission',
|
||||||
|
|
||||||
|
noPagePermission: 'No Page Permission',
|
||||||
|
authcertShowciphertext: 'Show Ciphertext',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -190,6 +190,15 @@ export default {
|
|||||||
loginFailCountPlaceholder: 'Disable login after n failed login attempts',
|
loginFailCountPlaceholder: 'Disable login after n failed login attempts',
|
||||||
loginFainMin: 'Prohibited login time',
|
loginFainMin: 'Prohibited login time',
|
||||||
loginFailMinPlaceholder: 'After a specified number of login failures, re-login is prohibited within m minutes',
|
loginFailMinPlaceholder: 'After a specified number of login failures, re-login is prohibited within m minutes',
|
||||||
|
|
||||||
|
aiModelConf: 'AI Model Config',
|
||||||
|
aiModelType: 'Model Type',
|
||||||
|
aiModelTypePlaceholder: 'Please select a model type',
|
||||||
|
aiModel: 'Model',
|
||||||
|
aiModelPlaceholder: 'Please enter the model',
|
||||||
|
aiBaseUrl: 'Base URL',
|
||||||
|
aiBaseUrlPlaceholder: 'Please enter the model request URL',
|
||||||
|
aiApiKey: 'API Key',
|
||||||
},
|
},
|
||||||
syslog: {
|
syslog: {
|
||||||
operator: 'Operator',
|
operator: 'Operator',
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default {
|
|||||||
tagTips1: '1. Used to group assets',
|
tagTips1: '1. Used to group assets',
|
||||||
tagTips2: '2. Can be allocated in team management for resource isolation',
|
tagTips2: '2. Can be allocated in team management for resource isolation',
|
||||||
tagTips3: '3. Team members who own a parent tag have access to resources that manipulate their own or child tag associations',
|
tagTips3: '3. Team members who own a parent tag have access to resources that manipulate their own or child tag associations',
|
||||||
|
tagTips4: '4. Right-click nodes to edit or add child tags',
|
||||||
machine: 'Machine',
|
machine: 'Machine',
|
||||||
db: 'Db',
|
db: 'Db',
|
||||||
code: 'Code',
|
code: 'Code',
|
||||||
@@ -14,6 +15,12 @@ export default {
|
|||||||
createSubTagTitle: 'Creates a child tag for {codePath}',
|
createSubTagTitle: 'Creates a child tag for {codePath}',
|
||||||
rootTag: 'Root Tag',
|
rootTag: 'Root Tag',
|
||||||
selectTagPlaceholder: 'Select the associated tag',
|
selectTagPlaceholder: 'Select the associated tag',
|
||||||
|
machineOp: 'Machine Operation',
|
||||||
|
dbDataOp: 'Db Operation',
|
||||||
|
redisDataOp: 'Redis Operation',
|
||||||
|
esDataOp: 'Es Operation',
|
||||||
|
mongoDataOp: 'Mongo Operation',
|
||||||
|
allResource: 'All Resource',
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
team: 'Team',
|
team: 'Team',
|
||||||
|
|||||||
@@ -201,145 +201,6 @@ export default {
|
|||||||
btnTwo: '马上更新',
|
btnTwo: '马上更新',
|
||||||
btnTwoLoading: '更新中',
|
btnTwoLoading: '更新中',
|
||||||
},
|
},
|
||||||
menu: {
|
|
||||||
index: '首页',
|
|
||||||
personalCenter: '个人中心',
|
|
||||||
|
|
||||||
tag: '标签管理',
|
|
||||||
tagTree: '标签树',
|
|
||||||
tagSave: '保存标签',
|
|
||||||
tagDelete: '删除标签',
|
|
||||||
authorization: '授权凭证',
|
|
||||||
authorizationBase: '基础权限',
|
|
||||||
authorizationSave: '保存权限',
|
|
||||||
authorizationDelete: '删除权限',
|
|
||||||
team: '团队管理',
|
|
||||||
teamSave: '保存团队',
|
|
||||||
teamDelete: '删除团队',
|
|
||||||
teamMemberAdd: '添加成员',
|
|
||||||
teamMemberDelete: '删除成员',
|
|
||||||
teamTagSave: '保存团队标签',
|
|
||||||
|
|
||||||
machine: '机器管理',
|
|
||||||
machineOp: '机器操作',
|
|
||||||
machineOpBase: '基本权限',
|
|
||||||
machineList: '机器列表',
|
|
||||||
machineBase: '基本权限',
|
|
||||||
machineCreate: '创建机器',
|
|
||||||
machineEdit: '编辑机器',
|
|
||||||
machineDelete: '删除机器',
|
|
||||||
machineTerminal: '机器终端',
|
|
||||||
machineFileConf: '文件管理',
|
|
||||||
machineFileConfCreate: '文件-添加配置',
|
|
||||||
machineFileConfDelete: '文件-删除配置',
|
|
||||||
machineFileCreate: '文件-创建',
|
|
||||||
machineFileDelete: '文件-删除',
|
|
||||||
machineFileWrite: '文件-写入',
|
|
||||||
machineFileUpload: '文件-上传',
|
|
||||||
machineScript: '脚本管理',
|
|
||||||
machineScriptSave: '脚本-保存',
|
|
||||||
machineScriptDelete: '脚本-删除',
|
|
||||||
machineScriptRun: '脚本-执行',
|
|
||||||
machineKillprocess: '终止进程',
|
|
||||||
machineCronJob: '计划任务',
|
|
||||||
machineCronJobSvae: '计划任务-保存',
|
|
||||||
machineCronJobDelete: '计划任务-删除',
|
|
||||||
machineSecurityConfig: '安全配置',
|
|
||||||
machineSecurityCmdSvae: '命令配置-保存',
|
|
||||||
machineSecurityCmdDelete: '命令配置-删除',
|
|
||||||
|
|
||||||
dbms: 'DBMS',
|
|
||||||
dbDataOp: '数据操作',
|
|
||||||
dbDataOpBase: '基本权限',
|
|
||||||
dbDataOpSqlScriptRun: 'SQL脚本执行',
|
|
||||||
dbInstance: '数据库实例',
|
|
||||||
dbInstanceBase: '基本权限',
|
|
||||||
dbInstanceSave: '保存实例',
|
|
||||||
dbInstanceDelete: '删除实例',
|
|
||||||
dbBase: '数据库基本权限',
|
|
||||||
dbSave: '保存数据库',
|
|
||||||
dbDelete: '删除数据库',
|
|
||||||
dbDataSync: '数据同步',
|
|
||||||
dbDataSyncBase: '基本权限',
|
|
||||||
dbDataSyncSave: '保存同步',
|
|
||||||
dbDataSyncDelete: '删除同步',
|
|
||||||
dbDataSyncChangeStatus: '启用停用',
|
|
||||||
dbDataSyncLog: '同步日志',
|
|
||||||
dbTransfer: '数据库迁移',
|
|
||||||
dbTransferBase: '基本权限',
|
|
||||||
dbTransferSave: '保存迁移任务',
|
|
||||||
dbTransferDelete: '删除迁移任务',
|
|
||||||
dbTransferChangeStatus: '启用停用',
|
|
||||||
dbTransferRun: '执行迁移任务',
|
|
||||||
dbTransferRunLog: '迁移日志查看',
|
|
||||||
dbTransferFileShow: '迁移文件-查看',
|
|
||||||
dbTransferFileDelete: '迁移文件-删除',
|
|
||||||
dbTransferFileDownload: '迁移文件-下载',
|
|
||||||
dbTransferFileRun: '迁移文件-执行',
|
|
||||||
|
|
||||||
redis: 'Redis',
|
|
||||||
redisDataOp: '数据操作',
|
|
||||||
redisDataOpBase: '基本权限',
|
|
||||||
redisDataOpSave: '数据保存',
|
|
||||||
redisDataOpDelete: '数据删除',
|
|
||||||
redisManage: 'Redis管理',
|
|
||||||
redisManageBase: '基本权限',
|
|
||||||
|
|
||||||
mongo: 'Mongo',
|
|
||||||
mongoDataOp: '数据操作',
|
|
||||||
mongoDataOpBase: '基本权限',
|
|
||||||
mongoDataOpSave: '数据保存',
|
|
||||||
mongoDataOpDelete: '数据删除',
|
|
||||||
mongoManage: 'Mongo管理',
|
|
||||||
mongoManageBase: '基本权限',
|
|
||||||
|
|
||||||
flow: '工单流程',
|
|
||||||
myTask: '我的任务',
|
|
||||||
myFlow: '我的流程',
|
|
||||||
flowProcDef: '流程定义',
|
|
||||||
flowProcDefSave: '保存流程定义',
|
|
||||||
flowProcDefDelete: '删除流程定义',
|
|
||||||
|
|
||||||
msgManage: '消息管理',
|
|
||||||
channel: '消息渠道',
|
|
||||||
msgChannelBase: '基础权限',
|
|
||||||
saveMsgChannel: '保存消息渠道',
|
|
||||||
delMsgChannel: '删除消息渠道',
|
|
||||||
msgTmpl: '消息模板',
|
|
||||||
msgTmplBase: '基础权限',
|
|
||||||
saveMsgTmpl: '保存消息模板',
|
|
||||||
delMsgTmpl: '删除消息模板',
|
|
||||||
sendMsg: '发送消息',
|
|
||||||
|
|
||||||
system: '系统管理',
|
|
||||||
menuPermission: '菜单权限',
|
|
||||||
menuPermissionBase: '基本权限',
|
|
||||||
menuPermissionAdd: '添加菜单权限',
|
|
||||||
menuPermissionEdit: '编辑菜单权限',
|
|
||||||
menuPermissionDelete: '删除菜单权限',
|
|
||||||
menuPermissionEnableDisable: '启用/禁用菜单权限',
|
|
||||||
account: '账号管理',
|
|
||||||
accountBase: '基本权限',
|
|
||||||
accountAdd: '添加账号',
|
|
||||||
accountEdit: '编辑账号',
|
|
||||||
accountDelete: '删除账号',
|
|
||||||
accountEnableDisable: '启用/禁用账号',
|
|
||||||
accountRoleAllocation: '角色分配',
|
|
||||||
role: '角色管理',
|
|
||||||
roleBase: '基本权限',
|
|
||||||
roleAdd: '添加角色',
|
|
||||||
roleEdit: '编辑角色',
|
|
||||||
roleDelete: '删除角色',
|
|
||||||
roleMenuPermissionAllocation: '菜单权限分配',
|
|
||||||
sysConf: '系统配置',
|
|
||||||
sysConfBase: '基本权限',
|
|
||||||
sysConfSave: '保存配置',
|
|
||||||
opLog: '操作日志',
|
|
||||||
opLogBase: '基本权限',
|
|
||||||
|
|
||||||
noPagePermission: '无页面权限',
|
|
||||||
authcertShowciphertext: '授权凭证密文查看',
|
|
||||||
},
|
|
||||||
home: {
|
home: {
|
||||||
personalInfo: '个人信息',
|
personalInfo: '个人信息',
|
||||||
welcomeMsg: '您好, {name},生活变的再糟糕,也不妨碍我变得更好!',
|
welcomeMsg: '您好, {name},生活变的再糟糕,也不妨碍我变得更好!',
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default {
|
|||||||
times: '耗时',
|
times: '耗时',
|
||||||
resultSet: '结果集',
|
resultSet: '结果集',
|
||||||
tableDataEmptyTextTips: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
|
tableDataEmptyTextTips: 'tips: select *开头的单表查询或点击表名默认查询的数据,可双击数据在线修改',
|
||||||
noSelctRunSqlMsg: '请选中需要执行的sql',
|
noSelectRunSqlMsg: '请选中需要执行的sql或将光标移动到要执行sql附近',
|
||||||
enterExecRemarkTips: '请输入备注',
|
enterExecRemarkTips: '请输入备注',
|
||||||
execRemarkPlaceholder: '输入执行该sql的备注信息',
|
execRemarkPlaceholder: '输入执行该sql的备注信息',
|
||||||
currentSqlTabIsRunning: '当前结果集tab正在执行, 请使用新标签执行',
|
currentSqlTabIsRunning: '当前结果集tab正在执行, 请使用新标签执行',
|
||||||
@@ -98,6 +98,7 @@ export default {
|
|||||||
cancelFiexd: '取消固定',
|
cancelFiexd: '取消固定',
|
||||||
formView: '表单视图',
|
formView: '表单视图',
|
||||||
genJson: '生成JSON',
|
genJson: '生成JSON',
|
||||||
|
exportExcel: '导出Excel',
|
||||||
exportCsv: '导出CSV',
|
exportCsv: '导出CSV',
|
||||||
exportSql: '导出SQL',
|
exportSql: '导出SQL',
|
||||||
onlySelectOneData: '只能选择一行数据',
|
onlySelectOneData: '只能选择一行数据',
|
||||||
|
|||||||
83
frontend/src/i18n/zh-cn/docker.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
export default {
|
||||||
|
docker: {
|
||||||
|
containerConf: '容器配置',
|
||||||
|
addr: '地址',
|
||||||
|
addrTips: '如:unix:///var/run/docker.sock 、tcp://192.168.1.1',
|
||||||
|
|
||||||
|
container: '容器',
|
||||||
|
containerName: '容器名',
|
||||||
|
running: '运行中',
|
||||||
|
stopped: '已停止',
|
||||||
|
name: '容器名',
|
||||||
|
ip: 'IP地址',
|
||||||
|
status: '状态',
|
||||||
|
stats: '资源使用率',
|
||||||
|
memory: '内存',
|
||||||
|
stop: '停止',
|
||||||
|
stopContainerConfirm: '确定停止容器 [{name}] ?',
|
||||||
|
removeContainerConfirm: '确定删除容器 [{name}] ?',
|
||||||
|
restart: '重启',
|
||||||
|
createContainer: '创建容器',
|
||||||
|
mount: '挂载',
|
||||||
|
hostDir: '本机目录',
|
||||||
|
containerDir: '容器目录',
|
||||||
|
permission: '权限',
|
||||||
|
rw: '读写',
|
||||||
|
ro: '只读',
|
||||||
|
port: '端口',
|
||||||
|
|
||||||
|
image: '镜像',
|
||||||
|
tag: '标签',
|
||||||
|
size: '大小',
|
||||||
|
used: '已使用',
|
||||||
|
unUsed: '未使用',
|
||||||
|
imageName: '镜像名',
|
||||||
|
log: '日志',
|
||||||
|
lines: '行数',
|
||||||
|
follow: '实时',
|
||||||
|
stopImageConfirm: '确定删除该镜像?',
|
||||||
|
export: '导出',
|
||||||
|
imageUploading: '镜像导入中,请稍后...',
|
||||||
|
imageTips: '支持手动输入并选择',
|
||||||
|
forcePull: '强制拉取镜像',
|
||||||
|
hostPortPlaceholder: '80',
|
||||||
|
forcePullTips: '忽略服务器已存在的镜像,重新拉取一次',
|
||||||
|
server: '服务器',
|
||||||
|
protocol: '协议',
|
||||||
|
networkMode: '网络模式',
|
||||||
|
consoleTerminal: '控制台交互',
|
||||||
|
otherOption: '其他可选项',
|
||||||
|
tty: '伪终端 (-t)',
|
||||||
|
openStdin: '标准输入 (-i)',
|
||||||
|
privileged: '特权模式',
|
||||||
|
restartPolicy: '重启策略',
|
||||||
|
noRestart: '不重启',
|
||||||
|
alwaysRestart: '一直重启',
|
||||||
|
onFailure: '失败后重启',
|
||||||
|
unlessStopped: '未手动停止则重启',
|
||||||
|
cpuShare: 'CPU权重',
|
||||||
|
cpuShareTips: '容器默认份额为 1024 个 CPU,增大可使当前容器获得更多的 CPU 时间',
|
||||||
|
cpuQuota: 'CPU 限制',
|
||||||
|
cpuLimitTips: 'CPU限制为 0 则关闭限制',
|
||||||
|
cpuCanUseTips: '最大可用为{cpuTotal}核',
|
||||||
|
core: '核',
|
||||||
|
memoryLimit: '内存限制',
|
||||||
|
memoryLimitTips: '内存限制为 0 则关闭限制',
|
||||||
|
shmSize: '共享内存',
|
||||||
|
memoryCanUseTips: '最大可用为{memTotal}',
|
||||||
|
tagTips: `一行一个,例如:
|
||||||
|
tag1=value1
|
||||||
|
tag2=value2`,
|
||||||
|
envParam: '环境变量',
|
||||||
|
envParamTips: `一行一个,例如:
|
||||||
|
env1=value1
|
||||||
|
env2=value2`,
|
||||||
|
device: '设备',
|
||||||
|
driver: '驱动',
|
||||||
|
driverTips: '容器需要使用的设备驱动程序,如: nvidia 等',
|
||||||
|
count: '数量',
|
||||||
|
capabilitie: '能力',
|
||||||
|
deviceId: '设备ID',
|
||||||
|
capabilitiePlaceholder: '如: gpu',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
es: {
|
es: {
|
||||||
keywordPlaceholder: 'host / 名称 / 编号',
|
keywordPlaceholder: 'host / 名称 / 编号',
|
||||||
|
protocol: '协议',
|
||||||
port: '端口',
|
port: '端口',
|
||||||
size: '存储大小',
|
size: '存储大小',
|
||||||
docs: '文档数',
|
docs: '文档数',
|
||||||
@@ -16,11 +17,11 @@ export default {
|
|||||||
connSuccess: '连接成功',
|
connSuccess: '连接成功',
|
||||||
shouldTestConn: '请先测试连接可用性',
|
shouldTestConn: '请先测试连接可用性',
|
||||||
instance: 'ES实例',
|
instance: 'ES实例',
|
||||||
instanceSave: '实例保存',
|
instanceSave: 'Es-实例保存',
|
||||||
instanceDel: '实例删除',
|
instanceDel: 'Es-实例删除',
|
||||||
operation: '数据操作',
|
operation: 'Es-数据操作',
|
||||||
dataSave: '数据保存',
|
dataSave: 'Es-数据保存',
|
||||||
dataDel: '数据删除',
|
dataDel: 'Es-数据删除',
|
||||||
indexName: '索引名',
|
indexName: '索引名',
|
||||||
requireIndexName: '请填写索引名',
|
requireIndexName: '请填写索引名',
|
||||||
indexDetail: '索引详情',
|
indexDetail: '索引详情',
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default {
|
|||||||
waitProcess: '待处理',
|
waitProcess: '待处理',
|
||||||
pass: '通过',
|
pass: '通过',
|
||||||
reject: '拒绝',
|
reject: '拒绝',
|
||||||
back: '回退',
|
back: '退回',
|
||||||
canceled: '取消',
|
canceled: '取消',
|
||||||
// FlowBizType
|
// FlowBizType
|
||||||
dbSqlExec: 'DBMS-执行SQL',
|
dbSqlExec: 'DBMS-执行SQL',
|
||||||
@@ -113,5 +113,9 @@ export default {
|
|||||||
taskBeginTime: '开始时间',
|
taskBeginTime: '开始时间',
|
||||||
flowAudit: '流程审批',
|
flowAudit: '流程审批',
|
||||||
notify: '通知',
|
notify: '通知',
|
||||||
|
|
||||||
|
aitask: 'AI任务',
|
||||||
|
aiAuditRule: '审核规则',
|
||||||
|
aiAuditRuleTip: '请输入审核规则',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
144
frontend/src/i18n/zh-cn/menu.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
export default {
|
||||||
|
menu: {
|
||||||
|
index: '首页',
|
||||||
|
personalCenter: '个人中心',
|
||||||
|
myResource: '我的资源',
|
||||||
|
|
||||||
|
tag: '资源',
|
||||||
|
tagTree: '资源树',
|
||||||
|
tagSave: '保存标签',
|
||||||
|
tagDelete: '删除标签',
|
||||||
|
authorization: '授权凭证',
|
||||||
|
authorizationBase: '基础权限',
|
||||||
|
authorizationSave: '保存权限',
|
||||||
|
authorizationDelete: '删除权限',
|
||||||
|
team: '团队',
|
||||||
|
teamSave: '保存团队',
|
||||||
|
teamDelete: '删除团队',
|
||||||
|
teamMemberAdd: '添加成员',
|
||||||
|
teamMemberDelete: '删除成员',
|
||||||
|
teamTagSave: '保存团队标签',
|
||||||
|
|
||||||
|
machine: '机器',
|
||||||
|
machineOp: '机器操作',
|
||||||
|
machineOpBase: '机器操作-基本权限',
|
||||||
|
machineList: '机器列表',
|
||||||
|
machineBase: '机器-基本权限',
|
||||||
|
machineCreate: '机器-创建机器',
|
||||||
|
machineEdit: '机器-编辑机器',
|
||||||
|
machineDelete: '机器-删除机器',
|
||||||
|
machineTerminal: '机器-机器终端',
|
||||||
|
machineFileConf: '机器-文件管理',
|
||||||
|
machineFileConfCreate: '机器-文件-添加配置',
|
||||||
|
machineFileConfDelete: '机器-文件-删除配置',
|
||||||
|
machineFileCreate: '机器-文件-创建',
|
||||||
|
machineFileDelete: '机器-文件-删除',
|
||||||
|
machineFileWrite: '机器-文件-写入',
|
||||||
|
machineFileUpload: '机器-文件-上传',
|
||||||
|
machineScript: '机器-脚本管理',
|
||||||
|
machineScriptSave: '机器-脚本-保存',
|
||||||
|
machineScriptDelete: '机器-脚本-删除',
|
||||||
|
machineScriptRun: '机器-脚本-执行',
|
||||||
|
machineKillprocess: '机器-终止进程',
|
||||||
|
machineCronJob: '计划任务',
|
||||||
|
machineCronJobSvae: '机器-计划任务-保存',
|
||||||
|
machineCronJobDelete: '机器-计划任务-删除',
|
||||||
|
machineSecurityConfig: '安全配置',
|
||||||
|
machineSecurityCmdSvae: '机器-命令配置-保存',
|
||||||
|
machineSecurityCmdDelete: '机器-命令配置-删除',
|
||||||
|
|
||||||
|
dbms: 'DBMS',
|
||||||
|
dbDataOp: '数据操作',
|
||||||
|
dbDataOpBase: 'Db-数据操作-基本权限',
|
||||||
|
dbDataOpSqlScriptRun: 'Db-SQL脚本执行',
|
||||||
|
dbInstance: '数据库实例',
|
||||||
|
dbInstanceBase: 'Db-基本权限',
|
||||||
|
dbInstanceSave: 'Db-保存实例',
|
||||||
|
dbInstanceDelete: 'Db-删除实例',
|
||||||
|
dbBase: '数据库基本权限',
|
||||||
|
dbSave: 'Db-保存数据库',
|
||||||
|
dbDelete: 'Db-删除数据库',
|
||||||
|
dbDataSync: '数据同步',
|
||||||
|
dbDataSyncBase: '基本权限',
|
||||||
|
dbDataSyncSave: '保存同步',
|
||||||
|
dbDataSyncDelete: '删除同步',
|
||||||
|
dbDataSyncChangeStatus: '启用停用',
|
||||||
|
dbDataSyncLog: '同步日志',
|
||||||
|
dbTransfer: '数据库迁移',
|
||||||
|
dbTransferBase: '基本权限',
|
||||||
|
dbTransferSave: '保存迁移任务',
|
||||||
|
dbTransferDelete: '删除迁移任务',
|
||||||
|
dbTransferChangeStatus: '启用停用',
|
||||||
|
dbTransferRun: '执行迁移任务',
|
||||||
|
dbTransferRunLog: '迁移日志查看',
|
||||||
|
dbTransferFileShow: '迁移文件-查看',
|
||||||
|
dbTransferFileDelete: '迁移文件-删除',
|
||||||
|
dbTransferFileDownload: '迁移文件-下载',
|
||||||
|
dbTransferFileRun: '迁移文件-执行',
|
||||||
|
|
||||||
|
redis: 'Redis',
|
||||||
|
redisDataOp: 'Redis-数据操作',
|
||||||
|
redisDataOpBase: 'Redis-数据操作-基本权限',
|
||||||
|
redisDataOpSave: 'Redis-数据操作-数据保存',
|
||||||
|
redisDataOpDelete: 'Redis-数据操作-数据删除',
|
||||||
|
redisManage: 'Redis管理',
|
||||||
|
redisManageBase: 'Redis-管理-基本权限',
|
||||||
|
|
||||||
|
mongo: 'Mongo',
|
||||||
|
mongoDataOp: '数据操作',
|
||||||
|
mongoDataOpBase: 'Mongo-数据操作-基本权限',
|
||||||
|
mongoDataOpSave: 'Mongo-数据操作-数据保存',
|
||||||
|
mongoDataOpDelete: 'Mongo-数据操作-数据删除',
|
||||||
|
mongoManage: 'Mongo管理',
|
||||||
|
mongoManageBase: 'Mongo-管理-基本权限',
|
||||||
|
|
||||||
|
containerManageBase: '容器-管理-基本权限',
|
||||||
|
|
||||||
|
flow: '工单流程',
|
||||||
|
myTask: '我的任务',
|
||||||
|
myFlow: '我的流程',
|
||||||
|
flowProcDef: '流程定义',
|
||||||
|
flowProcDefSave: '保存流程定义',
|
||||||
|
flowProcDefDelete: '删除流程定义',
|
||||||
|
|
||||||
|
msgManage: '消息',
|
||||||
|
channel: '消息渠道',
|
||||||
|
msgChannelBase: '基础权限',
|
||||||
|
saveMsgChannel: '保存消息渠道',
|
||||||
|
delMsgChannel: '删除消息渠道',
|
||||||
|
msgTmpl: '消息模板',
|
||||||
|
msgTmplBase: '基础权限',
|
||||||
|
saveMsgTmpl: '保存消息模板',
|
||||||
|
delMsgTmpl: '删除消息模板',
|
||||||
|
sendMsg: '发送消息',
|
||||||
|
|
||||||
|
system: '系统管理',
|
||||||
|
menuPermission: '菜单权限',
|
||||||
|
menuPermissionBase: '基本权限',
|
||||||
|
menuPermissionAdd: '添加菜单权限',
|
||||||
|
menuPermissionEdit: '编辑菜单权限',
|
||||||
|
menuPermissionDelete: '删除菜单权限',
|
||||||
|
menuPermissionEnableDisable: '启用/禁用菜单权限',
|
||||||
|
account: '账号管理',
|
||||||
|
accountBase: '基本权限',
|
||||||
|
accountAdd: '添加账号',
|
||||||
|
accountEdit: '编辑账号',
|
||||||
|
accountDelete: '删除账号',
|
||||||
|
accountEnableDisable: '启用/禁用账号',
|
||||||
|
accountRoleAllocation: '角色分配',
|
||||||
|
role: '角色管理',
|
||||||
|
roleBase: '基本权限',
|
||||||
|
roleAdd: '添加角色',
|
||||||
|
roleEdit: '编辑角色',
|
||||||
|
roleDelete: '删除角色',
|
||||||
|
roleMenuPermissionAllocation: '菜单权限分配',
|
||||||
|
sysConf: '系统配置',
|
||||||
|
sysConfBase: '基本权限',
|
||||||
|
sysConfSave: '保存配置',
|
||||||
|
opLog: '操作日志',
|
||||||
|
opLogBase: '基本权限',
|
||||||
|
|
||||||
|
noPagePermission: '无页面权限',
|
||||||
|
authcertShowciphertext: '授权凭证密文查看',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -190,6 +190,15 @@ export default {
|
|||||||
loginFailCountPlaceholder: '登录失败n次后禁止登录',
|
loginFailCountPlaceholder: '登录失败n次后禁止登录',
|
||||||
loginFainMin: '登录失败禁止登录时间',
|
loginFainMin: '登录失败禁止登录时间',
|
||||||
loginFailMinPlaceholder: '登录失败指定次数后禁止m分钟内再次登录',
|
loginFailMinPlaceholder: '登录失败指定次数后禁止m分钟内再次登录',
|
||||||
|
|
||||||
|
aiModelConf: 'AI模型配置',
|
||||||
|
aiModelType: '模型类型',
|
||||||
|
aiModelTypePlaceholder: '选择AI模型类型',
|
||||||
|
aiModel: '模型',
|
||||||
|
aiModelPlaceholder: '请输入模型',
|
||||||
|
aiBaseUrl: '地址',
|
||||||
|
aiBaseUrlPlaceholder: '请输入模型请求地址',
|
||||||
|
aiApiKey: 'API Key',
|
||||||
},
|
},
|
||||||
syslog: {
|
syslog: {
|
||||||
operator: '操作人',
|
operator: '操作人',
|
||||||
|
|||||||
@@ -7,14 +7,23 @@ export default {
|
|||||||
tagTips1: '1. 用于将资产进行归类',
|
tagTips1: '1. 用于将资产进行归类',
|
||||||
tagTips2: '2. 可在团队管理中进行分配,用于资源隔离',
|
tagTips2: '2. 可在团队管理中进行分配,用于资源隔离',
|
||||||
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
tagTips3: '3. 拥有父标签的团队成员可访问操作其自身或子标签关联的资源',
|
||||||
|
tagTips4: '4. 右击节点可进行编辑或添加子标签操作',
|
||||||
machine: '机器',
|
machine: '机器',
|
||||||
db: '数据库',
|
db: '数据库',
|
||||||
es: 'ES',
|
es: 'ES',
|
||||||
|
container: '容器',
|
||||||
code: '编号',
|
code: '编号',
|
||||||
createSubTag: '创建子标签',
|
createSubTag: '创建子标签',
|
||||||
createSubTagTitle: '创建【{codePath}】的子标签',
|
createSubTagTitle: '创建【{codePath}】的子标签',
|
||||||
rootTag: '根标签',
|
rootTag: '根标签',
|
||||||
selectTagPlaceholder: '请选择关联标签',
|
selectTagPlaceholder: '请选择关联标签',
|
||||||
|
machineOp: '机器操作',
|
||||||
|
dbDataOp: '数据库操作',
|
||||||
|
redisDataOp: 'Redis操作',
|
||||||
|
esDataOp: 'ES操作',
|
||||||
|
mongoDataOp: 'Mongo操作',
|
||||||
|
containerOp: '容器操作',
|
||||||
|
allResource: '所有资源',
|
||||||
},
|
},
|
||||||
team: {
|
team: {
|
||||||
team: '团队',
|
team: '团队',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-main class="layout-main !h-full">
|
<el-main class="layout-main h-full">
|
||||||
<el-scrollbar ref="layoutScrollbarRef" view-class="!h-full">
|
<el-scrollbar ref="layoutScrollbarRef" view-class="h-full">
|
||||||
<LayoutParentView />
|
<LayoutParentView />
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" name="layoutMain">
|
<script setup lang="ts" name="layoutMain">
|
||||||
import { getCurrentInstance, watch, defineAsyncComponent } from 'vue';
|
import { watch, defineAsyncComponent, useTemplateRef, nextTick, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useThemeConfig } from '@/store/themeConfig';
|
import { useThemeConfig } from '@/store/themeConfig';
|
||||||
@@ -21,22 +21,33 @@ import { useThemeConfig } from '@/store/themeConfig';
|
|||||||
const LayoutParentView = defineAsyncComponent(() => import('@/layout/routerView/parent.vue'));
|
const LayoutParentView = defineAsyncComponent(() => import('@/layout/routerView/parent.vue'));
|
||||||
const Footer = defineAsyncComponent(() => import('@/layout/footer/index.vue'));
|
const Footer = defineAsyncComponent(() => import('@/layout/footer/index.vue'));
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as any;
|
const layoutScrollbarRef = useTemplateRef('layoutScrollbarRef');
|
||||||
const { themeConfig } = storeToRefs(useThemeConfig());
|
const { themeConfig } = storeToRefs(useThemeConfig());
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
|
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
|
||||||
watch(themeConfig.value, (val) => {
|
watch(themeConfig.value, (val) => {
|
||||||
if (val.isFixedHeaderChange !== val.isFixedHeader) {
|
if (val.isFixedHeaderChange !== val.isFixedHeader) {
|
||||||
if (!proxy.$refs.layoutScrollbarRef) return false;
|
if (!layoutScrollbarRef.value) {
|
||||||
proxy.$refs.layoutScrollbarRef.update();
|
return;
|
||||||
|
}
|
||||||
|
layoutScrollbarRef.value.update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听路由的变化
|
// 监听路由的变化
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
() => {
|
() => {
|
||||||
proxy.$refs.layoutScrollbarRef.wrapRef.scrollTop = 0;
|
nextTick(() => {
|
||||||
|
if (!layoutScrollbarRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
layoutScrollbarRef.value.update();
|
||||||
|
}, 500);
|
||||||
|
layoutScrollbarRef.value.setScrollTop();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="layout-container flex-center layout-backtop">
|
<el-container class="layout-container layout-backtop !flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
<Main />
|
<Main />
|
||||||
</el-container>
|
</el-container>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
<el-option label="vs" value="vs"> </el-option>
|
<el-option label="vs" value="vs"> </el-option>
|
||||||
<el-option label="vs-dark" value="vs-dark"> </el-option>
|
<el-option label="vs-dark" value="vs-dark"> </el-option>
|
||||||
<el-option label="SolarizedLight" value="SolarizedLight"> </el-option>
|
<el-option label="SolarizedLight" value="SolarizedLight"> </el-option>
|
||||||
|
<el-option label="SolarizedDark" value="SolarizedDark"> </el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -654,8 +655,9 @@ const onCopyConfigClick = (target: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const checkClientWidth = () => {
|
const checkClientWidth = () => {
|
||||||
const oldLayout = getLocal('oldLayout');
|
let oldLayout = getLocal('oldLayout');
|
||||||
if (!oldLayout) {
|
if (!oldLayout) {
|
||||||
|
oldLayout = themeConfig.value.layout;
|
||||||
setLocal('oldLayout', themeConfig.value.layout);
|
setLocal('oldLayout', themeConfig.value.layout);
|
||||||
}
|
}
|
||||||
if (width.value < 1000) {
|
if (width.value < 1000) {
|
||||||
|
|||||||
@@ -34,13 +34,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup name="navMenuHorizontal">
|
<script lang="ts" setup name="navMenuHorizontal">
|
||||||
import { reactive, computed, onMounted, inject, defineAsyncComponent } from 'vue';
|
import { reactive, computed, onMounted, inject } from 'vue';
|
||||||
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
|
import { useRoute, onBeforeRouteUpdate } from 'vue-router';
|
||||||
|
import SubItem from '@/layout/navMenu/subItem.vue';
|
||||||
import { useRoutesList } from '@/store/routesList';
|
import { useRoutesList } from '@/store/routesList';
|
||||||
import { useThemeConfig } from '@/store/themeConfig';
|
import { useThemeConfig } from '@/store/themeConfig';
|
||||||
|
|
||||||
const SubItem = defineAsyncComponent(() => import('@/layout/navMenu/subItem.vue'));
|
|
||||||
|
|
||||||
// 定义父组件传过来的值
|
// 定义父组件传过来的值
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 菜单列表
|
// 菜单列表
|
||||||
@@ -117,42 +116,30 @@ onBeforeRouteUpdate((to) => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin-right: 30px;
|
margin-right: 30px;
|
||||||
|
|
||||||
.horizontal-menu {
|
::v-deep(.el-scrollbar__bar.is-vertical) {
|
||||||
border: none !important;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(a) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu.el-menu--horizontal {
|
||||||
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
border-bottom: none !important;
|
||||||
::v-deep(.el-menu-item) {
|
|
||||||
height: 42px;
|
|
||||||
line-height: 42px;
|
|
||||||
padding: 0 15px !important;
|
|
||||||
margin: 0 5px;
|
|
||||||
border-radius: 6px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.el-sub-menu__title) {
|
|
||||||
height: 42px;
|
|
||||||
line-height: 42px;
|
|
||||||
padding: 0 25px 0 15px !important; /* 右边留出更多空间给箭头图标 */
|
|
||||||
margin: 0 5px;
|
|
||||||
border-radius: 6px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.el-sub-menu__icon-arrow) {
|
|
||||||
right: 5px !important;
|
|
||||||
margin-top: -5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep(.el-menu-item.is-active),
|
|
||||||
::v-deep(.el-sub-menu.is-active .el-sub-menu__title) {
|
|
||||||
color: #409eff;
|
|
||||||
background-color: rgba(64, 158, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 菜单项基础样式 - 统一一级菜单和子菜单目录的宽度
|
||||||
|
.horizontal-menu :deep(.el-menu-item),
|
||||||
|
.horizontal-menu :deep(.el-sub-menu__title) {
|
||||||
|
margin: 0 5px !important;
|
||||||
|
justify-content: center;
|
||||||
|
width: fit-content;
|
||||||
|
text-align: center; // 使文字居中对齐
|
||||||
|
padding: 0 16px !important; // 统一内边距
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,55 +21,46 @@ const routeModules: Record<string, any> = import.meta.glob(['../views/**/route.{
|
|||||||
|
|
||||||
// 后端控制路由:执行路由数据初始化
|
// 后端控制路由:执行路由数据初始化
|
||||||
export async function initBackendRoutes() {
|
export async function initBackendRoutes() {
|
||||||
let allModuleRoutes = {};
|
// 合并所有模块路由
|
||||||
for (const path in routeModules) {
|
const allModuleRoutes = Object.values(routeModules).reduce((acc: any, module: any) => {
|
||||||
// 获取默认导出的路由
|
return { ...acc, ...module.default };
|
||||||
const routes = routeModules[path]?.default;
|
}, {});
|
||||||
allModuleRoutes = { ...allModuleRoutes, ...routes };
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = getToken(); // 获取浏览器缓存 token 值
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
// 无 token 停止执行下一步
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
useUserInfo().setUserInfo({});
|
useUserInfo().setUserInfo({});
|
||||||
// 获取路由
|
|
||||||
let menuRoute = await getBackEndControlRoutes();
|
|
||||||
|
|
||||||
const cacheList: Array<string> = [];
|
|
||||||
// 处理路由(component)
|
|
||||||
const routes = backEndRouterConverter(allModuleRoutes, menuRoute, (router: any) => {
|
|
||||||
// 可能为false时不存在isKeepAlive属性
|
|
||||||
if (!router.meta.isKeepAlive) {
|
|
||||||
router.meta.isKeepAlive = false;
|
|
||||||
}
|
|
||||||
if (router.meta.isKeepAlive) {
|
|
||||||
cacheList.push(router.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
routes.forEach((item: any) => {
|
|
||||||
if (item.meta.isFull) {
|
|
||||||
// 菜单为全屏展示 (示例:数据大屏页面等)
|
|
||||||
router.addRoute(item as RouteRecordRaw);
|
|
||||||
} else {
|
|
||||||
// 要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute(),这将有效地添加路由,就像通过 children 添加的一样
|
|
||||||
router.addRoute(LAYOUT_ROUTE_NAME, item as RouteRecordRaw);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useKeepALiveNames().setCacheKeepAlive(cacheList);
|
|
||||||
useRoutesList().setRoutesList(routes);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
|
|
||||||
export async function getBackEndControlRoutes() {
|
|
||||||
try {
|
try {
|
||||||
|
// 获取路由和权限
|
||||||
const menuAndPermission = await openApi.getPermissions();
|
const menuAndPermission = await openApi.getPermissions();
|
||||||
// 赋值权限码,用于控制按钮等
|
|
||||||
useUserInfo().userInfo.permissions = menuAndPermission.permissions;
|
useUserInfo().userInfo.permissions = menuAndPermission.permissions;
|
||||||
return menuAndPermission.menus;
|
const menuRoute = menuAndPermission.menus;
|
||||||
|
|
||||||
|
const cacheList: string[] = [];
|
||||||
|
|
||||||
|
// 处理路由(component)
|
||||||
|
const routes = backEndRouterConverter(allModuleRoutes, menuRoute, (router: any) => {
|
||||||
|
// 确保 isKeepAlive 属性存在
|
||||||
|
router.meta.isKeepAlive = router.meta.isKeepAlive ?? false;
|
||||||
|
if (router.meta.isKeepAlive) {
|
||||||
|
cacheList.push(router.name as string);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加路由
|
||||||
|
routes.forEach((item: any) => {
|
||||||
|
if (item.meta.isFull) {
|
||||||
|
router.addRoute(item as RouteRecordRaw);
|
||||||
|
} else {
|
||||||
|
router.addRoute(LAYOUT_ROUTE_NAME, item as RouteRecordRaw);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useKeepALiveNames().setCacheKeepAlive(cacheList);
|
||||||
|
useRoutesList().setRoutesList(routes);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('获取菜单权限信息失败', e);
|
console.error('获取菜单权限信息失败', e);
|
||||||
clearSession();
|
clearSession();
|
||||||
@@ -97,57 +88,52 @@ type RouterConvCallbackFunc = (router: any) => void;
|
|||||||
* @param meta.linkType ==> 外链类型, 内嵌: 以iframe展示、外链: 新标签打开
|
* @param meta.linkType ==> 外链类型, 内嵌: 以iframe展示、外链: 新标签打开
|
||||||
* @param meta.link ==> 外链地址
|
* @param meta.link ==> 外链地址
|
||||||
* */
|
* */
|
||||||
export function backEndRouterConverter(allModuleRoutes: any, routes: any, callbackFunc: RouterConvCallbackFunc = null as any, parentPath: string = '/') {
|
export function backEndRouterConverter(allModuleRoutes: any, routes: any, callbackFunc?: RouterConvCallbackFunc, parentPath = '/'): any[] {
|
||||||
if (!routes) {
|
if (!routes) return [];
|
||||||
return [];
|
|
||||||
}
|
return routes.map((item: any) => {
|
||||||
|
if (!item.meta) return item;
|
||||||
|
|
||||||
const routeItems = [];
|
|
||||||
for (let item of routes) {
|
|
||||||
if (!item.meta) {
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
// 将json字符串的meta转为对象
|
// 将json字符串的meta转为对象
|
||||||
item.meta = JSON.parse(item.meta);
|
const meta = typeof item.meta === 'string' ? JSON.parse(item.meta) : item.meta;
|
||||||
|
|
||||||
|
// 处理路径
|
||||||
let path = item.code;
|
let path = item.code;
|
||||||
// 如果不是以 / 开头,则路径需要拼接父路径
|
|
||||||
if (!path.startsWith('/')) {
|
if (!path.startsWith('/')) {
|
||||||
path = parentPath + '/' + path;
|
path = `${parentPath}/${path}`.replace(/\/+/g, '/');
|
||||||
}
|
}
|
||||||
item.path = path;
|
|
||||||
delete item['code'];
|
|
||||||
|
|
||||||
// route.meta.title == resource.name
|
// 构建路由对象
|
||||||
item.meta.title = item.name;
|
const routeItem: any = {
|
||||||
delete item['name'];
|
path,
|
||||||
|
name: meta.routeName,
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
title: item.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// route.name == resource.meta.routeName
|
// 处理外链
|
||||||
const routerName = item.meta.routeName;
|
if (meta.link) {
|
||||||
item.name = routerName;
|
routeItem.component = meta.linkType == LinkTypeEnum.Link.value ? Link : Iframe;
|
||||||
// 如果是外链类型,name的路由名都是Link 或者 Iframes会导致路由名重复,无法添加多个外链
|
|
||||||
if (item.meta.link) {
|
|
||||||
if (item.meta.linkType == LinkTypeEnum.Link.value) {
|
|
||||||
item.component = Link;
|
|
||||||
} else {
|
|
||||||
item.component = Iframe;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// routerName == 模块下route.ts 字段key == 组件名
|
// 使用模块路由组件
|
||||||
item.component = allModuleRoutes[routerName];
|
routeItem.component = allModuleRoutes[meta.routeName];
|
||||||
}
|
}
|
||||||
delete item.meta['routeName'];
|
|
||||||
|
|
||||||
// route.redirect == resource.meta.redirect
|
// 处理重定向
|
||||||
if (item.meta.redirect) {
|
if (meta.redirect) {
|
||||||
item.redirect = item.meta.redirect;
|
routeItem.redirect = meta.redirect;
|
||||||
delete item.meta['redirect'];
|
|
||||||
}
|
}
|
||||||
// 存在回调,则执行回调
|
|
||||||
callbackFunc && callbackFunc(item);
|
|
||||||
item.children && backEndRouterConverter(allModuleRoutes, item.children, callbackFunc, item.path);
|
|
||||||
routeItems.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return routeItems;
|
// 处理子路由
|
||||||
|
if (item.children) {
|
||||||
|
routeItem.children = backEndRouterConverter(allModuleRoutes, item.children, callbackFunc, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行回调
|
||||||
|
callbackFunc?.(routeItem);
|
||||||
|
|
||||||
|
return routeItem;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,12 @@ import { defineStore } from 'pinia';
|
|||||||
export const useAutoOpenResource = defineStore('autoOpenResource', {
|
export const useAutoOpenResource = defineStore('autoOpenResource', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
autoOpenResource: {
|
autoOpenResource: {
|
||||||
machineCodePath: '',
|
codePath: '',
|
||||||
dbCodePath: '',
|
|
||||||
redisCodePath: '',
|
|
||||||
mongoCodePath: '',
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setMachineCodePath(codePath: string) {
|
setCodePath(codePath: string) {
|
||||||
this.autoOpenResource.machineCodePath = codePath;
|
this.autoOpenResource.codePath = codePath;
|
||||||
},
|
|
||||||
setDbCodePath(codePath: string) {
|
|
||||||
this.autoOpenResource.dbCodePath = codePath;
|
|
||||||
},
|
|
||||||
setRedisCodePath(codePath: string) {
|
|
||||||
this.autoOpenResource.redisCodePath = codePath;
|
|
||||||
},
|
|
||||||
setMongoCodePath(codePath: string) {
|
|
||||||
this.autoOpenResource.mongoCodePath = codePath;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,41 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 淡入淡出滑动效果
|
||||||
|
.slide-fade-enter-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-active {
|
||||||
|
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
transform: translateX(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 水平滑动效果
|
||||||
|
.slide-x-enter-active,
|
||||||
|
.slide-x-leave-active {
|
||||||
|
transition: all 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-x-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-x-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-30px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Breadcrumb 面包屑过渡动画
|
/* Breadcrumb 面包屑过渡动画
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
.breadcrumb-enter-active,
|
.breadcrumb-enter-active,
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
@use 'mixins/index' as mixins;
|
|
||||||
|
|
||||||
/* NavMenu 导航菜单
|
/* NavMenu 导航菜单
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
$radius: 6px;
|
$radius: 6px;
|
||||||
@@ -24,12 +22,6 @@ $spacing: 8px;
|
|||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
color: #5a5a5a; // 统一调整菜单字体颜色为更舒适的深灰色
|
color: #5a5a5a; // 统一调整菜单字体颜色为更舒适的深灰色
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
// 第三方图标字体间距/大小设置
|
|
||||||
.icon,
|
|
||||||
.fa {
|
|
||||||
@include mixins.generalIcon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item {
|
.el-menu-item {
|
||||||
@@ -131,35 +123,10 @@ $spacing: 8px;
|
|||||||
|
|
||||||
// 横向菜单
|
// 横向菜单
|
||||||
.el-menu--horizontal {
|
.el-menu--horizontal {
|
||||||
background: var(--bg-topBar);
|
|
||||||
|
|
||||||
.el-menu-item,
|
.el-menu-item,
|
||||||
.el-sub-menu {
|
.el-sub-menu {
|
||||||
height: $menuHeight;
|
|
||||||
line-height: $menuHeight;
|
|
||||||
color: var(--bg-topBarColor);
|
color: var(--bg-topBarColor);
|
||||||
border-radius: $radius;
|
|
||||||
padding: 0 10px !important; // 减小内边距
|
|
||||||
|
|
||||||
.el-sub-menu__title {
|
|
||||||
height: $menuHeight;
|
|
||||||
line-height: $menuHeight;
|
|
||||||
color: var(--bg-topBarColor);
|
|
||||||
border-radius: $radius;
|
|
||||||
padding: 0 10px !important; // 减小内边距
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu-item.is-active,
|
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
|
||||||
color: #409eff;
|
|
||||||
background-color: rgba(64, 158, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu-item:hover,
|
|
||||||
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
|
||||||
background-color: rgba(64, 158, 255, 0.05);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,33 +138,15 @@ $spacing: 8px;
|
|||||||
|
|
||||||
.el-menu-item,
|
.el-menu-item,
|
||||||
.el-sub-menu__title {
|
.el-sub-menu__title {
|
||||||
height: $menuHeight;
|
|
||||||
line-height: $menuHeight;
|
|
||||||
color: var(--bg-topBarColor);
|
color: var(--bg-topBarColor);
|
||||||
border-radius: $radius;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
padding: 0 10px !important; // 减小内边距
|
padding: 0 10px !important; // 减小内边距
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-item:not(.is-active):hover,
|
|
||||||
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
|
|
||||||
color: var(--bg-topBarColor);
|
|
||||||
background-color: rgba(0, 0, 0, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu-item.is-active,
|
|
||||||
.el-sub-menu.is-active .el-sub-menu__title {
|
|
||||||
background-color: rgba(64, 158, 255, 0.1);
|
|
||||||
color: #409eff;
|
|
||||||
font-weight: 500;
|
|
||||||
border-bottom: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 为水平菜单的子菜单项正确处理箭头图标位置
|
// 为水平菜单的子菜单项正确处理箭头图标位置
|
||||||
.el-sub-menu {
|
.el-sub-menu {
|
||||||
.el-sub-menu__title {
|
.el-sub-menu__title {
|
||||||
padding-right: 20px !important; // 调整箭头图标空间
|
padding-right: 22px !important; // 调整箭头图标空间
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
/* 第三方图标字体间距/大小设置
|
|
||||||
------------------------------- */
|
|
||||||
@mixin generalIcon {
|
|
||||||
font-size: 14px !important;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 5px;
|
|
||||||
width: 24px;
|
|
||||||
text-align: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 文本不换行
|
/* 文本不换行
|
||||||
------------------------------- */
|
------------------------------- */
|
||||||
@mixin text-no-wrap() {
|
@mixin text-no-wrap() {
|
||||||
|
|||||||
@@ -5,47 +5,45 @@
|
|||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
|
<el-form :model="modelValue" ref="formRef" :rules="rules" label-width="auto">
|
||||||
<el-form-item prop="bizType" :label="$t('flow.bizType')">
|
<el-form-item prop="bizType" :label="$t('flow.bizType')">
|
||||||
<EnumSelect v-model="form.bizType" :enums="FlowBizType" />
|
<EnumSelect v-model="modelValue.bizType" :enums="FlowBizType" @change="changeBizType" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="remark" :label="$t('common.remark')">
|
<el-form-item prop="remark" :label="$t('common.remark')">
|
||||||
<el-input v-model.trim="form.remark" type="textarea" auto-complete="off" clearable></el-input>
|
<el-input v-model.trim="modelValue.remark" type="textarea" auto-complete="off" clearable></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-divider content-position="left">{{ $t('flow.bizInfo') }}</el-divider>
|
<el-divider content-position="left">{{ $t('flow.bizInfo') }}</el-divider>
|
||||||
<component
|
<component
|
||||||
ref="bizFormRef"
|
ref="bizFormRef"
|
||||||
v-if="form.bizType"
|
v-if="modelValue.bizType"
|
||||||
:is="bizComponents[form.bizType]"
|
:is="bizComponents[modelValue.bizType]"
|
||||||
v-model:bizForm="form.bizForm"
|
v-model:bizForm="modelValue.bizForm"
|
||||||
@changeResourceCode="changeResourceCode"
|
@changeResourceCode="changeResourceCode"
|
||||||
>
|
>
|
||||||
</component>
|
</component>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<span v-if="flowProcdef || !state.form.procdefId">
|
<span v-if="flowProcdef || !modelValue.procdefId">
|
||||||
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
|
<el-divider content-position="left">{{ $t('flow.approvalNode') }}</el-divider>
|
||||||
|
|
||||||
<FlowDesign height="300px" v-if="flowProcdef" :data="flowProcdef.flowDef" disabled center />
|
<FlowDesign height="300px" v-if="flowProcdef" :data="flowProcdef.flowDef" disabled center />
|
||||||
|
|
||||||
<el-result v-if="!state.form.procdefId" icon="error" :title="$t('flow.approvalNodeNotExist')" :sub-title="$t('flow.resourceNotExistFlow')">
|
<el-result v-if="!modelValue.procdefId" icon="error" :title="$t('flow.approvalNodeNotExist')" :sub-title="$t('flow.resourceNotExistFlow')">
|
||||||
</el-result>
|
</el-result>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div>
|
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
||||||
<el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
|
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!modelValue.procdefId">{{ $t('common.confirm') }}</el-button>
|
||||||
<el-button type="primary" :loading="saveBtnLoading" @click="btnOk" :disabled="!state.form.procdefId">{{ $t('common.confirm') }}</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef } from 'vue';
|
import { toRefs, reactive, defineAsyncComponent, shallowReactive, useTemplateRef, watch, onMounted } from 'vue';
|
||||||
import { procdefApi, procinstApi } from './api';
|
import { procdefApi, procinstApi } from './api';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
@@ -68,6 +66,17 @@ const props = defineProps({
|
|||||||
|
|
||||||
const visible = defineModel<boolean>('visible', { default: false });
|
const visible = defineModel<boolean>('visible', { default: false });
|
||||||
|
|
||||||
|
const modelValue = defineModel('modelValue', {
|
||||||
|
default: () => ({
|
||||||
|
bizType: FlowBizType.DbSqlExec.value,
|
||||||
|
procdefId: 0,
|
||||||
|
status: null,
|
||||||
|
remark: '',
|
||||||
|
bizKey: '',
|
||||||
|
bizForm: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['cancel', 'val-change']);
|
const emit = defineEmits(['cancel', 'val-change']);
|
||||||
|
|
||||||
@@ -85,34 +94,42 @@ const rules = {
|
|||||||
remark: [Rules.requiredInput('common.remark')],
|
remark: [Rules.requiredInput('common.remark')],
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultForm = {
|
|
||||||
bizType: FlowBizType.DbSqlExec.value,
|
|
||||||
procdefId: -1,
|
|
||||||
status: null,
|
|
||||||
remark: '',
|
|
||||||
bizForm: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
tasks: [] as any,
|
tasks: [] as any,
|
||||||
form: { ...defaultForm },
|
|
||||||
flowProcdef: null as any,
|
flowProcdef: null as any,
|
||||||
sortable: '' as any,
|
sortable: '' as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { form, flowProcdef } = toRefs(state);
|
const { flowProcdef } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(form);
|
const { isFetching: saveBtnLoading, execute: procinstStart } = procinstApi.start.useApi(modelValue);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => modelValue.value.procdefId,
|
||||||
|
async () => {
|
||||||
|
if (!modelValue.value.procdefId || state.flowProcdef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.flowProcdef = await procdefApi.detail.request({ id: modelValue.value.procdefId });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const changeResourceCode = async (resourceType: any, code: string) => {
|
const changeResourceCode = async (resourceType: any, code: string) => {
|
||||||
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
|
state.flowProcdef = await procdefApi.getByResource.request({ resourceType, resourceCode: code });
|
||||||
if (!state.flowProcdef) {
|
if (!state.flowProcdef) {
|
||||||
state.form.procdefId = 0;
|
modelValue.value.procdefId = 0;
|
||||||
} else {
|
} else {
|
||||||
state.form.procdefId = state.flowProcdef.id;
|
modelValue.value.procdefId = state.flowProcdef.id;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeBizType = () => {
|
||||||
|
//重置流程定义ID
|
||||||
|
modelValue.value.procdefId = 0;
|
||||||
|
state.flowProcdef = null;
|
||||||
|
modelValue.value.bizForm = {};
|
||||||
|
};
|
||||||
|
|
||||||
const btnOk = async () => {
|
const btnOk = async () => {
|
||||||
try {
|
try {
|
||||||
await formRef.value.validate();
|
await formRef.value.validate();
|
||||||
@@ -124,7 +141,7 @@ const btnOk = async () => {
|
|||||||
|
|
||||||
await procinstStart();
|
await procinstStart();
|
||||||
ElMessage.success(t('flow.procinstStartSuccess'));
|
ElMessage.success(t('flow.procinstStartSuccess'));
|
||||||
emit('val-change', state.form);
|
emit('val-change', modelValue.value);
|
||||||
//重置表单域
|
//重置表单域
|
||||||
cancel();
|
cancel();
|
||||||
};
|
};
|
||||||
@@ -136,7 +153,9 @@ const cancel = () => {
|
|||||||
formRef.value.resetFields();
|
formRef.value.resetFields();
|
||||||
bizFormRef.value.resetBizForm();
|
bizFormRef.value.resetBizForm();
|
||||||
|
|
||||||
state.form = { ...defaultForm };
|
setTimeout(() => {
|
||||||
|
modelValue.value = {} as any;
|
||||||
|
}, 500);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss"></style>
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
size="50%"
|
size="50%"
|
||||||
body-class="!p-2"
|
body-class="!p-2"
|
||||||
header-class="!mb-2"
|
header-class="!mb-2"
|
||||||
|
:destroy-on-close="true"
|
||||||
:close-on-click-modal="!props.instTaskId"
|
:close-on-click-modal="!props.instTaskId"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
<el-form-item prop="status" :label="$t('flow.approveResult')" required>
|
<el-form-item prop="status" :label="$t('flow.approveResult')" required>
|
||||||
<el-select v-model="form.status">
|
<el-select v-model="form.status">
|
||||||
<el-option :label="$t(ProcinstTaskStatus.Pass.label)" :value="ProcinstTaskStatus.Pass.value"> </el-option>
|
<el-option :label="$t(ProcinstTaskStatus.Pass.label)" :value="ProcinstTaskStatus.Pass.value"> </el-option>
|
||||||
<!-- <el-option :label="ProcinstTaskStatus.Back.label" :value="ProcinstTaskStatus.Back.value"> </el-option> -->
|
<el-option :label="$t(ProcinstTaskStatus.Back.label)" :value="ProcinstTaskStatus.Back.value"> </el-option>
|
||||||
<el-option :label="$t(ProcinstTaskStatus.Reject.label)" :value="ProcinstTaskStatus.Reject.value"> </el-option>
|
<el-option :label="$t(ProcinstTaskStatus.Reject.label)" :value="ProcinstTaskStatus.Reject.value"> </el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -133,6 +134,9 @@ const { procinst, flowDef, form, saveBtnLoading } = toRefs(state);
|
|||||||
watch(
|
watch(
|
||||||
() => props.procinstId,
|
() => props.procinstId,
|
||||||
async (newValue: any) => {
|
async (newValue: any) => {
|
||||||
|
state.form.status = ProcinstTaskStatus.Pass.value;
|
||||||
|
state.form.remark = '';
|
||||||
|
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
state.procinst = {};
|
state.procinst = {};
|
||||||
state.flowDef = null;
|
state.flowDef = null;
|
||||||
@@ -155,7 +159,7 @@ watch(
|
|||||||
{} as Record<string, typeof res>
|
{} as Record<string, typeof res>
|
||||||
);
|
);
|
||||||
|
|
||||||
const nodeKey2Tasks = state.procinst.procinstTasks.reduce(
|
const nodeKey2Tasks = state.procinst.procinstTasks?.reduce(
|
||||||
(acc: { [x: string]: any[] }, item: { nodeKey: any }) => {
|
(acc: { [x: string]: any[] }, item: { nodeKey: any }) => {
|
||||||
const key = item.nodeKey;
|
const key = item.nodeKey;
|
||||||
if (!acc[key]) {
|
if (!acc[key]) {
|
||||||
@@ -172,7 +176,7 @@ watch(
|
|||||||
if (nodeKey2Ops[key]) {
|
if (nodeKey2Ops[key]) {
|
||||||
// 将操作记录挂载到 node 下,例如命名为 historyList
|
// 将操作记录挂载到 node 下,例如命名为 historyList
|
||||||
node.extra.opLog = nodeKey2Ops[key][0];
|
node.extra.opLog = nodeKey2Ops[key][0];
|
||||||
node.extra.tasks = nodeKey2Tasks[key];
|
node.extra.tasks = nodeKey2Tasks?.[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,18 @@
|
|||||||
<template #action="{ data }">
|
<template #action="{ data }">
|
||||||
<el-button link @click="showProcinst(data)" type="primary">{{ $t('common.detail') }}</el-button>
|
<el-button link @click="showProcinst(data)" type="primary">{{ $t('common.detail') }}</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="data.status == ProcinstStatus.Back.value && data.creator == useUserInfo().userInfo.username"
|
||||||
|
link
|
||||||
|
@click="startProcInst(data)"
|
||||||
|
type="primary"
|
||||||
|
>{{ $t('common.edit') }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-popconfirm
|
<el-popconfirm
|
||||||
v-if="data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value"
|
v-if="
|
||||||
|
data.status == ProcinstStatus.Active.value || data.status == ProcinstStatus.Suspended.value || data.status == ProcinstStatus.Back.value
|
||||||
|
"
|
||||||
:title="$t('flow.cancelProcessConfirm')"
|
:title="$t('flow.cancelProcessConfirm')"
|
||||||
width="160"
|
width="160"
|
||||||
@confirm="procinstCancel(data)"
|
@confirm="procinstCancel(data)"
|
||||||
@@ -37,7 +47,7 @@
|
|||||||
@cancel="procinstDetail.procinstId = 0"
|
@cancel="procinstDetail.procinstId = 0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProcInstEdit v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
|
<ProcinstEdit v-model="procinstEdit.procinst" v-model:visible="procinstEdit.visible" :title="procinstEdit.title" @val-change="search" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -50,9 +60,10 @@ import { SearchItem } from '@/components/pagetable/SearchForm';
|
|||||||
import ProcinstDetail from './ProcinstDetail.vue';
|
import ProcinstDetail from './ProcinstDetail.vue';
|
||||||
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
import { FlowBizType, ProcinstBizStatus, ProcinstStatus } from './enums';
|
||||||
import { formatTime } from '@/common/utils/format';
|
import { formatTime } from '@/common/utils/format';
|
||||||
import ProcInstEdit from './ProcInstEdit.vue';
|
|
||||||
import { useI18nDetailTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nDetailTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import ProcinstEdit from '@/views/flow/ProcinstEdit.vue';
|
||||||
|
import { useUserInfo } from '@/store/userInfo';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -106,6 +117,7 @@ const state = reactive({
|
|||||||
procinstEdit: {
|
procinstEdit: {
|
||||||
title: '',
|
title: '',
|
||||||
visible: false,
|
visible: false,
|
||||||
|
procinst: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,8 +139,19 @@ const showProcinst = (data: any) => {
|
|||||||
state.procinstDetail.visible = true;
|
state.procinstDetail.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const startProcInst = () => {
|
const startProcInst = (procinst: any = null) => {
|
||||||
state.procinstEdit.title = t('flow.startProcess');
|
state.procinstEdit.title = t('flow.startProcess');
|
||||||
|
if (procinst) {
|
||||||
|
const data = { ...procinst };
|
||||||
|
data.bizForm = JSON.parse(procinst.bizForm || {});
|
||||||
|
state.procinstEdit.procinst = data;
|
||||||
|
} else {
|
||||||
|
state.procinstEdit.procinst = {
|
||||||
|
bizType: FlowBizType.DbSqlExec.value,
|
||||||
|
bizForm: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
state.procinstEdit.visible = true;
|
state.procinstEdit.visible = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :style="{ height: props.height }" class="flex flex-col" v-loading="saveing">
|
<div :style="{ height: props.height }" class="flex flex-col" v-loading="saveing">
|
||||||
<div class="h-[100vh]" ref="flowContainerRef"></div>
|
<div class="h-screen" ref="flowContainerRef"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PropSettingDrawer
|
<PropSettingDrawer
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<el-tabs v-model="activeTabName">
|
||||||
|
<el-tab-pane :name="approvalRecordTabName" v-if="activeTabName == approvalRecordTabName" :label="$t('flow.approvalRecord')">
|
||||||
|
<el-table :data="props.node?.properties?.tasks" stripe width="100%">
|
||||||
|
<el-table-column :label="$t('common.createTime')" min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('common.time')" min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.endTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('flow.approver')" min-width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.handler || '' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('flow.approveResult')" width="80">
|
||||||
|
<template #default="scope">
|
||||||
|
<EnumTag :enums="ProcinstTaskStatus" :value="scope.row.status" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column :label="$t('flow.approvalRemark')" min-width="150">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ scope.row.remark }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane :label="$t('common.basic')" :name="basicTabName">
|
||||||
|
<el-form-item prop="auditRule" :label="$t('flow.aiAuditRule')">
|
||||||
|
<el-input v-model="form.auditRule" type="textarea" :rows="10" :placeholder="$t('flow.aiAuditRuleTip')" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { notEmpty } from '@/common/assert';
|
||||||
|
import { formatDate } from '@/common/utils/format';
|
||||||
|
import EnumTag from '@/components/enumtag/EnumTag.vue';
|
||||||
|
import { useI18nPleaseInput } from '@/hooks/useI18n';
|
||||||
|
import { ProcinstTaskStatus } from '@/views/flow/enums';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
// 节点信息
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const basicTabName = 'basic';
|
||||||
|
const approvalRecordTabName = 'approvalRecord';
|
||||||
|
|
||||||
|
const activeTabName = computed(() => {
|
||||||
|
console.log(props.node);
|
||||||
|
// 如果存在审批记录 tasks 且长度大于0,则激活审批记录 tab
|
||||||
|
if (props.node?.properties?.opLog) {
|
||||||
|
return approvalRecordTabName;
|
||||||
|
}
|
||||||
|
return basicTabName;
|
||||||
|
});
|
||||||
|
|
||||||
|
const form: any = defineModel<any>('modelValue', { required: true });
|
||||||
|
|
||||||
|
const confirm = () => {
|
||||||
|
notEmpty(form.value.auditRule, useI18nPleaseInput('flow.aiAuditRule'));
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
confirm,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { RectNode, RectNodeModel, h } from '@logicflow/core';
|
||||||
|
import PropSetting from './PropSetting.vue';
|
||||||
|
import { NodeTypeEnum } from '../enums';
|
||||||
|
import { HisProcinstOpState, ProcinstTaskStatus } from '@/views/flow/enums';
|
||||||
|
|
||||||
|
class AiTaskNodeModel extends RectNodeModel {
|
||||||
|
initNodeData(data: any) {
|
||||||
|
super.initNodeData(data);
|
||||||
|
this.width = 100;
|
||||||
|
this.height = 60;
|
||||||
|
this.radius = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeStyle() {
|
||||||
|
const style = super.getNodeStyle();
|
||||||
|
const properties = this.properties;
|
||||||
|
|
||||||
|
const opLog: any = properties.opLog;
|
||||||
|
if (!opLog) {
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
|
||||||
|
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
|
||||||
|
style.stroke = 'green';
|
||||||
|
} else if (opLog.extra.approvalResult == ProcinstTaskStatus.Back.value) {
|
||||||
|
style.stroke = '#e6a23c';
|
||||||
|
} else {
|
||||||
|
style.stroke = 'red';
|
||||||
|
}
|
||||||
|
} else if (opLog.state == HisProcinstOpState.Failed.value) {
|
||||||
|
style.stroke = 'red';
|
||||||
|
} else {
|
||||||
|
style.stroke = 'rgb(100, 100, 255)'; // AI模型节点使用不同的颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AiTaskNodeView extends RectNode {
|
||||||
|
// 获取标签形状的方法,用于在节点中添加一个自定义的 SVG 元素
|
||||||
|
getShape() {
|
||||||
|
// 获取XxxNodeModel中定义的形状属性
|
||||||
|
const { model } = this.props;
|
||||||
|
console.log(model.properties);
|
||||||
|
const { x, y, width, height, radius } = model;
|
||||||
|
// 获取XxxNodeModel中定义的样式属性
|
||||||
|
const style = model.getNodeStyle();
|
||||||
|
|
||||||
|
return h('g', {}, [
|
||||||
|
h('rect', {
|
||||||
|
...style,
|
||||||
|
x: x - width / 2,
|
||||||
|
y: y - height / 2,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rx: radius,
|
||||||
|
ry: radius,
|
||||||
|
}),
|
||||||
|
h(
|
||||||
|
'svg',
|
||||||
|
{
|
||||||
|
x: x - width / 2 + 5,
|
||||||
|
y: y - height / 2 + 5,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
viewBox: '0 0 1024 1024',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h('path', {
|
||||||
|
d: 'M517.818182 23.272727a488.727273 488.727273 0 1 0 488.727273 488.727273 488.727273 488.727273 0 0 0-488.727273-488.727273z m0 930.909091a442.181818 442.181818 0 1 1 442.181818-442.181818 442.181818 442.181818 0 0 1-442.181818 442.181818z',
|
||||||
|
}),
|
||||||
|
h('path', {
|
||||||
|
d: 'M490.356364 346.298182l-40.029091-18.618182-162.909091 349.090909 42.123636 19.781818 47.941818-102.865454h162.909091v-25.6l48.174546 126.836363 43.52-16.523636-128-337.454545z m-91.229091 200.610909l73.774545-158.254546 60.043637 158.254546zM704 337.454545h46.545455v349.09091h-46.545455z',
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeType = NodeTypeEnum.AiTask;
|
||||||
|
const nodeTypeExtra = nodeType.extra;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
order: nodeTypeExtra.order,
|
||||||
|
type: nodeType.value,
|
||||||
|
// 注册配置信息
|
||||||
|
registerConf: {
|
||||||
|
type: nodeType.value,
|
||||||
|
model: AiTaskNodeModel,
|
||||||
|
view: AiTaskNodeView,
|
||||||
|
},
|
||||||
|
dndPanelConf: {
|
||||||
|
type: nodeType.value,
|
||||||
|
text: nodeTypeExtra.text,
|
||||||
|
label: nodeType.label,
|
||||||
|
icon: 'data:image/svg+xml;charset=utf-8;base64,PHN2ZyB0PSIxNzY0NDkwMzI5ODU0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzMTMxIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik01MTcuODE4MTgyIDIzLjI3MjcyN2E0ODguNzI3MjczIDQ4OC43MjcyNzMgMCAxIDAgNDg4LjcyNzI3MyA0ODguNzI3MjczIDQ4OC43MjcyNzMgNDg4LjcyNzI3MyAwIDAgMC00ODguNzI3MjczLTQ4OC43MjcyNzN6IG0wIDkzMC45MDkwOTFhNDQyLjE4MTgxOCA0NDIuMTgxODE4IDAgMSAxIDQ0Mi4xODE4MTgtNDQyLjE4MTgxOCA0NDIuMTgxODE4IDQ0Mi4xODE4MTggMCAwIDEtNDQyLjE4MTgxOCA0NDIuMTgxODE4eiIgcC1pZD0iMTMxMzIiPjwvcGF0aD48cGF0aCBkPSJNNDkwLjM1NjM2NCAzNDYuMjk4MTgybC00MC4wMjkwOTEtMTguNjE4MTgyLTE2Mi45MDkwOTEgMzQ5LjA5MDkwOSA0Mi4xMjM2MzYgMTkuNzgxODE4IDQ3Ljk0MTgxOC0xMDIuODY1NDU0aDE2Mi45MDkwOTF2LTI1LjZsNDguMTc0NTQ2IDEyNi44MzYzNjMgNDMuNTItMTYuNTIzNjM2LTEyOC0zMzcuNDU0NTQ1eiBtLTkxLjIyOTA5MSAyMDAuNjEwOTA5bDczLjc3NDU0NS0xNTguMjU0NTQ2IDYwLjA0MzYzNyAxNTguMjU0NTQ2ek03MDQgMzM3LjQ1NDU0NWg0Ni41NDU0NTV2MzQ5LjA5MDkxaC00Ni41NDU0NTV6IiBwLWlkPSIxMzEzMyI+PC9wYXRoPjwvc3ZnPg==',
|
||||||
|
properties: nodeTypeExtra.defaultProp,
|
||||||
|
},
|
||||||
|
propSettingComp: PropSetting,
|
||||||
|
};
|
||||||
@@ -24,14 +24,20 @@ export const NodeTypeEnum = {
|
|||||||
text: i18n.global.t('flow.usertask'),
|
text: i18n.global.t('flow.usertask'),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Serial: EnumValue.of('serial', i18n.global.t('flow.serial')).setExtra({
|
AiTask: EnumValue.of('aitask', i18n.global.t('flow.aitask')).setExtra({
|
||||||
order: 3,
|
order: 3,
|
||||||
|
type: 'aitask',
|
||||||
|
text: i18n.global.t('flow.aitask'),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Serial: EnumValue.of('serial', i18n.global.t('flow.serial')).setExtra({
|
||||||
|
order: 4,
|
||||||
text: i18n.global.t('flow.serial'),
|
text: i18n.global.t('flow.serial'),
|
||||||
defaultProp: { condition: `{{ procinstTaskStatus == 1 }}` },
|
defaultProp: { condition: `{{ procinstTaskStatus == 1.0 }}` },
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Parallel: EnumValue.of('parallel', i18n.global.t('flow.parallel')).setExtra({
|
Parallel: EnumValue.of('parallel', i18n.global.t('flow.parallel')).setExtra({
|
||||||
order: 4,
|
order: 5,
|
||||||
text: i18n.global.t('flow.parallel'),
|
text: i18n.global.t('flow.parallel'),
|
||||||
defaultProp: {},
|
defaultProp: {},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
<el-tabs v-model="activeTabName">
|
<el-tabs v-model="activeTabName">
|
||||||
<el-tab-pane :name="approvalRecordTabName" v-if="activeTabName == approvalRecordTabName" :label="$t('flow.approvalRecord')">
|
<el-tab-pane :name="approvalRecordTabName" v-if="activeTabName == approvalRecordTabName" :label="$t('flow.approvalRecord')">
|
||||||
<el-table :data="props.node?.properties?.tasks" stripe width="100%">
|
<el-table :data="props.node?.properties?.tasks" stripe width="100%">
|
||||||
|
<el-table-column :label="$t('common.createTime')" min-width="135">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.createTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column :label="$t('common.time')" min-width="135">
|
<el-table-column :label="$t('common.time')" min-width="135">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ formatDate(scope.row.endTime) }}
|
{{ formatDate(scope.row.endTime) }}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class UserTaskModel extends RectNodeModel {
|
|||||||
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
|
if (opLog.state == HisProcinstOpState.Completed.value && opLog.extra) {
|
||||||
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
|
if (opLog.extra.approvalResult == ProcinstTaskStatus.Pass.value) {
|
||||||
style.stroke = 'green';
|
style.stroke = 'green';
|
||||||
|
} else if (opLog.extra.approvalResult == ProcinstTaskStatus.Back.value) {
|
||||||
|
style.stroke = '#e6a23c';
|
||||||
} else {
|
} else {
|
||||||
style.stroke = 'red';
|
style.stroke = 'red';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const ProcinstStatus = {
|
|||||||
Active: EnumValue.of(1, 'flow.active').setTagType('primary'),
|
Active: EnumValue.of(1, 'flow.active').setTagType('primary'),
|
||||||
Completed: EnumValue.of(2, 'flow.completed').setTagType('success'),
|
Completed: EnumValue.of(2, 'flow.completed').setTagType('success'),
|
||||||
Suspended: EnumValue.of(-1, 'flow.suspended').setTagType('warning'),
|
Suspended: EnumValue.of(-1, 'flow.suspended').setTagType('warning'),
|
||||||
|
Back: EnumValue.of(-11, 'flow.back').setTagType('warning'),
|
||||||
Terminated: EnumValue.of(-2, 'flow.terminated').setTagType('danger'),
|
Terminated: EnumValue.of(-2, 'flow.terminated').setTagType('danger'),
|
||||||
Cancelled: EnumValue.of(-3, 'flow.cancelled').setTagType('warning'),
|
Cancelled: EnumValue.of(-3, 'flow.cancelled').setTagType('warning'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,15 @@
|
|||||||
:placeholder="$t('flow.selectDbPlaceholder')"
|
:placeholder="$t('flow.selectDbPlaceholder')"
|
||||||
v-model:db-id="bizForm.dbId"
|
v-model:db-id="bizForm.dbId"
|
||||||
v-model:db-name="bizForm.dbName"
|
v-model:db-name="bizForm.dbName"
|
||||||
v-model:db-type="dbType"
|
v-model:inst-name="bizForm.instName"
|
||||||
|
v-model:db-type="bizForm.dbType"
|
||||||
|
v-model:tag-path="bizForm.tagPath"
|
||||||
@select-db="changeResourceCode"
|
@select-db="changeResourceCode"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="sql" label="SQL" required>
|
<el-form-item prop="sql" label="SQL" required>
|
||||||
<div class="!w-full">
|
<div class="w-full!">
|
||||||
<monaco-editor height="300px" language="sql" v-model="bizForm.sql" />
|
<monaco-editor height="300px" language="sql" v-model="bizForm.sql" />
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -19,7 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
import { registerDbCompletionItemProvider } from '@/views/ops/db/db';
|
||||||
@@ -38,17 +40,24 @@ const formRef: any = ref(null);
|
|||||||
const bizForm = defineModel<any>('bizForm', {
|
const bizForm = defineModel<any>('bizForm', {
|
||||||
default: {
|
default: {
|
||||||
dbId: 0,
|
dbId: 0,
|
||||||
|
instName: '',
|
||||||
dbName: '',
|
dbName: '',
|
||||||
|
dbType: '',
|
||||||
|
tagPath: '',
|
||||||
sql: '',
|
sql: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const dbType = ref('');
|
onMounted(() => {
|
||||||
|
if (bizForm.value.dbId) {
|
||||||
|
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], bizForm.value.dbType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => bizForm.value.dbId,
|
() => bizForm.value.dbId,
|
||||||
() => {
|
() => {
|
||||||
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], dbType.value);
|
registerDbCompletionItemProvider(bizForm.value.dbId, bizForm.value.dbName, [bizForm.value.dbName], bizForm.value.dbType);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
<el-form :model="bizForm" ref="formRef" :rules="rules" label-width="auto">
|
||||||
<el-form-item prop="id" label="DB" required>
|
<el-form-item prop="id" label="DB" required>
|
||||||
<TagTreeResourceSelect
|
<ResourceSelect
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-model="selectRedis"
|
v-model="selectRedis"
|
||||||
@change="changeRedis"
|
@change="changeRedis"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
:tag-path-node-type="NodeTypeTagPath"
|
:tag-path-node-type="NodeTypeTagPath"
|
||||||
:placeholder="$t('flow.selectRedisPlaceholder')"
|
:placeholder="$t('flow.selectRedisPlaceholder')"
|
||||||
>
|
>
|
||||||
</TagTreeResourceSelect>
|
</ResourceSelect>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="cmd" label="CMD" required>
|
<el-form-item prop="cmd" label="CMD" required>
|
||||||
@@ -21,12 +21,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import TagTreeResourceSelect from '@/views/ops/component/TagTreeResourceSelect.vue';
|
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
|
||||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { redisApi } from '@/views/ops/redis/api';
|
import { redisApi } from '@/views/ops/redis/api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { RedisIcon } from '@/views/ops/redis/resource';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(asyn
|
|||||||
await sleep(100);
|
await sleep(100);
|
||||||
return redisInfos.map((x: any) => {
|
return redisInfos.map((x: any) => {
|
||||||
x.tagPath = parentNode.key;
|
x.tagPath = parentNode.key;
|
||||||
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x);
|
return new TagTreeNode(`${x.code}`, x.name, NodeTypeRedis).withParams(x).withIcon(RedisIcon);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,15 +62,18 @@ const NodeTypeRedis = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTr
|
|||||||
const redisInfo = parentNode.params;
|
const redisInfo = parentNode.params;
|
||||||
|
|
||||||
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
let dbs: TagTreeNode[] = redisInfo.db.split(',').map((x: string) => {
|
||||||
return new TagTreeNode(x, `db${x}`, 2 as any).withIsLeaf(true).withParams({
|
return new TagTreeNode(x, `db${x}`, 2 as any)
|
||||||
id: redisInfo.id,
|
.withIsLeaf(true)
|
||||||
db: x,
|
.withParams({
|
||||||
name: `db${x}`,
|
id: redisInfo.id,
|
||||||
keys: 0,
|
db: x,
|
||||||
tagPath: redisInfo.tagPath,
|
name: `db${x}`,
|
||||||
redisName: redisInfo.name,
|
keys: 0,
|
||||||
code: redisInfo.code,
|
tagPath: redisInfo.tagPath,
|
||||||
});
|
redisName: redisInfo.name,
|
||||||
|
code: redisInfo.code,
|
||||||
|
})
|
||||||
|
.withIcon({ name: 'Coin', color: '#67c23a' });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (redisInfo.mode == 'cluster') {
|
if (redisInfo.mode == 'cluster') {
|
||||||
@@ -100,15 +104,14 @@ const bizForm = defineModel<any>('bizForm', {
|
|||||||
id: 0,
|
id: 0,
|
||||||
db: 0,
|
db: 0,
|
||||||
cmd: '',
|
cmd: '',
|
||||||
|
tagPath: '',
|
||||||
|
redisName: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const redisName = ref('');
|
|
||||||
const tagPath = ref('');
|
|
||||||
|
|
||||||
const selectRedis = computed({
|
const selectRedis = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return redisName.value ? `${tagPath.value} > ${redisName.value} > db${bizForm.value.db}` : '';
|
return bizForm.value.redisName ? `${bizForm.value.tagPath} > ${bizForm.value.redisName} > db${bizForm.value.db}` : '';
|
||||||
},
|
},
|
||||||
set: () => {
|
set: () => {
|
||||||
//
|
//
|
||||||
@@ -117,8 +120,8 @@ const selectRedis = computed({
|
|||||||
|
|
||||||
const changeRedis = (nodeData: TagTreeNode) => {
|
const changeRedis = (nodeData: TagTreeNode) => {
|
||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
tagPath.value = params.tagPath;
|
bizForm.value.tagPath = params.tagPath;
|
||||||
redisName.value = params.redisName;
|
bizForm.value.redisName = params.redisName;
|
||||||
bizForm.value.id = params.id;
|
bizForm.value.id = params.id;
|
||||||
bizForm.value.db = parseInt(params.db);
|
bizForm.value.db = parseInt(params.db);
|
||||||
|
|
||||||
|
|||||||
@@ -365,30 +365,14 @@ const initData = async () => {
|
|||||||
|
|
||||||
const toPage = (item: any, codePath = '') => {
|
const toPage = (item: any, codePath = '') => {
|
||||||
let path;
|
let path;
|
||||||
|
useAutoOpenResource().setCodePath(codePath);
|
||||||
switch (item) {
|
switch (item) {
|
||||||
case 'personal': {
|
case 'personal': {
|
||||||
router.push('/personal');
|
router.push('/personal');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'mongo': {
|
default: {
|
||||||
useAutoOpenResource().setMongoCodePath(codePath);
|
path = '/my-resource';
|
||||||
path = '/mongo/mongo-data-operation';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'machine': {
|
|
||||||
useAutoOpenResource().setMachineCodePath(codePath);
|
|
||||||
path = '/machine/machines-op';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'db': {
|
|
||||||
useAutoOpenResource().setDbCodePath(codePath);
|
|
||||||
path = '/dbms/sql-exec';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'redis': {
|
|
||||||
useAutoOpenResource().setRedisCodePath(codePath);
|
|
||||||
path = '/redis/data-operation';
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI
|
|||||||
import { tmplApi } from '../api';
|
import { tmplApi } from '../api';
|
||||||
import { TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
|
import { TmplStatusEnum, TmplTypeEnum, ChannelTypeEnum } from '../enums';
|
||||||
import TmplEdit from './TmplEdit.vue';
|
import TmplEdit from './TmplEdit.vue';
|
||||||
import EnumValue from '../../../common/Enum';
|
import EnumValue from '@/common/Enum';
|
||||||
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
import AccountSelectFormItem from '@/views/system/account/components/AccountSelectFormItem.vue';
|
||||||
|
|
||||||
const perms = {
|
const perms = {
|
||||||
|
|||||||
@@ -37,10 +37,9 @@
|
|||||||
:label="$t(TagResourceTypeEnum.Machine.label)"
|
:label="$t(TagResourceTypeEnum.Machine.label)"
|
||||||
:value="TagResourceTypeEnum.Machine.value"
|
:value="TagResourceTypeEnum.Machine.value"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-option
|
<el-option
|
||||||
:key="TagResourceTypeEnum.DbInstance.value"
|
:key="TagResourceTypeEnum.DbInstance.value"
|
||||||
:label="TagResourceTypeEnum.DbInstance.label"
|
:label="$t(TagResourceTypeEnum.DbInstance.label)"
|
||||||
:value="TagResourceTypeEnum.DbInstance.value"
|
:value="TagResourceTypeEnum.DbInstance.value"
|
||||||
/>
|
/>
|
||||||
<el-option
|
<el-option
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button v-auth="'authcert:save'" @click="edit(scope.row, scope.$index)" type="primary" icon="edit" link></el-button>
|
<el-button v-auth="'authcert:save'" @click="edit(scope.row, scope.$index)" type="primary" icon="edit" link></el-button>
|
||||||
<el-button class="!ml-0.5" v-auth="'authcert:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete" link></el-button>
|
<el-button class="ml-0.5!" v-auth="'authcert:del'" type="danger" @click="deleteRow(scope.$index)" icon="delete" link></el-button>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
:title="$t('ac.testConn')"
|
:title="$t('ac.testConn')"
|
||||||
:loading="props.testConnBtnLoading && scope.$index == state.idx"
|
:loading="props.testConnBtnLoading && scope.$index == state.idx"
|
||||||
:disabled="props.testConnBtnLoading"
|
:disabled="props.testConnBtnLoading"
|
||||||
class="!ml-0.5"
|
class="ml-0.5!"
|
||||||
type="success"
|
type="success"
|
||||||
@click="testConn(scope.row, scope.$index)"
|
@click="testConn(scope.row, scope.$index)"
|
||||||
icon="Link"
|
icon="Link"
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-splitter @resize="handleResize">
|
|
||||||
<el-splitter-panel :size="leftPaneSize + '%'" max="30%">
|
|
||||||
<slot name="left"></slot>
|
|
||||||
</el-splitter-panel>
|
|
||||||
|
|
||||||
<el-splitter-panel>
|
|
||||||
<slot name="right"></slot>
|
|
||||||
</el-splitter-panel>
|
|
||||||
</el-splitter>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useWindowSize } from '@vueuse/core';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['resize']);
|
|
||||||
|
|
||||||
const { width } = useWindowSize();
|
|
||||||
|
|
||||||
const leftPaneSize = computed(() => (width.value >= 1600 ? 20 : 24));
|
|
||||||
|
|
||||||
// 处理 resize 事件
|
|
||||||
const handleResize = (event: any) => {
|
|
||||||
emit('resize', event);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-card class="h-full flex" body-class="!p-1 flex flex-col w-full">
|
|
||||||
<el-input v-model="filterText" :placeholder="$t('tag.tagFilterPlaceholder')" clearable size="small" class="!mb-1 w-full" />
|
|
||||||
<el-scrollbar>
|
|
||||||
<el-tree
|
|
||||||
class="min-w-full inline-block"
|
|
||||||
ref="treeRef"
|
|
||||||
:highlight-current="true"
|
|
||||||
:indent="10"
|
|
||||||
:load="loadNode"
|
|
||||||
:props="treeProps"
|
|
||||||
lazy
|
|
||||||
node-key="key"
|
|
||||||
:expand-on-click-node="false"
|
|
||||||
:filter-node-method="filterNode"
|
|
||||||
@node-click="treeNodeClick"
|
|
||||||
@node-expand="treeNodeClick"
|
|
||||||
@node-contextmenu="nodeContextmenu"
|
|
||||||
:default-expanded-keys="props.defaultExpandedKeys"
|
|
||||||
>
|
|
||||||
<template #default="{ node, data }">
|
|
||||||
<div
|
|
||||||
:id="node.key"
|
|
||||||
class="w-full node-container flex items-center cursor-pointer select-none"
|
|
||||||
:class="data.type.nodeDblclickFunc ? 'select-none' : ''"
|
|
||||||
>
|
|
||||||
<span v-if="data.type.value == TagTreeNode.TagPath">
|
|
||||||
<tag-info :tag-path="data.label" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<slot v-else :node="node" :data="data" name="prefix"></slot>
|
|
||||||
|
|
||||||
<span class="ml-0.5" :title="data.labelRemark">
|
|
||||||
<slot name="label" :data="data" v-if="!data.disabled"> {{ $t(data.label) }}</slot>
|
|
||||||
<!-- 禁用状态 -->
|
|
||||||
<slot name="disabledLabel" :data="data" v-else>
|
|
||||||
<el-link type="danger" disabled underline="never">
|
|
||||||
{{ `${$t(data.label)}` }}
|
|
||||||
</el-link>
|
|
||||||
</slot>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="absolute right-2.5 mt-0.5 text-[10px] text-gray-400">
|
|
||||||
<slot :node="node" :data="data" name="suffix"></slot>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
|
|
||||||
<contextmenu :dropdown="state.dropdown" :items="state.contextmenuItems" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
|
|
||||||
</el-scrollbar>
|
|
||||||
</el-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
|
|
||||||
import { NodeType, TagTreeNode } from './tag';
|
|
||||||
import TagInfo from './TagInfo.vue';
|
|
||||||
import { Contextmenu } from '@/components/contextmenu';
|
|
||||||
import { tagApi } from '../tag/api';
|
|
||||||
import { isPrefixSubsequence } from '@/common/utils/string';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
resourceType: {
|
|
||||||
type: [Number, String],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
defaultExpandedKeys: {
|
|
||||||
type: [Array],
|
|
||||||
},
|
|
||||||
tagPathNodeType: {
|
|
||||||
type: [NodeType],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
load: {
|
|
||||||
type: Function,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
loadContextmenuItems: {
|
|
||||||
type: Function,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const treeProps = {
|
|
||||||
label: 'name',
|
|
||||||
children: 'zones',
|
|
||||||
isLeaf: 'isLeaf',
|
|
||||||
};
|
|
||||||
|
|
||||||
const emit = defineEmits(['nodeClick', 'currentContextmenuClick']);
|
|
||||||
const treeRef: any = ref(null);
|
|
||||||
const contextmenuRef = ref();
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
height: 600 as any,
|
|
||||||
filterText: '',
|
|
||||||
dropdown: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
contextmenuItems: [],
|
|
||||||
opend: {},
|
|
||||||
});
|
|
||||||
const { filterText } = toRefs(state);
|
|
||||||
|
|
||||||
onMounted(async () => {});
|
|
||||||
|
|
||||||
watch(filterText, (val) => {
|
|
||||||
treeRef.value?.filter(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
const filterNode = (value: string, data: any) => {
|
|
||||||
return !value || isPrefixSubsequence(value, data.label);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载标签树节点
|
|
||||||
*/
|
|
||||||
const loadTags = async () => {
|
|
||||||
const tags = await tagApi.getResourceTagPaths.request({ resourceType: props.resourceType });
|
|
||||||
const tagNodes = [];
|
|
||||||
for (let tagPath of tags) {
|
|
||||||
tagNodes.push(new TagTreeNode(tagPath, tagPath, props.tagPathNodeType));
|
|
||||||
}
|
|
||||||
return tagNodes;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载树节点
|
|
||||||
* @param { Object } node
|
|
||||||
* @param { Object } resolve
|
|
||||||
*/
|
|
||||||
const loadNode = async (node: any, resolve: (data: any) => void, reject: () => void) => {
|
|
||||||
if (typeof resolve !== 'function') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let nodes = [];
|
|
||||||
try {
|
|
||||||
if (node.level == 0) {
|
|
||||||
nodes = await loadTags();
|
|
||||||
} else if (props.load) {
|
|
||||||
nodes = await props.load(node);
|
|
||||||
} else {
|
|
||||||
nodes = await node.data.loadChildren();
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e);
|
|
||||||
// 调用 reject 以保持节点状态,并允许远程加载继续。
|
|
||||||
return reject();
|
|
||||||
}
|
|
||||||
return resolve(nodes);
|
|
||||||
};
|
|
||||||
|
|
||||||
let lastNodeClickTime = 0;
|
|
||||||
|
|
||||||
const treeNodeClick = async (data: any, node: any) => {
|
|
||||||
const currentClickNodeTime = Date.now();
|
|
||||||
if (currentClickNodeTime - lastNodeClickTime < 300) {
|
|
||||||
treeNodeDblclick(data, node);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastNodeClickTime = currentClickNodeTime;
|
|
||||||
|
|
||||||
if (!data.disabled && !data.type.nodeDblclickFunc && data.type.nodeClickFunc) {
|
|
||||||
emit('nodeClick', data);
|
|
||||||
await data.type.nodeClickFunc(data);
|
|
||||||
}
|
|
||||||
// 关闭可能存在的右击菜单
|
|
||||||
contextmenuRef.value.closeContextmenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 树节点双击事件
|
|
||||||
const treeNodeDblclick = (data: any, node: any) => {
|
|
||||||
if (node.expanded) {
|
|
||||||
node.collapse();
|
|
||||||
} else {
|
|
||||||
node.expand();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.disabled && data.type.nodeDblclickFunc) {
|
|
||||||
data.type.nodeDblclickFunc(data);
|
|
||||||
}
|
|
||||||
// 关闭可能存在的右击菜单
|
|
||||||
contextmenuRef.value.closeContextmenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 树节点右击事件
|
|
||||||
const nodeContextmenu = (event: any, data: any) => {
|
|
||||||
if (data.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载当前节点是否需要显示右击菜单
|
|
||||||
let items = data.type.contextMenuItems;
|
|
||||||
if (!items || items.length == 0) {
|
|
||||||
if (props.loadContextmenuItems) {
|
|
||||||
items = props.loadContextmenuItems(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!items) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.contextmenuItems = items;
|
|
||||||
const { clientX, clientY } = event;
|
|
||||||
state.dropdown.x = clientX;
|
|
||||||
state.dropdown.y = clientY;
|
|
||||||
contextmenuRef.value.openContextmenu(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCurrentContextmenuClick = (clickData: any) => {
|
|
||||||
emit('currentContextmenuClick', clickData);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadNode = (nodeKey: any) => {
|
|
||||||
let node = getNode(nodeKey);
|
|
||||||
node.loaded = false;
|
|
||||||
node.expand();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getNode = (nodeKey: any) => {
|
|
||||||
let node = treeRef.value.getNode(nodeKey);
|
|
||||||
if (!node) {
|
|
||||||
throw new Error('未找到节点: ' + nodeKey);
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCurrentKey = (nodeKey: any) => {
|
|
||||||
treeRef.value.setCurrentKey(nodeKey);
|
|
||||||
|
|
||||||
// 通过Id获取到对应的dom元素
|
|
||||||
const node = document.getElementById(nodeKey);
|
|
||||||
if (node) {
|
|
||||||
setTimeout(() => {
|
|
||||||
nextTick(() => {
|
|
||||||
// 通过scrollIntoView方法将对应的dom元素定位到可见区域 【block: 'center'】这个属性是在垂直方向居中显示
|
|
||||||
node.scrollIntoView({ block: 'center' });
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
reloadNode,
|
|
||||||
getNode,
|
|
||||||
setCurrentKey,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
|
||||||
@@ -2,6 +2,49 @@ import { OptionsApi, SearchItem } from '@/components/pagetable/SearchForm';
|
|||||||
import { ContextmenuItem } from '@/components/contextmenu';
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
import { TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
import { tagApi } from '../tag/api';
|
import { tagApi } from '../tag/api';
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
|
// 资源配置
|
||||||
|
export interface ResourceConfig {
|
||||||
|
order?: number;
|
||||||
|
resourceType: number; // 资源类型
|
||||||
|
rootNodeType: NodeType; // 资源根节点类型
|
||||||
|
|
||||||
|
// 资源管理组件配置
|
||||||
|
manager?: {
|
||||||
|
componentConf: ResourceComponentConfig; // 组件
|
||||||
|
countKey?: string; // 统计数key,tab展示的数字对象key
|
||||||
|
permCode?: string; // 权限码
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceComponentConfig {
|
||||||
|
name: string; // 名称
|
||||||
|
component?: any; // 组件
|
||||||
|
icon?: {
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceOpCtx {
|
||||||
|
/**
|
||||||
|
* 添加资源相关组件
|
||||||
|
* @param component 资源相关组件配置
|
||||||
|
* @returns 组件引用
|
||||||
|
*/
|
||||||
|
addResourceComponent(component: ResourceComponentConfig): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取树节点
|
||||||
|
* @param nodeKey 节点key
|
||||||
|
*/
|
||||||
|
getTreeNode(nodeKey: string): any;
|
||||||
|
|
||||||
|
setCurrentTreeKey(nodeKey: string): void;
|
||||||
|
|
||||||
|
reloadTreeNode(nodeKey: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
export class TagTreeNode {
|
export class TagTreeNode {
|
||||||
/**
|
/**
|
||||||
@@ -41,6 +84,14 @@ export class TagTreeNode {
|
|||||||
|
|
||||||
icon: any;
|
icon: any;
|
||||||
|
|
||||||
|
// 节点组件
|
||||||
|
nodeComponent?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点上下文
|
||||||
|
*/
|
||||||
|
ctx?: ResourceOpCtx;
|
||||||
|
|
||||||
static TagPath = -1;
|
static TagPath = -1;
|
||||||
|
|
||||||
constructor(key: any, label: string, type?: NodeType) {
|
constructor(key: any, label: string, type?: NodeType) {
|
||||||
@@ -49,6 +100,10 @@ export class TagTreeNode {
|
|||||||
this.type = type || new NodeType(TagTreeNode.TagPath);
|
this.type = type || new NodeType(TagTreeNode.TagPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static new(parent: TagTreeNode, key: any, label: string, type?: NodeType) {
|
||||||
|
return new TagTreeNode(key, label, type).withContext(parent.ctx);
|
||||||
|
}
|
||||||
|
|
||||||
withLabelRemark(labelRemark: any) {
|
withLabelRemark(labelRemark: any) {
|
||||||
this.labelRemark = labelRemark;
|
this.labelRemark = labelRemark;
|
||||||
return this;
|
return this;
|
||||||
@@ -74,6 +129,16 @@ export class TagTreeNode {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withNodeComponent(component: any) {
|
||||||
|
this.nodeComponent = markRaw(component);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(ctx: ResourceOpCtx | undefined) {
|
||||||
|
this.ctx = ctx;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载子节点,使用节点类型的loadNodesFunc去加载子节点
|
* 加载子节点,使用节点类型的loadNodesFunc去加载子节点
|
||||||
* @returns 子节点信息
|
* @returns 子节点信息
|
||||||
@@ -108,7 +173,7 @@ export class NodeType {
|
|||||||
nodeClickFunc: (node: TagTreeNode) => void;
|
nodeClickFunc: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
// 节点双击事件
|
// 节点双击事件
|
||||||
nodeDblclickFunc: (node: TagTreeNode) => void;
|
nodeDblclickFunc?: (node: TagTreeNode) => void;
|
||||||
|
|
||||||
constructor(value: number) {
|
constructor(value: number) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|||||||
@@ -1,246 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" :destroy-on-close="true" width="38%">
|
|
||||||
<el-form :model="state.form" ref="backupForm" label-width="auto" :rules="rules">
|
|
||||||
<el-form-item prop="dbNames" label="数据库名称">
|
|
||||||
<el-select
|
|
||||||
v-model="state.dbNamesSelected"
|
|
||||||
multiple
|
|
||||||
clearable
|
|
||||||
collapse-tags
|
|
||||||
collapse-tags-tooltip
|
|
||||||
filterable
|
|
||||||
:disabled="state.editOrCreate"
|
|
||||||
:filter-method="filterDbNames"
|
|
||||||
placeholder="数据库名称"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<el-checkbox v-model="checkAllDbNames" :indeterminate="indeterminateDbNames" @change="handleCheckAll"> 全选 </el-checkbox>
|
|
||||||
</template>
|
|
||||||
<el-option v-for="db in state.dbNamesFiltered" :key="db" :label="db" :value="db" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="name" label="任务名称">
|
|
||||||
<el-input v-model="state.form.name" type="text" placeholder="任务名称"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="startTime" label="开始时间">
|
|
||||||
<el-date-picker v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="intervalDay" label="备份周期(天)">
|
|
||||||
<el-input v-model.number="state.form.intervalDay" type="number" placeholder="单位:天"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="maxSaveDays" label="备份历史保留天数">
|
|
||||||
<el-input v-model.number="state.form.maxSaveDays" type="number" placeholder="0: 永久保留"></el-input>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="cancel()">取 消</el-button>
|
|
||||||
<el-button type="primary" :loading="state.btnLoading" @click="btnOk">确 定</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { reactive, ref, toRefs, watch } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import type { CheckboxValueType } from 'element-plus';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: {
|
|
||||||
type: [Boolean, Object],
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible', {
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
//定义事件
|
|
||||||
const emit = defineEmits(['cancel', 'val-change']);
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
dbNames: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择需要备份的数据库',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
intervalDay: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
pattern: /^[1-9]\d*$/,
|
|
||||||
message: '请输入正整数',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
startTime: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择开始时间',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
maxSaveDays: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
pattern: /^[0-9]\d*$/,
|
|
||||||
message: '请输入非负整数',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const backupForm: any = ref(null);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
form: {
|
|
||||||
id: 0,
|
|
||||||
dbId: 0,
|
|
||||||
dbNames: '',
|
|
||||||
name: '',
|
|
||||||
intervalDay: 1,
|
|
||||||
startTime: null as any,
|
|
||||||
repeated: true,
|
|
||||||
maxSaveDays: 0,
|
|
||||||
},
|
|
||||||
btnLoading: false,
|
|
||||||
dbNamesSelected: [] as any,
|
|
||||||
dbNamesWithoutBackup: [] as any,
|
|
||||||
dbNamesFiltered: [] as any,
|
|
||||||
filterString: '',
|
|
||||||
editOrCreate: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { dbNamesSelected, dbNamesWithoutBackup } = toRefs(state);
|
|
||||||
|
|
||||||
const checkAllDbNames = ref(false);
|
|
||||||
const indeterminateDbNames = ref(false);
|
|
||||||
|
|
||||||
watch(visible, (newValue: any) => {
|
|
||||||
if (newValue) {
|
|
||||||
init(props.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const init = (data: any) => {
|
|
||||||
state.dbNamesSelected = [];
|
|
||||||
state.form.dbId = props.dbId;
|
|
||||||
if (data) {
|
|
||||||
state.editOrCreate = true;
|
|
||||||
state.dbNamesWithoutBackup = [data.dbName];
|
|
||||||
state.dbNamesSelected = [data.dbName];
|
|
||||||
state.form.id = data.id;
|
|
||||||
state.form.dbNames = data.dbName;
|
|
||||||
state.form.name = data.name;
|
|
||||||
state.form.intervalDay = data.intervalDay;
|
|
||||||
state.form.startTime = data.startTime;
|
|
||||||
state.form.maxSaveDays = data.maxSaveDays;
|
|
||||||
} else {
|
|
||||||
state.editOrCreate = false;
|
|
||||||
state.form.name = '';
|
|
||||||
state.form.intervalDay = 1;
|
|
||||||
const now = new Date();
|
|
||||||
state.form.startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
||||||
state.form.maxSaveDays = 0;
|
|
||||||
getDbNamesWithoutBackup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDbNamesWithoutBackup = async () => {
|
|
||||||
if (props.dbId > 0) {
|
|
||||||
state.dbNamesWithoutBackup = await dbApi.getDbNamesWithoutBackup.request({ dbId: props.dbId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const btnOk = async () => {
|
|
||||||
backupForm.value.validate(async (valid: boolean) => {
|
|
||||||
if (!valid) {
|
|
||||||
ElMessage.error('请正确填写信息');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.form.repeated = true;
|
|
||||||
const reqForm = { ...state.form };
|
|
||||||
let api = dbApi.createDbBackup;
|
|
||||||
if (props.data) {
|
|
||||||
api = dbApi.saveDbBackup;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
state.btnLoading = true;
|
|
||||||
await api.request(reqForm);
|
|
||||||
ElMessage.success('保存成功');
|
|
||||||
emit('val-change', state.form);
|
|
||||||
cancel();
|
|
||||||
} finally {
|
|
||||||
state.btnLoading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
visible.value = false;
|
|
||||||
emit('cancel');
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkDbSelect = (val: string[]) => {
|
|
||||||
const selected = val.filter((dbName: string) => {
|
|
||||||
return dbName.includes(state.filterString);
|
|
||||||
});
|
|
||||||
if (selected.length === 0) {
|
|
||||||
checkAllDbNames.value = false;
|
|
||||||
indeterminateDbNames.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (selected.length === state.dbNamesFiltered.length) {
|
|
||||||
checkAllDbNames.value = true;
|
|
||||||
indeterminateDbNames.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
indeterminateDbNames.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(dbNamesSelected, (val: string[]) => {
|
|
||||||
checkDbSelect(val);
|
|
||||||
state.form.dbNames = val.join(' ');
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(dbNamesWithoutBackup, (val: string[]) => {
|
|
||||||
state.dbNamesFiltered = val.map((dbName: string) => dbName);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCheckAll = (val: CheckboxValueType) => {
|
|
||||||
const selected = state.dbNamesSelected.filter((dbName: string) => {
|
|
||||||
return !state.dbNamesFiltered.includes(dbName);
|
|
||||||
});
|
|
||||||
if (val) {
|
|
||||||
state.dbNamesSelected = selected.concat(state.dbNamesFiltered);
|
|
||||||
} else {
|
|
||||||
state.dbNamesSelected = selected;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterDbNames = (filterString: string) => {
|
|
||||||
state.dbNamesFiltered = state.dbNamesWithoutBackup.filter((dbName: string) => {
|
|
||||||
return dbName.includes(filterString);
|
|
||||||
});
|
|
||||||
state.filterString = filterString;
|
|
||||||
checkDbSelect(state.dbNamesSelected);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="db-backup-history">
|
|
||||||
<page-table
|
|
||||||
height="100%"
|
|
||||||
ref="pageTableRef"
|
|
||||||
:page-api="dbApi.getDbBackupHistories"
|
|
||||||
:show-selection="true"
|
|
||||||
v-model:selection-data="state.selectedData"
|
|
||||||
:searchItems="searchItems"
|
|
||||||
:before-query-fn="beforeQueryFn"
|
|
||||||
v-model:query-form="query"
|
|
||||||
:columns="columns"
|
|
||||||
>
|
|
||||||
<template #dbSelect>
|
|
||||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
|
||||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #tableHeader>
|
|
||||||
<el-button type="primary" icon="back" @click="restoreDbBackupHistory(null)">立即恢复</el-button>
|
|
||||||
<el-button type="danger" icon="delete" @click="deleteDbBackupHistory(null)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action="{ data }">
|
|
||||||
<div>
|
|
||||||
<el-button @click="restoreDbBackupHistory(data)" type="primary" link>立即恢复</el-button>
|
|
||||||
<el-button @click="deleteDbBackupHistory(data)" type="danger" link>删除</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</page-table>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toRefs, reactive, Ref, ref } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
|
||||||
import { TableColumn } from '@/components/pagetable';
|
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
|
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dbNames: {
|
|
||||||
type: [Array<String>],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
TableColumn.new('dbName', '数据库名称'),
|
|
||||||
TableColumn.new('name', '备份名称'),
|
|
||||||
TableColumn.new('createTime', '创建时间').isTime(),
|
|
||||||
TableColumn.new('lastResult', '恢复结果'),
|
|
||||||
TableColumn.new('lastTime', '恢复时间').isTime(),
|
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(160).fixedRight(),
|
|
||||||
];
|
|
||||||
|
|
||||||
const emptyQuery = {
|
|
||||||
dbId: 0,
|
|
||||||
dbName: '',
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
query: emptyQuery,
|
|
||||||
/**
|
|
||||||
* 选中的数据
|
|
||||||
*/
|
|
||||||
selectedData: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { query } = toRefs(state);
|
|
||||||
|
|
||||||
const beforeQueryFn = (query: any) => {
|
|
||||||
query.dbId = props.dbId;
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = async () => {
|
|
||||||
await pageTableRef.value.search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDbBackupHistory = async (data: any) => {
|
|
||||||
let backupHistoryId: string;
|
|
||||||
if (data) {
|
|
||||||
backupHistoryId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要删除的数据库备份历史');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ElMessageBox.confirm(`确定删除 “数据库备份历史” 吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
await dbApi.deleteDbBackupHistory.request({ dbId: props.dbId, backupHistoryId: backupHistoryId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const restoreDbBackupHistory = async (data: any) => {
|
|
||||||
let backupHistoryId: string;
|
|
||||||
if (data) {
|
|
||||||
backupHistoryId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
const pluralDbNames: string[] = [];
|
|
||||||
const dbNames: Map<string, boolean> = new Map();
|
|
||||||
state.selectedData.forEach((item: any) => {
|
|
||||||
if (!dbNames.has(item.dbName)) {
|
|
||||||
dbNames.set(item.dbName, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!dbNames.get(item.dbName)) {
|
|
||||||
dbNames.set(item.dbName, true);
|
|
||||||
pluralDbNames.push(item.dbName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (pluralDbNames.length > 0) {
|
|
||||||
ElMessage.error('多次选择相同数据库:' + pluralDbNames.join(', '));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
backupHistoryId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要恢复的数据库备份历史');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ElMessageBox.confirm(`确定从 “数据库备份历史” 中恢复数据库吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
|
|
||||||
await dbApi.restoreDbBackupHistory.request({
|
|
||||||
dbId: props.dbId,
|
|
||||||
backupHistoryId: backupHistoryId,
|
|
||||||
});
|
|
||||||
await search();
|
|
||||||
ElMessage.success('成功创建数据库恢复任务');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="db-backup">
|
|
||||||
<page-table
|
|
||||||
height="100%"
|
|
||||||
ref="pageTableRef"
|
|
||||||
:page-api="dbApi.getDbBackups"
|
|
||||||
:show-selection="true"
|
|
||||||
v-model:selection-data="state.selectedData"
|
|
||||||
:searchItems="searchItems"
|
|
||||||
:before-query-fn="beforeQueryFn"
|
|
||||||
v-model:query-form="query"
|
|
||||||
:columns="columns"
|
|
||||||
>
|
|
||||||
<template #dbSelect>
|
|
||||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
|
||||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #tableHeader>
|
|
||||||
<el-button type="primary" icon="plus" @click="createDbBackup()">添加</el-button>
|
|
||||||
<el-button type="primary" icon="video-play" @click="enableDbBackup(null)">启用</el-button>
|
|
||||||
<el-button type="primary" icon="video-pause" @click="disableDbBackup(null)">禁用</el-button>
|
|
||||||
<el-button type="danger" icon="delete" @click="deleteDbBackup(null)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action="{ data }">
|
|
||||||
<div>
|
|
||||||
<el-button @click="editDbBackup(data)" type="primary" link>编辑</el-button>
|
|
||||||
<el-button v-if="!data.enabled" @click="enableDbBackup(data)" type="primary" link>启用</el-button>
|
|
||||||
<el-button v-if="data.enabled" @click="disableDbBackup(data)" type="primary" link>禁用</el-button>
|
|
||||||
<el-button v-if="data.enabled" @click="startDbBackup(data)" type="primary" link>立即备份</el-button>
|
|
||||||
<el-button @click="deleteDbBackup(data)" type="danger" link>删除</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</page-table>
|
|
||||||
|
|
||||||
<db-backup-edit
|
|
||||||
@val-change="search"
|
|
||||||
:title="dbBackupEditDialog.title"
|
|
||||||
:dbId="dbId"
|
|
||||||
:data="dbBackupEditDialog.data"
|
|
||||||
v-model:visible="dbBackupEditDialog.visible"
|
|
||||||
></db-backup-edit>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
|
||||||
import { TableColumn } from '@/components/pagetable';
|
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
|
|
||||||
const DbBackupEdit = defineAsyncComponent(() => import('./DbBackupEdit.vue'));
|
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dbNames: {
|
|
||||||
type: [Array<String>],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
TableColumn.new('dbName', '数据库名称'),
|
|
||||||
TableColumn.new('name', '任务名称'),
|
|
||||||
TableColumn.new('startTime', '启动时间').isTime(),
|
|
||||||
TableColumn.new('intervalDay', '备份周期'),
|
|
||||||
TableColumn.new('enabledDesc', '是否启用'),
|
|
||||||
TableColumn.new('lastResult', '执行结果'),
|
|
||||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight(),
|
|
||||||
];
|
|
||||||
|
|
||||||
const emptyQuery = {
|
|
||||||
dbId: 0,
|
|
||||||
dbName: '',
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
repeated: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
query: emptyQuery,
|
|
||||||
dbBackupEditDialog: {
|
|
||||||
visible: false,
|
|
||||||
data: null as any,
|
|
||||||
title: '创建数据库备份任务',
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 选中的数据
|
|
||||||
*/
|
|
||||||
selectedData: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { query, dbBackupEditDialog } = toRefs(state);
|
|
||||||
|
|
||||||
const beforeQueryFn = (query: any) => {
|
|
||||||
query.dbId = props.dbId;
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = async () => {
|
|
||||||
await pageTableRef.value.search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDbBackup = async () => {
|
|
||||||
state.dbBackupEditDialog.data = null;
|
|
||||||
state.dbBackupEditDialog.title = '创建数据库备份任务';
|
|
||||||
state.dbBackupEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const editDbBackup = async (data: any) => {
|
|
||||||
state.dbBackupEditDialog.data = data;
|
|
||||||
state.dbBackupEditDialog.title = '修改数据库备份任务';
|
|
||||||
state.dbBackupEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableDbBackup = async (data: any) => {
|
|
||||||
let backupId: String;
|
|
||||||
if (data) {
|
|
||||||
backupId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要启用的备份任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.enableDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('启用成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableDbBackup = async (data: any) => {
|
|
||||||
let backupId: String;
|
|
||||||
if (data) {
|
|
||||||
backupId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要禁用的备份任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.disableDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('禁用成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const startDbBackup = async (data: any) => {
|
|
||||||
let backupId: String;
|
|
||||||
if (data) {
|
|
||||||
backupId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要启用的备份任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.startDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('备份任务启动成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDbBackup = async (data: any) => {
|
|
||||||
let backupId: string;
|
|
||||||
if (data) {
|
|
||||||
backupId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
backupId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要删除的数据库备份任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ElMessageBox.confirm(`确定删除 “数据库备份任务” 吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
await dbApi.deleteDbBackup.request({ dbId: props.dbId, backupId: backupId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog :title="title" :model-value="visible" :before-close="cancel" :close-on-click-modal="false" width="38%">
|
|
||||||
<el-form :model="state.form" ref="restoreForm" label-width="auto" :rules="rules">
|
|
||||||
<el-form-item label="恢复方式">
|
|
||||||
<el-radio-group :disabled="state.editOrCreate" v-model="state.restoreMode">
|
|
||||||
<el-radio label="point-in-time">指定时间点</el-radio>
|
|
||||||
<el-radio label="backup-history">指定备份</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="dbName" label="数据库名称">
|
|
||||||
<el-select
|
|
||||||
:disabled="state.editOrCreate"
|
|
||||||
@change="changeDatabase"
|
|
||||||
v-model="state.form.dbName"
|
|
||||||
placeholder="数据库名称"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
class="!w-full"
|
|
||||||
>
|
|
||||||
<el-option v-for="item in props.dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="state.restoreMode == 'point-in-time'" prop="pointInTime" label="恢复时间点">
|
|
||||||
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.pointInTime" type="datetime" placeholder="恢复时间点" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="state.restoreMode == 'backup-history'" prop="dbBackupHistoryId" label="数据库备份">
|
|
||||||
<el-select
|
|
||||||
:disabled="state.editOrCreate"
|
|
||||||
@change="changeHistory"
|
|
||||||
v-model="state.history"
|
|
||||||
value-key="id"
|
|
||||||
placeholder="数据库备份"
|
|
||||||
filterable
|
|
||||||
clearable
|
|
||||||
class="!w-full"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="item in state.histories"
|
|
||||||
:key="item.id"
|
|
||||||
:label="item.name + (item.binlogFileName ? ' ' : ' 不') + '支持指定时间点恢复'"
|
|
||||||
:value="item"
|
|
||||||
>
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item prop="startTime" label="开始时间">
|
|
||||||
<el-date-picker :disabled="state.editOrCreate" v-model="state.form.startTime" type="datetime" placeholder="开始时间" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="cancel()">取 消</el-button>
|
|
||||||
<el-button type="primary" :loading="state.btnLoading" @click="btnOk">确 定</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, reactive, ref, watch } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: {
|
|
||||||
type: [Boolean, Object],
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dbNames: {
|
|
||||||
type: Array,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
//定义事件
|
|
||||||
const emit = defineEmits(['cancel', 'val-change']);
|
|
||||||
|
|
||||||
const visible = defineModel<boolean>('visible', {
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const validatePointInTime = (rule: any, value: any, callback: any) => {
|
|
||||||
if (value > new Date()) {
|
|
||||||
callback(new Error('恢复时间点晚于当前时间'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.histories || state.histories.length == 0) {
|
|
||||||
callback(new Error('数据库没有备份记录'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let last = null;
|
|
||||||
for (const history of state.histories) {
|
|
||||||
if (!history.binlogFileName || history.binlogFileName.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (new Date(history.createTime) < value) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
last = history;
|
|
||||||
}
|
|
||||||
if (!last) {
|
|
||||||
callback(new Error('现有数据库备份不支持指定时间恢复'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
callback(last.name + ' 之前的数据库备份不支持指定时间恢复');
|
|
||||||
};
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
dbName: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择需要恢复的数据库',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pointInTime: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
validator: validatePointInTime,
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dbBackupHistoryId: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择数据库备份',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
intervalDay: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
pattern: /^[1-9]\d*$/,
|
|
||||||
message: '请输入正整数',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
startTime: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: '请选择开始时间',
|
|
||||||
trigger: ['change', 'blur'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const restoreForm: any = ref(null);
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
form: {
|
|
||||||
id: 0,
|
|
||||||
dbId: 0,
|
|
||||||
dbName: null as any,
|
|
||||||
intervalDay: 0,
|
|
||||||
startTime: null as any,
|
|
||||||
repeated: null as any,
|
|
||||||
dbBackupId: null as any,
|
|
||||||
dbBackupHistoryId: null as any,
|
|
||||||
dbBackupHistoryName: null as any,
|
|
||||||
pointInTime: null as any,
|
|
||||||
},
|
|
||||||
btnLoading: false,
|
|
||||||
dbNamesSelected: [] as any,
|
|
||||||
dbNamesWithoutRestore: [] as any,
|
|
||||||
editOrCreate: false,
|
|
||||||
histories: [] as any,
|
|
||||||
history: null as any,
|
|
||||||
restoreMode: null as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await init(props.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(visible, (newValue: any) => {
|
|
||||||
if (newValue) {
|
|
||||||
init(props.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 改变表单中的数据库字段,方便表单错误提示。如全部删光,可提示请添加数据库
|
|
||||||
*/
|
|
||||||
const changeDatabase = async () => {
|
|
||||||
await getBackupHistories(props.dbId, state.form.dbName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeHistory = async () => {
|
|
||||||
if (state.history) {
|
|
||||||
state.form.dbBackupId = state.history.dbBackupId;
|
|
||||||
state.form.dbBackupHistoryId = state.history.id;
|
|
||||||
state.form.dbBackupHistoryName = state.history.name;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async (data: any) => {
|
|
||||||
state.dbNamesSelected = [];
|
|
||||||
state.form.dbId = props.dbId;
|
|
||||||
if (data) {
|
|
||||||
state.editOrCreate = true;
|
|
||||||
state.dbNamesWithoutRestore = [data.dbName];
|
|
||||||
state.dbNamesSelected = [data.dbName];
|
|
||||||
state.form.id = data.id;
|
|
||||||
state.form.dbName = data.dbName;
|
|
||||||
state.form.intervalDay = data.intervalDay;
|
|
||||||
state.form.pointInTime = data.pointInTime;
|
|
||||||
state.form.startTime = data.startTime;
|
|
||||||
state.form.dbBackupId = data.dbBackupId;
|
|
||||||
state.form.dbBackupHistoryId = data.dbBackupHistoryId;
|
|
||||||
state.form.dbBackupHistoryName = data.dbBackupHistoryName;
|
|
||||||
if (data.pointInTime) {
|
|
||||||
state.restoreMode = 'point-in-time';
|
|
||||||
} else {
|
|
||||||
state.restoreMode = 'backup-history';
|
|
||||||
}
|
|
||||||
state.history = {
|
|
||||||
dbBackupId: data.dbBackupId,
|
|
||||||
id: data.dbBackupHistoryId,
|
|
||||||
name: data.dbBackupHistoryName,
|
|
||||||
createTime: data.createTime,
|
|
||||||
};
|
|
||||||
await getBackupHistories(props.dbId, data.dbName);
|
|
||||||
} else {
|
|
||||||
state.form.dbName = '';
|
|
||||||
state.editOrCreate = false;
|
|
||||||
state.form.intervalDay = 0;
|
|
||||||
state.form.repeated = false;
|
|
||||||
state.form.pointInTime = new Date();
|
|
||||||
state.form.startTime = new Date();
|
|
||||||
state.histories = [];
|
|
||||||
state.history = null;
|
|
||||||
state.restoreMode = 'point-in-time';
|
|
||||||
await getDbNamesWithoutRestore();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDbNamesWithoutRestore = async () => {
|
|
||||||
if (props.dbId > 0) {
|
|
||||||
state.dbNamesWithoutRestore = await dbApi.getDbNamesWithoutRestore.request({ dbId: props.dbId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const btnOk = async () => {
|
|
||||||
restoreForm.value.validate(async (valid: any) => {
|
|
||||||
if (valid) {
|
|
||||||
await ElMessageBox.confirm(`确定恢复数据库吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (state.restoreMode == 'point-in-time') {
|
|
||||||
state.form.dbBackupId = 0;
|
|
||||||
state.form.dbBackupHistoryId = 0;
|
|
||||||
state.form.dbBackupHistoryName = '';
|
|
||||||
} else {
|
|
||||||
state.form.pointInTime = null;
|
|
||||||
}
|
|
||||||
state.form.repeated = false;
|
|
||||||
state.form.intervalDay = 0;
|
|
||||||
const reqForm = { ...state.form };
|
|
||||||
let api = dbApi.createDbRestore;
|
|
||||||
if (props.data) {
|
|
||||||
api = dbApi.saveDbRestore;
|
|
||||||
}
|
|
||||||
api.request(reqForm).then(() => {
|
|
||||||
ElMessage.success('成功创建数据库恢复任务');
|
|
||||||
emit('val-change', state.form);
|
|
||||||
state.btnLoading = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
state.btnLoading = false;
|
|
||||||
}, 1000);
|
|
||||||
cancel();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请正确填写信息');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
visible.value = false;
|
|
||||||
emit('cancel');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBackupHistories = async (dbId: Number, dbName: String) => {
|
|
||||||
if (!dbId || !dbName) {
|
|
||||||
state.histories = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = await dbApi.getDbBackupHistories.request({ dbId, dbName });
|
|
||||||
if (!data || !data.list) {
|
|
||||||
ElMessage.error('该数据库没有备份记录,无法创建数据库恢复任务');
|
|
||||||
state.histories = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.histories = data.list;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="db-restore">
|
|
||||||
<page-table
|
|
||||||
height="100%"
|
|
||||||
ref="pageTableRef"
|
|
||||||
:page-api="dbApi.getDbRestores"
|
|
||||||
:show-selection="true"
|
|
||||||
v-model:selection-data="state.selectedData"
|
|
||||||
:searchItems="searchItems"
|
|
||||||
:before-query-fn="beforeQueryFn"
|
|
||||||
v-model:query-form="query"
|
|
||||||
:columns="columns"
|
|
||||||
>
|
|
||||||
<template #dbSelect>
|
|
||||||
<el-select v-model="query.dbName" placeholder="请选择数据库" style="width: 200px" filterable clearable>
|
|
||||||
<el-option v-for="item in dbNames" :key="item" :label="`${item}`" :value="item"> </el-option>
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #tableHeader>
|
|
||||||
<el-button type="primary" icon="plus" @click="createDbRestore()">添加</el-button>
|
|
||||||
<el-button type="primary" icon="video-play" @click="enableDbRestore(null)">启用</el-button>
|
|
||||||
<el-button type="primary" icon="video-pause" @click="disableDbRestore(null)">禁用</el-button>
|
|
||||||
<el-button type="danger" icon="delete" @click="deleteDbRestore(null)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #action="{ data }">
|
|
||||||
<el-button @click="showDbRestore(data)" type="primary" link>详情</el-button>
|
|
||||||
<el-button @click="enableDbRestore(data)" v-if="!data.enabled" type="primary" link>启用</el-button>
|
|
||||||
<el-button @click="disableDbRestore(data)" v-if="data.enabled" type="primary" link>禁用</el-button>
|
|
||||||
<el-button @click="deleteDbRestore(data)" type="danger" link>删除</el-button>
|
|
||||||
</template>
|
|
||||||
</page-table>
|
|
||||||
|
|
||||||
<db-restore-edit
|
|
||||||
@val-change="search"
|
|
||||||
:title="dbRestoreEditDialog.title"
|
|
||||||
:dbId="dbId"
|
|
||||||
:dbNames="dbNames"
|
|
||||||
:data="dbRestoreEditDialog.data"
|
|
||||||
v-model:visible="dbRestoreEditDialog.visible"
|
|
||||||
></db-restore-edit>
|
|
||||||
|
|
||||||
<el-dialog v-model="infoDialog.visible" title="数据库恢复">
|
|
||||||
<el-descriptions :column="1" border>
|
|
||||||
<el-descriptions-item :span="1" label="数据库名称">{{ infoDialog.data.dbName }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item v-if="infoDialog.data.pointInTime" :span="1" label="恢复时间点">{{
|
|
||||||
formatDate(infoDialog.data.pointInTime)
|
|
||||||
}}</el-descriptions-item>
|
|
||||||
<el-descriptions-item v-if="!infoDialog.data.pointInTime" :span="1" label="数据库备份">{{
|
|
||||||
infoDialog.data.dbBackupHistoryName
|
|
||||||
}}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="开始时间">{{ formatDate(infoDialog.data.startTime) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="是否启用">{{ infoDialog.data.enabledDesc }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="执行时间">{{ formatDate(infoDialog.data.lastTime) }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item :span="1" label="执行结果">{{ infoDialog.data.lastResult }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { toRefs, reactive, defineAsyncComponent, Ref, ref } from 'vue';
|
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
|
||||||
import { TableColumn } from '@/components/pagetable';
|
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
||||||
import { formatDate } from '@/common/utils/format';
|
|
||||||
const DbRestoreEdit = defineAsyncComponent(() => import('./DbRestoreEdit.vue'));
|
|
||||||
const pageTableRef: Ref<any> = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
dbId: {
|
|
||||||
type: [Number],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
dbNames: {
|
|
||||||
type: [Array<String>],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// const queryConfig = [TableQuery.slot('dbName', '数据库名称', 'dbSelect')];
|
|
||||||
const searchItems = [SearchItem.slot('dbName', '数据库名称', 'dbSelect')];
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
TableColumn.new('dbName', '数据库名称'),
|
|
||||||
TableColumn.new('startTime', '启动时间').isTime(),
|
|
||||||
TableColumn.new('enabledDesc', '是否启用'),
|
|
||||||
TableColumn.new('lastTime', '执行时间').isTime(),
|
|
||||||
TableColumn.new('lastResult', '执行结果'),
|
|
||||||
TableColumn.new('action', '操作').isSlot().setMinWidth(220).fixedRight().alignCenter(),
|
|
||||||
];
|
|
||||||
|
|
||||||
const emptyQuery = {
|
|
||||||
dbId: props.dbId,
|
|
||||||
dbName: '',
|
|
||||||
pageNum: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
repeated: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
data: [],
|
|
||||||
total: 0,
|
|
||||||
query: emptyQuery,
|
|
||||||
dbRestoreEditDialog: {
|
|
||||||
visible: false,
|
|
||||||
data: null as any,
|
|
||||||
title: '创建数据库恢复任务',
|
|
||||||
},
|
|
||||||
infoDialog: {
|
|
||||||
visible: false,
|
|
||||||
data: null as any,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* 选中的数据
|
|
||||||
*/
|
|
||||||
selectedData: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const { query, dbRestoreEditDialog, infoDialog } = toRefs(state);
|
|
||||||
|
|
||||||
const beforeQueryFn = (query: any) => {
|
|
||||||
query.dbId = props.dbId;
|
|
||||||
return query;
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = async () => {
|
|
||||||
await pageTableRef.value.search();
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDbRestore = async () => {
|
|
||||||
state.dbRestoreEditDialog.data = null;
|
|
||||||
state.dbRestoreEditDialog.title = '数据库恢复';
|
|
||||||
state.dbRestoreEditDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDbRestore = async (data: any) => {
|
|
||||||
let restoreId: string;
|
|
||||||
if (data) {
|
|
||||||
restoreId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要删除的数据库恢复任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ElMessageBox.confirm(`确定删除 “数据库恢复任务” 吗?`, '提示', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
});
|
|
||||||
await dbApi.deleteDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const showDbRestore = async (data: any) => {
|
|
||||||
state.infoDialog.data = data;
|
|
||||||
state.infoDialog.visible = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableDbRestore = async (data: any) => {
|
|
||||||
let restoreId: string;
|
|
||||||
if (data) {
|
|
||||||
restoreId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要启用的数据库恢复任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.enableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('启用成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableDbRestore = async (data: any) => {
|
|
||||||
let restoreId: string;
|
|
||||||
if (data) {
|
|
||||||
restoreId = data.id;
|
|
||||||
} else if (state.selectedData.length > 0) {
|
|
||||||
restoreId = state.selectedData.map((x: any) => x.id).join(' ');
|
|
||||||
} else {
|
|
||||||
ElMessage.error('请选择需要禁用的数据库恢复任务');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await dbApi.disableDbRestore.request({ dbId: props.dbId, restoreId: restoreId });
|
|
||||||
await search();
|
|
||||||
ElMessage.success('禁用成功');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style lang="scss"></style>
|
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
lazy
|
lazy
|
||||||
>
|
>
|
||||||
<template #tableHeader>
|
<template #tableHeader>
|
||||||
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)">{{ $t('common.create') }}</el-button>
|
<el-button v-auth="perms.saveInstance" type="primary" icon="plus" @click="editInstance(false)" plain>{{ $t('common.create') }}</el-button>
|
||||||
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete">
|
<el-button v-auth="perms.delInstance" :disabled="selectionData.length < 1" @click="deleteInstance()" type="danger" icon="delete" plain>
|
||||||
{{ $t('common.delete') }}
|
{{ $t('common.delete') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -59,36 +59,13 @@ export const dbApi = {
|
|||||||
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
enableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/enable'),
|
||||||
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
disableDbRestore: Api.newPut('/dbs/{dbId}/restores/{restoreId}/disable'),
|
||||||
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
|
saveDbRestore: Api.newPut('/dbs/{dbId}/restores/{id}'),
|
||||||
|
|
||||||
// 数据同步相关
|
|
||||||
datasyncTasks: Api.newGet('/datasync/tasks'),
|
|
||||||
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
|
|
||||||
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
|
|
||||||
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
|
|
||||||
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
|
|
||||||
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
|
|
||||||
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
|
||||||
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
|
||||||
|
|
||||||
// 数据库迁移相关
|
|
||||||
dbTransferTasks: Api.newGet('/dbTransfer'),
|
|
||||||
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
|
|
||||||
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
|
|
||||||
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
|
|
||||||
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
|
|
||||||
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
|
|
||||||
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
|
|
||||||
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
|
|
||||||
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
|
|
||||||
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
|
|
||||||
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dbSqlExecApi = {
|
export const dbSqlExecApi = {
|
||||||
// 根据业务key获取sql执行信息
|
// 根据业务key获取sql执行信息
|
||||||
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
getSqlExecByBizKey: Api.newGet('/dbs/sql-execs'),
|
||||||
};
|
};
|
||||||
const encryptField = async (param: any, field: string) => {
|
export const encryptField = async (param: any, field: string) => {
|
||||||
// sql编码处理
|
// sql编码处理
|
||||||
if (!param['_encrypted'] && param[field]) {
|
if (!param['_encrypted'] && param[field]) {
|
||||||
// 判断是开发环境就打印sql
|
// 判断是开发环境就打印sql
|
||||||
|
|||||||
@@ -1,31 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<TagTreeResourceSelect
|
<ResourceSelect v-bind="$attrs" v-model="selectNode" @change="changeNode" :resource-type="TagResourceTypePath.Db" :tag-path-node-type="NodeTypeDbInst">
|
||||||
v-bind="$attrs"
|
|
||||||
v-model="selectNode"
|
|
||||||
@change="changeNode"
|
|
||||||
:resource-type="TagResourceTypePath.Db"
|
|
||||||
:tag-path-node-type="NodeTypeTagPath"
|
|
||||||
>
|
|
||||||
<template #iconPrefix>
|
<template #iconPrefix>
|
||||||
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
<SvgIcon v-if="dbType && getDbDialect(dbType)" :name="getDbDialect(dbType).getInfo().icon" :size="18" />
|
||||||
</template>
|
</template>
|
||||||
<template #prefix="{ data }">
|
</ResourceSelect>
|
||||||
<SvgIcon v-if="data.type.value == SqlExecNodeType.DbInst" :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
|
||||||
<SvgIcon v-if="data.icon" :name="data.icon.name" :color="data.icon.color" />
|
|
||||||
</template>
|
|
||||||
</TagTreeResourceSelect>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
|
||||||
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
import { NodeType, TagTreeNode } from '@/views/ops/component/tag';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
import { sleep } from '@/common/utils/loading';
|
import { sleep } from '@/common/utils/loading';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import { getDbDialect, schemaDbTypes } from '@/views/ops/db/dialect';
|
||||||
import { getDbDialect, noSchemaTypes } from '@/views/ops/db/dialect';
|
import ResourceSelect from '@/views/ops/resource/ResourceSelect.vue';
|
||||||
import TagTreeResourceSelect from '../../component/TagTreeResourceSelect.vue';
|
import NodeDbInst from '@/views/ops/db/resource/NodeDbInst.vue';
|
||||||
import { computed } from 'vue';
|
import NodeDb from '@/views/ops/db/resource/NodeDb.vue';
|
||||||
import { DbInst } from '../db';
|
import { DbIcon, SchemaIcon } from '@/views/ops/db/resource';
|
||||||
|
import { DbInst } from '@/views/ops/db/db';
|
||||||
|
|
||||||
const dbId = defineModel<number>('dbId');
|
const dbId = defineModel<number>('dbId');
|
||||||
const instName = defineModel<string>('instName');
|
const instName = defineModel<string>('instName');
|
||||||
@@ -35,20 +27,6 @@ const dbType = defineModel<string>('dbType');
|
|||||||
|
|
||||||
const emits = defineEmits(['selectDb']);
|
const emits = defineEmits(['selectDb']);
|
||||||
|
|
||||||
/**
|
|
||||||
* 树节点类型
|
|
||||||
*/
|
|
||||||
class SqlExecNodeType {
|
|
||||||
static DbInst = 1;
|
|
||||||
static Db = 2;
|
|
||||||
static TableMenu = 3;
|
|
||||||
static SqlMenu = 4;
|
|
||||||
static Table = 5;
|
|
||||||
static Sql = 6;
|
|
||||||
static PgSchemaMenu = 7;
|
|
||||||
static PgSchema = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectNode = computed({
|
const selectNode = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
|
return dbName.value ? `${tagPath.value} > ${instName.value} > ${dbName.value}` : '';
|
||||||
@@ -58,90 +36,94 @@ const selectNode = computed({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const DbIcon = {
|
const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
name: 'Coin',
|
const tagPath = parentNode.key;
|
||||||
color: '#67c23a',
|
|
||||||
};
|
|
||||||
|
|
||||||
// pgsql schema icon
|
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
|
||||||
const SchemaIcon = {
|
const dbInstances = dbInstancesRes.list;
|
||||||
name: 'List',
|
if (!dbInstances) {
|
||||||
color: '#67c23a',
|
|
||||||
};
|
|
||||||
|
|
||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
|
||||||
const dbInfoRes = await dbApi.dbs.request({ tagPath: parentNode.key });
|
|
||||||
const dbInfos = dbInfoRes.list;
|
|
||||||
if (!dbInfos) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防止过快加载会出现一闪而过,对眼睛不好
|
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
return dbInfos?.map((x: any) => {
|
return dbInstances?.map((x: any) => {
|
||||||
x.tagPath = parentNode.key;
|
x.tagPath = tagPath;
|
||||||
return new TagTreeNode(`${parentNode.key}.${x.id}`, x.name, NodeTypeDbInst).withParams(x);
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/** mysql类型的数据库,没有schema层 */
|
const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const noSchemaType = (type: string) => {
|
const params = parentNode.params;
|
||||||
return noSchemaTypes.includes(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 数据库实例节点类型
|
const tagPath = params.tagPath;
|
||||||
const NodeTypeDbInst = new NodeType(SqlExecNodeType.DbInst).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
const authCerts = {} as any;
|
||||||
|
for (let authCert of params.authCerts) {
|
||||||
|
authCerts[authCert.name] = authCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbInfoRes = await dbApi.dbs.request({
|
||||||
|
tagPath: `${tagPath}${TagResourceTypeEnum.DbInstance.value}|${params.code}`,
|
||||||
|
});
|
||||||
|
const dbInfos = dbInfoRes.list;
|
||||||
|
if (!dbInfos) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbInfos?.map((x: any) => {
|
||||||
|
x.tagPath = tagPath;
|
||||||
|
x.username = authCerts[x.authCertName]?.username;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据库列表名类型
|
||||||
|
const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
const dbs = (await DbInst.getDbNames(params))?.sort();
|
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||||
let fn: NodeType;
|
const hasSchema = schemaDbTypes.includes(params.type);
|
||||||
if (noSchemaType(params.type)) {
|
const nodeType = hasSchema ? NodeTypeDbSchema : NodeTypeNoSchemaDb;
|
||||||
fn = MysqlNodeTypes;
|
|
||||||
} else {
|
|
||||||
fn = PgNodeTypes;
|
|
||||||
}
|
|
||||||
return dbs.map((x: any) => {
|
return dbs.map((x: any) => {
|
||||||
let tagTreeNode = new TagTreeNode(`${parentNode.key}.${x}`, `${x}`, fn)
|
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, nodeType)
|
||||||
.withParams({
|
.withParams({
|
||||||
tagPath: params.tagPath,
|
tagPath: params.tagPath,
|
||||||
id: params.id,
|
id: params.id,
|
||||||
code: params.code,
|
|
||||||
instanceId: params.instanceId,
|
|
||||||
name: params.name,
|
name: params.name,
|
||||||
type: params.type,
|
type: params.type,
|
||||||
host: `${params.host}:${params.port}`,
|
host: `${params.host}:${params.port}`,
|
||||||
dbs: dbs,
|
dbs: dbs,
|
||||||
db: x,
|
db: x,
|
||||||
|
code: params.code,
|
||||||
})
|
})
|
||||||
.withIcon(DbIcon);
|
.withIcon(DbIcon)
|
||||||
if (noSchemaType(params.type)) {
|
.withIsLeaf(!hasSchema);
|
||||||
tagTreeNode.isLeaf = true;
|
|
||||||
}
|
|
||||||
return tagTreeNode;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 数据库节点
|
// 数据库节点
|
||||||
const PgNodeTypes = new NodeType(SqlExecNodeType.Db).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
const NodeTypeDbSchema = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
// pg类数据库会多一层schema
|
|
||||||
const params = parentNode.params;
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
const { id, db } = params;
|
const { id, db } = params;
|
||||||
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||||
|
const dbs = schemaNames.map((x: any) => `${db}/${x}`);
|
||||||
return schemaNames.map((sn: any) => {
|
return schemaNames.map((sn: any) => {
|
||||||
// 将db变更为 db/schema;
|
// 将db变更为 db/schema;
|
||||||
const nParams = { ...params };
|
const nParams = { ...params };
|
||||||
nParams.schema = sn;
|
nParams.schema = sn;
|
||||||
nParams.db = nParams.db + '/' + sn;
|
nParams.db = nParams.db + '/' + sn;
|
||||||
nParams.dbs = schemaNames;
|
nParams.dbs = dbs;
|
||||||
let tagTreeNode = new TagTreeNode(`${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema).withParams(nParams).withIcon(SchemaIcon);
|
return TagTreeNode.new(parentNode, `${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema)
|
||||||
tagTreeNode.isLeaf = true;
|
.withParams(nParams)
|
||||||
return tagTreeNode;
|
.withIcon(SchemaIcon)
|
||||||
|
.withIsLeaf(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const MysqlNodeTypes = new NodeType(SqlExecNodeType.Db);
|
|
||||||
|
|
||||||
// postgres schema模式
|
// postgres schema模式
|
||||||
const NodeTypePostgresSchema = new NodeType(SqlExecNodeType.PgSchema);
|
const NodeTypePostgresSchema = new NodeType(99);
|
||||||
|
const NodeTypeNoSchemaDb = new NodeType(99);
|
||||||
|
|
||||||
const changeNode = (nodeData: TagTreeNode) => {
|
const changeNode = (nodeData: TagTreeNode) => {
|
||||||
const params = nodeData.params;
|
const params = nodeData.params;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-row>
|
<el-row>
|
||||||
<span v-if="dt.hasUpdatedFileds" class="mt-1">
|
<span v-if="dt.hasUpdatedFields" class="mt-1">
|
||||||
<span>
|
<span>
|
||||||
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
<el-link type="success" underline="never" @click="submitUpdateFields(dt)"
|
||||||
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
><span style="font-size: 12px">{{ $t('common.submit') }}</span></el-link
|
||||||
@@ -110,6 +110,7 @@
|
|||||||
:data="dt.data"
|
:data="dt.data"
|
||||||
:table="dt.table"
|
:table="dt.table"
|
||||||
:columns="dt.tableColumn"
|
:columns="dt.tableColumn"
|
||||||
|
:column-more-actions="['fixed']"
|
||||||
:loading="dt.loading"
|
:loading="dt.loading"
|
||||||
:abort-fn="dt.abortFn"
|
:abort-fn="dt.abortFn"
|
||||||
:height="tableDataHeight"
|
:height="tableDataHeight"
|
||||||
@@ -199,7 +200,7 @@ class ExecResTab {
|
|||||||
/**
|
/**
|
||||||
* 是否有更新字段
|
* 是否有更新字段
|
||||||
*/
|
*/
|
||||||
hasUpdatedFileds: boolean;
|
hasUpdatedFields: boolean;
|
||||||
|
|
||||||
errorMsg: string;
|
errorMsg: string;
|
||||||
|
|
||||||
@@ -305,13 +306,8 @@ const getKey = () => {
|
|||||||
* 执行sql
|
* 执行sql
|
||||||
*/
|
*/
|
||||||
const onRunSql = async (newTab = false) => {
|
const onRunSql = async (newTab = false) => {
|
||||||
// 没有选中的文本,则为全部文本
|
const sqls = getSql();
|
||||||
let sql = getSql() as string;
|
notBlank(sqls, t('db.noSelectRunSqlMsg'));
|
||||||
notBlank(sql && sql.trim(), t('db.noSelctRunSqlTips'));
|
|
||||||
// 去除字符串前的空格、换行等
|
|
||||||
sql = sql.replace(/(^\s*)/g, '');
|
|
||||||
|
|
||||||
const sqls = splitSql(sql);
|
|
||||||
|
|
||||||
if (sqls.length == 1) {
|
if (sqls.length == 1) {
|
||||||
const oneSql = sqls[0];
|
const oneSql = sqls[0];
|
||||||
@@ -522,11 +518,56 @@ const runSql = async (sql: string, remark = '', newTab = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function splitSql(sql: string, delimiter: string = ';') {
|
/**
|
||||||
|
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回当前光标附近的sql
|
||||||
|
*/
|
||||||
|
const getSql = (): string[] => {
|
||||||
|
// 编辑器还没初始化
|
||||||
|
if (!monacoEditor?.getModel()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = '' as string | undefined;
|
||||||
|
// 选择选中的sql
|
||||||
|
let selection = monacoEditor.getSelection();
|
||||||
|
if (selection) {
|
||||||
|
sql = monacoEditor.getModel()?.getValueInRange(selection);
|
||||||
|
sql = sql?.replace(/(^\s*)/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有选中的内容且不为空,直接返回
|
||||||
|
if (sql && sql.trim()) {
|
||||||
|
return splitSqlStatements(sql).map((x) => x.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有选中任何内容时,自动选择当前光标所在的SQL语句行
|
||||||
|
const currentPosition = monacoEditor.getPosition();
|
||||||
|
if (currentPosition) {
|
||||||
|
const model = monacoEditor.getModel();
|
||||||
|
if (model) {
|
||||||
|
const fullSql = model.getValue();
|
||||||
|
const sqlStatement = getCurrentStatement(fullSql, currentPosition, model);
|
||||||
|
if (sqlStatement) {
|
||||||
|
return [sqlStatement];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用SQL解析器,用于提取SQL语句及其位置信息
|
||||||
|
* @param sql 完整的SQL文本
|
||||||
|
* @param delimiter SQL语句分隔符,默认为分号
|
||||||
|
* @param withPosition 是否需要返回位置信息
|
||||||
|
*/
|
||||||
|
function splitSqlStatements(sql: string, delimiter: string = ';') {
|
||||||
let state = 'normal';
|
let state = 'normal';
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
let result = [];
|
let result = [];
|
||||||
let inString = null; // 用于记录当前字符串的引号类型(' 或 ")
|
let inString = null; // 用于记录当前字符串的引号类型(' 或 ")
|
||||||
|
let startPos = 0;
|
||||||
|
|
||||||
for (let i = 0; i < sql.length; i++) {
|
for (let i = 0; i < sql.length; i++) {
|
||||||
const char = sql[i];
|
const char = sql[i];
|
||||||
@@ -535,9 +576,11 @@ function splitSql(sql: string, delimiter: string = ';') {
|
|||||||
if (state === 'normal') {
|
if (state === 'normal') {
|
||||||
if (char === '-' && nextChar === '-') {
|
if (char === '-' && nextChar === '-') {
|
||||||
state = 'singleLineComment';
|
state = 'singleLineComment';
|
||||||
|
// buffer += char + nextChar;
|
||||||
i++; // 跳过下一个字符
|
i++; // 跳过下一个字符
|
||||||
} else if (char === '/' && nextChar === '*') {
|
} else if (char === '/' && nextChar === '*') {
|
||||||
state = 'multiLineComment';
|
state = 'multiLineComment';
|
||||||
|
// buffer += char + nextChar;
|
||||||
i++; // 跳过下一个字符
|
i++; // 跳过下一个字符
|
||||||
} else if (char === "'" || char === '"') {
|
} else if (char === "'" || char === '"') {
|
||||||
state = 'string';
|
state = 'string';
|
||||||
@@ -545,9 +588,14 @@ function splitSql(sql: string, delimiter: string = ';') {
|
|||||||
buffer += char;
|
buffer += char;
|
||||||
} else if (char === delimiter) {
|
} else if (char === delimiter) {
|
||||||
if (buffer.trim()) {
|
if (buffer.trim()) {
|
||||||
result.push(buffer.trim());
|
result.push({
|
||||||
|
text: buffer.trim(),
|
||||||
|
start: startPos,
|
||||||
|
end: i,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
buffer = '';
|
buffer = '';
|
||||||
|
startPos = i + 1;
|
||||||
} else {
|
} else {
|
||||||
buffer += char;
|
buffer += char;
|
||||||
}
|
}
|
||||||
@@ -562,45 +610,70 @@ function splitSql(sql: string, delimiter: string = ';') {
|
|||||||
inString = null;
|
inString = null;
|
||||||
}
|
}
|
||||||
} else if (state === 'singleLineComment') {
|
} else if (state === 'singleLineComment') {
|
||||||
|
// buffer += char;
|
||||||
if (char === '\n') {
|
if (char === '\n') {
|
||||||
state = 'normal';
|
state = 'normal';
|
||||||
}
|
}
|
||||||
} else if (state === 'multiLineComment') {
|
} else if (state === 'multiLineComment') {
|
||||||
|
// buffer += char;
|
||||||
if (char === '*' && nextChar === '/') {
|
if (char === '*' && nextChar === '/') {
|
||||||
|
buffer += nextChar;
|
||||||
state = 'normal';
|
state = 'normal';
|
||||||
i++; // 跳过下一个字符
|
i++; // 跳过下一个字符
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理最后一个语句(没有以分号结尾的情况)
|
||||||
if (buffer.trim()) {
|
if (buffer.trim()) {
|
||||||
result.push(buffer.trim());
|
result.push({
|
||||||
|
text: buffer.trim(),
|
||||||
|
start: startPos,
|
||||||
|
end: sql.length,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取sql,如果有鼠标选中,则返回选中内容,否则返回输入框内所有内容
|
* 获取光标所在的SQL语句
|
||||||
|
* @param fullSql 完整的SQL文本
|
||||||
|
* @param position 光标位置
|
||||||
|
* @param model Monaco编辑器模型
|
||||||
*/
|
*/
|
||||||
const getSql = () => {
|
function getCurrentStatement(fullSql: string, position: monaco.Position, model: monaco.editor.ITextModel): string | null {
|
||||||
let res = '' as string | undefined;
|
// 使用通用SQL解析器来分割SQL语句,并记录每个语句的位置
|
||||||
// 编辑器还没初始化
|
const statements: { text: string; start: number; end: number }[] = splitSqlStatements(fullSql);
|
||||||
if (!monacoEditor?.getModel()) {
|
|
||||||
return res;
|
// 根据光标位置找到对应的SQL语句
|
||||||
}
|
if (position) {
|
||||||
// 选择选中的sql
|
const offset = model.getOffsetAt(position);
|
||||||
let selection = monacoEditor.getSelection();
|
|
||||||
if (selection) {
|
// 遍历所有语句,找到光标所在的语句
|
||||||
res = monacoEditor.getModel()?.getValueInRange(selection);
|
for (let i = 0; i < statements.length; i++) {
|
||||||
|
const stmt = statements[i];
|
||||||
|
// 光标在语句范围内(包括末尾分号)
|
||||||
|
if (offset >= stmt.start && offset <= stmt.end) {
|
||||||
|
return stmt.text;
|
||||||
|
}
|
||||||
|
// 光标在语句分号后一个位置
|
||||||
|
if (offset === stmt.end + 1) {
|
||||||
|
return stmt.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果光标处没有SQL,则执行光标前的最后一个SQL
|
||||||
|
for (let i = statements.length - 1; i >= 0; i--) {
|
||||||
|
const stmt = statements[i];
|
||||||
|
if (offset > stmt.end) {
|
||||||
|
return stmt.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 整个编辑器的sql
|
return null;
|
||||||
if (!res) {
|
}
|
||||||
return monacoEditor.getModel()?.getValue();
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSql = async () => {
|
const saveSql = async () => {
|
||||||
const sql = monacoEditor.getModel()?.getValue();
|
const sql = monacoEditor.getModel()?.getValue();
|
||||||
@@ -710,7 +783,7 @@ const getUploadSqlFileUrl = () => {
|
|||||||
|
|
||||||
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
const changeUpdatedField = (updatedFields: any, dt: ExecResTab) => {
|
||||||
// 如果存在要更新字段,则显示提交和取消按钮
|
// 如果存在要更新字段,则显示提交和取消按钮
|
||||||
dt.hasUpdatedFileds = updatedFields && updatedFields.size > 0;
|
dt.hasUpdatedFields = updatedFields && updatedFields.size > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 字段名列 -->
|
<!-- 字段名列 -->
|
||||||
<div v-else @contextmenu="headerContextmenuClick($event, column)" style="position: relative">
|
<div v-else style="position: relative" @mouseenter="showColumnAction(column)" @mouseleave="hideColumnAction">
|
||||||
<!-- 字段列的数据类型 -->
|
<!-- 字段列的数据类型 -->
|
||||||
<div class="column-type">
|
<div class="column-type">
|
||||||
<span v-if="column.dataTypeSubscript === 'icon-clock'">
|
<span v-if="column.dataTypeSubscript === 'icon-clock'">
|
||||||
@@ -65,9 +65,57 @@
|
|||||||
|
|
||||||
<!-- 字段列右部分内容 -->
|
<!-- 字段列右部分内容 -->
|
||||||
<div class="column-right">
|
<div class="column-right">
|
||||||
<span v-if="column.title == nowSortColumn?.columnName">
|
<el-dropdown
|
||||||
<SvgIcon color="var(--el-color-primary)" :name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"></SvgIcon>
|
@command="handleColumnCommand(column, $event)"
|
||||||
</span>
|
@visibleChange="onColumnActionVisibleChange(column, $event)"
|
||||||
|
trigger="click"
|
||||||
|
v-if="column.key !== rowNoColumn.key"
|
||||||
|
size="small"
|
||||||
|
placement="bottom-start"
|
||||||
|
>
|
||||||
|
<span class="column-actions-trigger">
|
||||||
|
<!-- 排序箭头图标 -->
|
||||||
|
<SvgIcon
|
||||||
|
v-if="
|
||||||
|
column.title == nowSortColumn?.columnName &&
|
||||||
|
!showColumnActions[column.key] &&
|
||||||
|
!columnActionVisible[column.key]
|
||||||
|
"
|
||||||
|
:color="'var(--el-color-primary)'"
|
||||||
|
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
|
||||||
|
:size="14"
|
||||||
|
/>
|
||||||
|
<!-- 更多操作图标 -->
|
||||||
|
<SvgIcon
|
||||||
|
v-if="columnActionVisible[column.key] || showColumnActions[column.key]"
|
||||||
|
name="MoreFilled"
|
||||||
|
:size="14"
|
||||||
|
:color="'var(--el-color-primary)'"
|
||||||
|
class="column-more-icon"
|
||||||
|
:class="{ 'column-more-icon-visible': columnActionVisible[column.key] || showColumnActions[column.key] }"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item v-if="showColumnActionSort" command="sort-asc">
|
||||||
|
<SvgIcon name="top" class="mr-1" />
|
||||||
|
{{ $t('db.asc') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="showColumnActionSort" command="sort-desc">
|
||||||
|
<SvgIcon name="bottom" class="mr-1" />
|
||||||
|
{{ $t('db.desc') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="showColumnActionFixed && !column.fixed" command="fix">
|
||||||
|
<SvgIcon name="Paperclip" class="mr-1" />
|
||||||
|
{{ $t('db.fixed') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item v-if="showColumnActionFixed && column.fixed" command="unfix">
|
||||||
|
<SvgIcon name="Minus" class="mr-1" />
|
||||||
|
{{ $t('db.cancelFiexd') }}
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,13 +201,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch, Ref } from 'vue';
|
import { onBeforeUnmount, onMounted, reactive, ref, toRefs, watch, Ref, computed } from 'vue';
|
||||||
import { ElInput, ElMessage } from 'element-plus';
|
import { ElInput, ElMessage } from 'element-plus';
|
||||||
import { copyToClipboard } from '@/common/utils/string';
|
import { copyToClipboard } from '@/common/utils/string';
|
||||||
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
import { DbInst, DbThemeConfig } from '@/views/ops/db/db';
|
||||||
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
import SvgIcon from '@/components/svgIcon/index.vue';
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
import { exportCsv, exportFile } from '@/common/utils/export';
|
import { exportCsv, exportExcel, exportFile } from '@/common/utils/export';
|
||||||
import { formatDate } from '@/common/utils/format';
|
import { formatDate } from '@/common/utils/format';
|
||||||
import { useIntervalFn, useStorage } from '@vueuse/core';
|
import { useIntervalFn, useStorage } from '@vueuse/core';
|
||||||
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
import { ColumnTypeSubscript, DataType, DbDialect, getDbDialect } from '../../dialect/index';
|
||||||
@@ -190,6 +238,10 @@ const props = defineProps({
|
|||||||
columns: {
|
columns: {
|
||||||
type: Array<any>,
|
type: Array<any>,
|
||||||
},
|
},
|
||||||
|
columnMoreActions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['sort', 'fixed'],
|
||||||
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -214,43 +266,9 @@ const props = defineProps({
|
|||||||
const contextmenuRef = ref();
|
const contextmenuRef = ref();
|
||||||
const tableRef = ref();
|
const tableRef = ref();
|
||||||
|
|
||||||
/** 表头 menu items **/
|
// 用于控制列操作按钮的显示
|
||||||
|
const showColumnActions = ref({} as any);
|
||||||
const cmHeaderAsc = new ContextmenuItem('asc', 'db.asc')
|
const columnActionVisible = ref({} as any);
|
||||||
.withIcon('top')
|
|
||||||
.withOnClick((data: any) => {
|
|
||||||
onTableSortChange({ columnName: data.dataKey, order: 'asc' });
|
|
||||||
})
|
|
||||||
.withHideFunc(() => !props.showColumnTip);
|
|
||||||
|
|
||||||
const cmHeaderDesc = new ContextmenuItem('desc', 'db.desc')
|
|
||||||
.withIcon('bottom')
|
|
||||||
.withOnClick((data: any) => {
|
|
||||||
onTableSortChange({ columnName: data.dataKey, order: 'desc' });
|
|
||||||
})
|
|
||||||
.withHideFunc(() => !props.showColumnTip);
|
|
||||||
|
|
||||||
const cmHeaderFixed = new ContextmenuItem('fixed', 'db.fixed')
|
|
||||||
.withIcon('Paperclip')
|
|
||||||
.withOnClick((data: any) => {
|
|
||||||
state.columns.forEach((column: any) => {
|
|
||||||
if (column.dataKey == data.dataKey) {
|
|
||||||
column.fixed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.withHideFunc((data: any) => data.fixed);
|
|
||||||
|
|
||||||
const cmHeaderCancelFixed = new ContextmenuItem('cancelFixed', 'db.cancelFiexd')
|
|
||||||
.withIcon('Minus')
|
|
||||||
.withOnClick((data: any) => {
|
|
||||||
state.columns.forEach((column: any) => {
|
|
||||||
if (column.dataKey == data.dataKey) {
|
|
||||||
column.fixed = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.withHideFunc((data: any) => !data.fixed);
|
|
||||||
|
|
||||||
/** 表数据 contextmenu items **/
|
/** 表数据 contextmenu items **/
|
||||||
|
|
||||||
@@ -287,6 +305,8 @@ const cmDataGenJson = new ContextmenuItem('genJson', 'db.genJson').withIcon('tic
|
|||||||
|
|
||||||
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv').withIcon('document').withOnClick(() => onExportCsv());
|
const cmDataExportCsv = new ContextmenuItem('exportCsv', 'db.exportCsv').withIcon('document').withOnClick(() => onExportCsv());
|
||||||
|
|
||||||
|
const cmDataExportExcel = new ContextmenuItem('exportExcel', 'db.exportExcel').withIcon('document').withOnClick(() => onExportExcel());
|
||||||
|
|
||||||
const cmDataExportSql = new ContextmenuItem('exportSql', 'db.exportSql')
|
const cmDataExportSql = new ContextmenuItem('exportSql', 'db.exportSql')
|
||||||
.withIcon('document')
|
.withIcon('document')
|
||||||
.withOnClick(() => onExportSql())
|
.withOnClick(() => onExportSql())
|
||||||
@@ -438,6 +458,16 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 显示列排序
|
||||||
|
const showColumnActionSort = computed(() => {
|
||||||
|
return props.columnMoreActions.includes('sort');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示列固定
|
||||||
|
const showColumnActionFixed = computed(() => {
|
||||||
|
return props.columnMoreActions.includes('fixed');
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log('in DbTable mounted');
|
console.log('in DbTable mounted');
|
||||||
state.tableHeight = props.height;
|
state.tableHeight = props.height;
|
||||||
@@ -508,6 +538,55 @@ const cancelLoading = async () => {
|
|||||||
endLoading();
|
endLoading();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示列操作按钮
|
||||||
|
*/
|
||||||
|
const showColumnAction = (column: any) => {
|
||||||
|
showColumnActions.value[column.key] = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏列操作按钮
|
||||||
|
*/
|
||||||
|
const hideColumnAction = () => {
|
||||||
|
showColumnActions.value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理列操作命令
|
||||||
|
*/
|
||||||
|
const handleColumnCommand = (column: any, command: string) => {
|
||||||
|
switch (command) {
|
||||||
|
case 'sort-asc':
|
||||||
|
onTableSortChange({ columnName: column.dataKey, order: 'asc' });
|
||||||
|
break;
|
||||||
|
case 'sort-desc':
|
||||||
|
onTableSortChange({ columnName: column.dataKey, order: 'desc' });
|
||||||
|
break;
|
||||||
|
case 'fix':
|
||||||
|
state.columns.forEach((col: any) => {
|
||||||
|
if (col.dataKey == column.dataKey) {
|
||||||
|
col.fixed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'unfix':
|
||||||
|
state.columns.forEach((col: any) => {
|
||||||
|
if (col.dataKey == column.dataKey) {
|
||||||
|
col.fixed = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 点击了取消固定等操作后,可能更多的icon还是显示在列上,所以需要重新置为空对象。暂时不懂是组件bug还是啥
|
||||||
|
columnActionVisible.value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onColumnActionVisibleChange = (column: any, visible: boolean) => {
|
||||||
|
columnActionVisible.value = {}; // 只显示一个列的更多icon
|
||||||
|
columnActionVisible.value[column.key] = visible;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前单元格是否允许编辑
|
* 当前单元格是否允许编辑
|
||||||
* @param rowIndex ri
|
* @param rowIndex ri
|
||||||
@@ -570,16 +649,6 @@ const rowEventHandlers = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerContextmenuClick = (event: any, data: any) => {
|
|
||||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
|
||||||
|
|
||||||
const { clientX, clientY } = event;
|
|
||||||
state.contextmenu.dropdown.x = clientX;
|
|
||||||
state.contextmenu.dropdown.y = clientY;
|
|
||||||
state.contextmenu.items = [cmHeaderAsc, cmHeaderDesc, cmHeaderFixed, cmHeaderCancelFixed];
|
|
||||||
contextmenuRef.value.openContextmenu(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: any) => {
|
const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: any) => {
|
||||||
event.preventDefault(); // 阻止默认的右击菜单行为
|
event.preventDefault(); // 阻止默认的右击菜单行为
|
||||||
|
|
||||||
@@ -590,7 +659,7 @@ const dataContextmenuClick = (event: any, rowIndex: number, column: any, data: a
|
|||||||
const { clientX, clientY } = event;
|
const { clientX, clientY } = event;
|
||||||
state.contextmenu.dropdown.x = clientX;
|
state.contextmenu.dropdown.x = clientX;
|
||||||
state.contextmenu.dropdown.y = clientY;
|
state.contextmenu.dropdown.y = clientY;
|
||||||
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportCsv, cmDataExportSql];
|
state.contextmenu.items = [cmDataCopyCell, cmDataDel, cmFormView, cmDataGenInsertSql, cmDataGenJson, cmDataExportExcel, cmDataExportCsv, cmDataExportSql];
|
||||||
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
contextmenuRef.value.openContextmenu({ column, rowData: data });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -671,6 +740,20 @@ const onExportCsv = () => {
|
|||||||
exportCsv(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}`, columnNames, dataList);
|
exportCsv(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}`, columnNames, dataList);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出当前页数据
|
||||||
|
*/
|
||||||
|
const onExportExcel = () => {
|
||||||
|
const dataList = state.datas as any;
|
||||||
|
let columnNames = [];
|
||||||
|
for (let column of state.columns) {
|
||||||
|
if (column.show) {
|
||||||
|
columnNames.push(column.columnName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exportExcel(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}`, [{ name: 'Data', columns: columnNames, datas: dataList }]);
|
||||||
|
};
|
||||||
|
|
||||||
const onExportSql = async () => {
|
const onExportSql = async () => {
|
||||||
const selectionDatas = state.datas;
|
const selectionDatas = state.datas;
|
||||||
exportFile(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}.sql`, await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas));
|
exportFile(`Data-${state.table}-${formatDate(new Date(), 'YYYYMMDDHHmm')}.sql`, await getNowDbInst().genInsertSql(state.db, state.table, selectionDatas));
|
||||||
@@ -851,6 +934,31 @@ defineExpose({
|
|||||||
top: 2px;
|
top: 2px;
|
||||||
right: 0;
|
right: 0;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-actions-trigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-more-icon {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-more-icon-visible {
|
||||||
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
>
|
>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-row :gutter="10" justify="left">
|
<el-row :gutter="10" justify="start">
|
||||||
<el-link class="op-page" underline="never" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" :title="$t('db.homePage')" />
|
<el-link class="op-page" underline="never" @click="pageNum = 1" :disabled="pageNum == 1" icon="DArrowLeft" :title="$t('db.homePage')" />
|
||||||
<el-link
|
<el-link
|
||||||
class="op-page"
|
class="op-page"
|
||||||
|
|||||||
@@ -128,10 +128,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, reactive, ref, toRefs, watch, useTemplateRef, nextTick } from 'vue';
|
import { computed, reactive, ref, toRefs, watch, useTemplateRef, nextTick, Ref } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import SqlExecBox from '../sqleditor/SqlExecBox';
|
import SqlExecBox from '../sqleditor/SqlExecBox';
|
||||||
import { DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
|
import { DbDialect, DbType, getDbDialect, IndexDefinition, RowDefinition } from '../../dialect/index';
|
||||||
import { DbInst } from '../../db';
|
import { DbInst } from '../../db';
|
||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -165,7 +165,7 @@ const props = defineProps({
|
|||||||
//定义事件
|
//定义事件
|
||||||
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
const emit = defineEmits(['update:visible', 'cancel', 'val-change', 'submit-sql']);
|
||||||
|
|
||||||
let dbDialect: any = computed(() => getDbDialect(props.dbType!, props.version));
|
let dbDialect: Ref<DbDialect> = computed(() => getDbDialect(props.dbType!, props.version));
|
||||||
|
|
||||||
type ColName = {
|
type ColName = {
|
||||||
prop: string;
|
prop: string;
|
||||||
|
|||||||
@@ -497,8 +497,8 @@ export class DbInst {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符、排序图标等
|
// 获取列名称的长度 加上排序图标长度、abc为字段类型简称占位符、更多/排序图标等
|
||||||
const columnWidth: number = getTextWidth(prop + 'abc') + 10;
|
const columnWidth: number = getTextWidth(prop + 'abc') + 25;
|
||||||
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
|
// prop为该列的字段名(传字符串);tableData为该表格的数据源(传变量);
|
||||||
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
|
if (!tableData || !tableData.length || tableData.length === 0 || tableData === undefined) {
|
||||||
return columnWidth;
|
return columnWidth;
|
||||||
@@ -506,8 +506,9 @@ export class DbInst {
|
|||||||
|
|
||||||
// 获取该列中最长的数据(内容)
|
// 获取该列中最长的数据(内容)
|
||||||
let maxWidthText = '';
|
let maxWidthText = '';
|
||||||
|
const length = tableData.length > 10 ? 10 : tableData.length; // 只取前几条数据计算宽度
|
||||||
// 获取该列中最长的数据(内容)
|
// 获取该列中最长的数据(内容)
|
||||||
for (let i = 0; i < tableData.length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
let nowValue = tableData[i][prop];
|
let nowValue = tableData[i][prop];
|
||||||
if (!nowValue) {
|
if (!nowValue) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -205,7 +205,10 @@ class MysqlDialect implements DbDialect {
|
|||||||
genColumnBasicSql(cl: any): string {
|
genColumnBasicSql(cl: any): string {
|
||||||
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
let val = cl.value ? (cl.value === 'CURRENT_TIMESTAMP' ? cl.value : `'${cl.value}'`) : '';
|
||||||
let defVal = val ? `DEFAULT ${val}` : '';
|
let defVal = val ? `DEFAULT ${val}` : '';
|
||||||
let length = cl.length ? `(${cl.length})` : '';
|
let length = cl.length;
|
||||||
|
if (length) {
|
||||||
|
length = cl.numScale ? `(${cl.length},${cl.numScale})` : `(${cl.length})`;
|
||||||
|
}
|
||||||
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
let onUpdate = 'update_time' === cl.name ? ' ON UPDATE CURRENT_TIMESTAMP ' : '';
|
||||||
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
return ` ${this.quoteIdentifier(cl.name)} ${cl.type}${length} ${cl.notNull ? 'NOT NULL' : 'NULL'} ${
|
||||||
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
cl.auto_increment ? 'AUTO_INCREMENT' : ''
|
||||||
|
|||||||
@@ -19,39 +19,3 @@ export const DbSqlExecStatusEnum = {
|
|||||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
||||||
Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'),
|
Fail: EnumValue.of(-2, 'common.fail').setTagType('danger'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DbDataSyncDuplicateStrategyEnum = {
|
|
||||||
None: EnumValue.of(-1, 'db.none'),
|
|
||||||
Ignore: EnumValue.of(1, 'db.ignore'),
|
|
||||||
Replace: EnumValue.of(2, 'db.replace'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbDataSyncRecentStateEnum = {
|
|
||||||
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
|
||||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbDataSyncLogStatusEnum = {
|
|
||||||
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
|
||||||
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
|
|
||||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbDataSyncRunningStateEnum = {
|
|
||||||
Running: EnumValue.of(1, 'db.running').setTagType('success'),
|
|
||||||
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
|
|
||||||
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbTransferRunningStateEnum = {
|
|
||||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
|
||||||
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
|
||||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
|
||||||
Stop: EnumValue.of(-2, 'db.stop').setTagType('warning'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DbTransferFileStatusEnum = {
|
|
||||||
Running: EnumValue.of(1, 'db.running').setTagType('primary'),
|
|
||||||
Success: EnumValue.of(2, 'common.success').setTagType('success'),
|
|
||||||
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
|
||||||
};
|
|
||||||
|
|||||||
690
frontend/src/views/ops/db/resource/DbDataOp.vue
Normal file
@@ -0,0 +1,690 @@
|
|||||||
|
<template>
|
||||||
|
<div class="db-sql-exec h-full">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24" v-if="state.db">
|
||||||
|
<el-descriptions :column="4" size="small" border>
|
||||||
|
<el-descriptions-item label-align="right" :label="$t('common.operation')">
|
||||||
|
<el-button
|
||||||
|
:disabled="!state.db || !nowDbInst.id"
|
||||||
|
type="primary"
|
||||||
|
icon="Search"
|
||||||
|
link
|
||||||
|
@click="addQueryTab({ id: nowDbInst.id, dbs: nowDbInst.databases, nodeKey: getSqlMenuNodeKey(nowDbInst.id, state.db) }, state.db)"
|
||||||
|
:title="$t('db.newQuery')"
|
||||||
|
>
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<template v-if="!dbConfig.locationTreeNode">
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<el-button @click="locationNowTreeNode(null)" :title="$t('db.locationTagTree')" icon="Location" link></el-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
<!-- 数据库展示配置 -->
|
||||||
|
<el-popover
|
||||||
|
popper-style="max-height: 550px; overflow: auto; max-width: 450px"
|
||||||
|
placement="bottom"
|
||||||
|
width="auto"
|
||||||
|
:title="$t('db.dbShowSetting')"
|
||||||
|
trigger="click"
|
||||||
|
>
|
||||||
|
<el-row>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="dbConfig.showColumnComment"
|
||||||
|
:label="$t('db.showFieldComments')"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="dbConfig.locationTreeNode"
|
||||||
|
:label="$t('db.autoLocationTagTree')"
|
||||||
|
:true-value="1"
|
||||||
|
:false-value="0"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row>
|
||||||
|
<el-checkbox v-model="dbConfig.cacheTable" :label="$t('db.cacheTableInfo')" :true-value="1" :false-value="0" size="small" />
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<template #reference>
|
||||||
|
<el-link type="primary" icon="setting" underline="never"></el-link>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label-align="right" label="tag">{{ nowDbInst.tagPath }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item label-align="right">
|
||||||
|
<template #label>
|
||||||
|
<div>
|
||||||
|
<SvgIcon :name="nowDbInst.getDialect().getInfo().icon" :size="18" />
|
||||||
|
{{ $t('db.dbInst') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
{{ nowDbInst.id }}
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
{{ nowDbInst.name }}
|
||||||
|
<el-divider direction="vertical" border-style="dashed" />
|
||||||
|
{{ nowDbInst.host }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
|
||||||
|
<el-descriptions-item :label="$t('db.dbName')" label-align="right">{{ state.db }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<div id="data-exec" class="mt-1">
|
||||||
|
<el-tabs
|
||||||
|
v-if="state.tabs.size > 0"
|
||||||
|
type="card"
|
||||||
|
@tab-remove="onRemoveTab"
|
||||||
|
@tab-change="onTabChange"
|
||||||
|
v-model="state.activeName"
|
||||||
|
class="!h-full w-full"
|
||||||
|
>
|
||||||
|
<el-tab-pane class="!h-full" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
|
||||||
|
<template #label>
|
||||||
|
<el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<span @contextmenu.prevent="onTabContextmenu(dt, $event)" class="!text-[12px]">{{ dt.label }}</span>
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item label="tagPath">
|
||||||
|
{{ dt.params.tagPath }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('common.name')">
|
||||||
|
{{ dt.params.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Host">
|
||||||
|
<SvgIcon :name="getDbDialect(dt.params.type).getInfo().icon" :size="18" />
|
||||||
|
{{ dt.params.host }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="$t('db.dbName')">
|
||||||
|
{{ dt.params.dbName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<db-table-data-op
|
||||||
|
v-if="dt.type === TabType.TableData"
|
||||||
|
:db-id="dt.dbId"
|
||||||
|
:db-name="dt.db"
|
||||||
|
:table-name="dt.params.table"
|
||||||
|
:table-height="state.dataTabsTableHeight"
|
||||||
|
:ref="(el: any) => (dt.componentRef = el)"
|
||||||
|
></db-table-data-op>
|
||||||
|
|
||||||
|
<db-sql-editor
|
||||||
|
v-if="dt.type === TabType.Query"
|
||||||
|
:db-id="dt.dbId"
|
||||||
|
:db-name="dt.db"
|
||||||
|
:sql-name="dt.params.sqlName"
|
||||||
|
@save-sql-success="reloadSqls"
|
||||||
|
:ref="(el: any) => (dt.componentRef = el)"
|
||||||
|
>
|
||||||
|
</db-sql-editor>
|
||||||
|
|
||||||
|
<db-tables-op
|
||||||
|
v-if="dt.type == TabType.TablesOp"
|
||||||
|
:db-id="dt.params.id"
|
||||||
|
:db="dt.params.db"
|
||||||
|
:db-type="dt.params.type"
|
||||||
|
:height="state.tablesOpHeight"
|
||||||
|
/>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<db-table-op
|
||||||
|
:title="tableCreateDialog.title"
|
||||||
|
:active-name="tableCreateDialog.activeName"
|
||||||
|
:dbId="tableCreateDialog.dbId"
|
||||||
|
:db="tableCreateDialog.db"
|
||||||
|
:dbType="tableCreateDialog.dbType"
|
||||||
|
:version="tableCreateDialog.version"
|
||||||
|
:data="tableCreateDialog.data"
|
||||||
|
v-model:visible="tableCreateDialog.visible"
|
||||||
|
@submit-sql="onSubmitEditTableSql"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-dialog width="55%" :title="`'${state.chooseTableName}' DDL`" v-model="state.ddlDialog.visible">
|
||||||
|
<monaco-editor height="400px" language="sql" v-model="state.ddlDialog.ddl" :options="{ readOnly: true }" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<contextmenu ref="tabContextmenuRef" :dropdown="state.tabContextmenu.dropdown" :items="state.tabContextmenu.items" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { defineAsyncComponent, getCurrentInstance, h, inject, onBeforeUnmount, onMounted, reactive, ref, toRefs, useTemplateRef, watch } from 'vue';
|
||||||
|
import { ElCheckbox, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { DbInst, DbThemeConfig, registerDbCompletionItemProvider, TabInfo, TabType } from '../db';
|
||||||
|
import { ResourceOpCtx } from '@/views/ops/component/tag';
|
||||||
|
import { dbApi } from '../api';
|
||||||
|
import { dispposeCompletionItemProvider } from '@/components/monaco/completionItemProvider';
|
||||||
|
import SvgIcon from '@/components/svgIcon/index.vue';
|
||||||
|
import { Contextmenu, ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
import { getDbDialect } from '../dialect/index';
|
||||||
|
import { useEventListener, useStorage } from '@vueuse/core';
|
||||||
|
import SqlExecBox from '@/views/ops/db/component/sqleditor/SqlExecBox';
|
||||||
|
import { format as sqlFormatter } from 'sql-formatter';
|
||||||
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
|
||||||
|
import { DbDataOpComp } from '@/views/ops/db/resource';
|
||||||
|
|
||||||
|
const DbTableOp = defineAsyncComponent(() => import('../component/table/DbTableOp.vue'));
|
||||||
|
const DbSqlEditor = defineAsyncComponent(() => import('../component/sqleditor/DbSqlEditor.vue'));
|
||||||
|
const DbTableDataOp = defineAsyncComponent(() => import('../component/table/DbTableDataOp.vue'));
|
||||||
|
const DbTablesOp = defineAsyncComponent(() => import('../component/table/DbTablesOp.vue'));
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
|
||||||
|
|
||||||
|
const emits = defineEmits(['init']);
|
||||||
|
|
||||||
|
const tabContextmenuRef: any = useTemplateRef('tabContextmenuRef');
|
||||||
|
|
||||||
|
const tabContextmenuItems = [
|
||||||
|
new ContextmenuItem(1, 'db.close').withIcon('Close').withOnClick((data: any) => {
|
||||||
|
onRemoveTab(data.key);
|
||||||
|
}),
|
||||||
|
|
||||||
|
new ContextmenuItem(2, 'db.closeOther').withIcon('CircleClose').withOnClick((data: any) => {
|
||||||
|
const tabName = data.key;
|
||||||
|
const tabNames = [...state.tabs.keys()];
|
||||||
|
for (let tab of tabNames) {
|
||||||
|
if (tab !== tabName) {
|
||||||
|
onRemoveTab(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const tabs: Map<string, TabInfo> = new Map();
|
||||||
|
const state = reactive({
|
||||||
|
defaultExpendKey: [] as any,
|
||||||
|
/**
|
||||||
|
* 当前操作的数据库实例
|
||||||
|
*/
|
||||||
|
nowDbInst: {} as DbInst,
|
||||||
|
db: '', // 当前操作的数据库
|
||||||
|
activeName: '',
|
||||||
|
reloadStatus: false,
|
||||||
|
tabs,
|
||||||
|
tabContextmenu: {
|
||||||
|
dropdown: { x: 0, y: 0 },
|
||||||
|
items: tabContextmenuItems,
|
||||||
|
},
|
||||||
|
dataTabsTableHeight: '600px',
|
||||||
|
tablesOpHeight: '600',
|
||||||
|
dbServerInfo: {
|
||||||
|
loading: true,
|
||||||
|
version: '',
|
||||||
|
},
|
||||||
|
tableCreateDialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
activeName: '',
|
||||||
|
dbId: 0,
|
||||||
|
version: '',
|
||||||
|
db: '',
|
||||||
|
dbType: '',
|
||||||
|
data: {},
|
||||||
|
parentKey: '',
|
||||||
|
},
|
||||||
|
chooseTableName: '',
|
||||||
|
ddlDialog: {
|
||||||
|
visible: false,
|
||||||
|
ddl: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { nowDbInst, tableCreateDialog } = toRefs(state);
|
||||||
|
|
||||||
|
const dbConfig = useStorage('dbConfig', DbThemeConfig);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
state.reloadStatus = !dbConfig.value.cacheTable;
|
||||||
|
emits('init', { name: DbDataOpComp.name, ref: getCurrentInstance()?.exposed });
|
||||||
|
setHeight();
|
||||||
|
// 监听浏览器窗口大小变化,更新对应组件高度
|
||||||
|
useEventListener(window, 'resize', setHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
dispposeCompletionItemProvider('sql');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置editor高度和数据表高度
|
||||||
|
*/
|
||||||
|
const setHeight = () => {
|
||||||
|
state.dataTabsTableHeight = window.innerHeight - 253 + 'px';
|
||||||
|
state.tablesOpHeight = window.innerHeight - 225 + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择数据库,改变当前正在操作的数据库信息
|
||||||
|
const changeDb = async (db: any, dbName: string) => {
|
||||||
|
state.nowDbInst = await DbInst.getOrNewInst(db);
|
||||||
|
state.nowDbInst.databases = db.databases;
|
||||||
|
state.db = dbName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载选中的表数据,即新增表数据操作tab
|
||||||
|
const loadTableData = async (db: any, dbName: string, tableName: string) => {
|
||||||
|
if (tableName == '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await changeDb(db, dbName);
|
||||||
|
|
||||||
|
const key = `tableData:${db.id}.${dbName}.${tableName}`;
|
||||||
|
let tab = state.tabs.get(key);
|
||||||
|
state.activeName = key;
|
||||||
|
// 如果存在该表tab,则直接返回
|
||||||
|
if (tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab = new TabInfo();
|
||||||
|
tab.label = tableName;
|
||||||
|
tab.key = key;
|
||||||
|
tab.treeNodeKey = db.nodeKey;
|
||||||
|
tab.dbId = db.id;
|
||||||
|
tab.db = dbName;
|
||||||
|
tab.type = TabType.TableData;
|
||||||
|
tab.params = {
|
||||||
|
...getNowDbInfo(),
|
||||||
|
table: tableName,
|
||||||
|
};
|
||||||
|
state.tabs.set(key, tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新建查询tab
|
||||||
|
const addQueryTab = async (db: any, dbName: string, sqlName: string = '') => {
|
||||||
|
if (!dbName || !db.id) {
|
||||||
|
ElMessage.warning(t('db.noDbInstMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await changeDb(db, dbName);
|
||||||
|
|
||||||
|
const dbId = db.id;
|
||||||
|
let label;
|
||||||
|
let key;
|
||||||
|
// 存在sql模板名,则该模板名只允许一个tab
|
||||||
|
if (sqlName) {
|
||||||
|
label = `${t('db.query')}-${sqlName}`;
|
||||||
|
key = `query:${dbId}.${dbName}.${sqlName}`;
|
||||||
|
} else {
|
||||||
|
let count = 1;
|
||||||
|
state.tabs.forEach((v) => {
|
||||||
|
if (v.type == TabType.Query && !v.params.sqlName) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
label = `${t('db.nQuery')}-${count}`;
|
||||||
|
key = `query:${count}.${dbId}.${dbName}`;
|
||||||
|
}
|
||||||
|
state.activeName = key;
|
||||||
|
let tab = state.tabs.get(key);
|
||||||
|
if (tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab = new TabInfo();
|
||||||
|
tab.key = key;
|
||||||
|
tab.label = label;
|
||||||
|
tab.treeNodeKey = db.nodeKey;
|
||||||
|
tab.dbId = dbId;
|
||||||
|
tab.db = dbName;
|
||||||
|
tab.type = TabType.Query;
|
||||||
|
tab.params = {
|
||||||
|
...getNowDbInfo(),
|
||||||
|
sqlName: sqlName,
|
||||||
|
dbs: db.dbs,
|
||||||
|
};
|
||||||
|
state.tabs.set(key, tab);
|
||||||
|
// 注册当前sql编辑框提示词
|
||||||
|
registerDbCompletionItemProvider(tab.dbId, tab.db, tab.params.dbs, nowDbInst.value.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加数据操作tab
|
||||||
|
* @param inst
|
||||||
|
*/
|
||||||
|
const addTablesOpTab = async (db: any) => {
|
||||||
|
const dbName = db.db;
|
||||||
|
if (!db || !db.id) {
|
||||||
|
ElMessage.warning(t('db.noDbInstMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await changeDb(db, dbName);
|
||||||
|
|
||||||
|
const dbId = db.id;
|
||||||
|
let key = `tablesOp:${dbId}.${dbName}`;
|
||||||
|
state.activeName = key;
|
||||||
|
|
||||||
|
let tab = state.tabs.get(key);
|
||||||
|
if (tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab = new TabInfo();
|
||||||
|
tab.key = key;
|
||||||
|
tab.label = `${t('db.tableOp')}-${dbName}`;
|
||||||
|
tab.treeNodeKey = db.nodeKey;
|
||||||
|
tab.dbId = dbId;
|
||||||
|
tab.db = dbName;
|
||||||
|
tab.type = TabType.TablesOp;
|
||||||
|
tab.params = {
|
||||||
|
...getNowDbInfo(),
|
||||||
|
id: db.id,
|
||||||
|
db: dbName,
|
||||||
|
type: db.type,
|
||||||
|
};
|
||||||
|
state.tabs.set(key, tab);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemoveTab = (targetName: string) => {
|
||||||
|
let activeName = state.activeName;
|
||||||
|
const tabNames = [...state.tabs.keys()];
|
||||||
|
for (let i = 0; i < tabNames.length; i++) {
|
||||||
|
const tabName = tabNames[i];
|
||||||
|
if (tabName !== targetName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tabs.delete(targetName);
|
||||||
|
if (activeName != targetName) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
|
||||||
|
const nextTab = tabNames[i + 1] || tabNames[i - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeName = nextTab;
|
||||||
|
} else {
|
||||||
|
activeName = '';
|
||||||
|
}
|
||||||
|
state.activeName = activeName;
|
||||||
|
onTabChange();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTabChange = () => {
|
||||||
|
if (!state.activeName) {
|
||||||
|
state.nowDbInst = {} as DbInst;
|
||||||
|
state.db = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nowTab = state.tabs.get(state.activeName);
|
||||||
|
state.nowDbInst = DbInst.getInst(nowTab?.dbId);
|
||||||
|
state.db = nowTab?.db as string;
|
||||||
|
|
||||||
|
if (nowTab?.type == TabType.Query) {
|
||||||
|
// 注册sql提示
|
||||||
|
registerDbCompletionItemProvider(nowTab.dbId, nowTab.db, nowTab.params.dbs, nowDbInst.value.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激活当前tab(需要调用DbTableData组件的active,否则表头与数据会出现错位,暂不知为啥,先这样处理)
|
||||||
|
nowTab?.componentRef?.active();
|
||||||
|
|
||||||
|
if (dbConfig.value.locationTreeNode) {
|
||||||
|
locationNowTreeNode(nowTab);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 右键点击时:传 x,y 坐标值到子组件中(props)
|
||||||
|
const onTabContextmenu = (v: any, e: any) => {
|
||||||
|
const { clientX, clientY } = e;
|
||||||
|
state.tabContextmenu.dropdown.x = clientX;
|
||||||
|
state.tabContextmenu.dropdown.y = clientY;
|
||||||
|
tabContextmenuRef.value.openContextmenu(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位至当前树节点
|
||||||
|
*/
|
||||||
|
const locationNowTreeNode = (nowTab: any = null) => {
|
||||||
|
if (!nowTab) {
|
||||||
|
nowTab = state.tabs.get(state.activeName);
|
||||||
|
}
|
||||||
|
setTimeout(() => resourceOpCtx?.setCurrentTreeKey(nowTab?.treeNodeKey), 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadSqls = (dbId: number, db: string) => {
|
||||||
|
resourceOpCtx?.reloadTreeNode(getSqlMenuNodeKey(dbId, db));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSql = async (dbId: any, db: string, sqlName: string) => {
|
||||||
|
try {
|
||||||
|
await useI18nDeleteConfirm(sqlName);
|
||||||
|
await dbApi.deleteDbSql.request({ id: dbId, db: db, name: sqlName });
|
||||||
|
useI18nDeleteSuccessMsg();
|
||||||
|
reloadSqls(dbId, db);
|
||||||
|
} catch (err) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
||||||
|
return `${dbId}.${db}.sql-menu`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadNode = (nodeKey: string) => {
|
||||||
|
state.reloadStatus = true;
|
||||||
|
resourceOpCtx?.reloadTreeNode(nodeKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, tableComment, type, parentKey, key, version } = data.params;
|
||||||
|
// data.label就是表名
|
||||||
|
if (tableName) {
|
||||||
|
state.tableCreateDialog.title = useI18nEditTitle('db.table');
|
||||||
|
let indexs = await dbApi.tableIndex.request({ id, db, tableName });
|
||||||
|
let columns = await dbApi.columnMetadata.request({ id, db, tableName });
|
||||||
|
let row = { tableName, tableComment };
|
||||||
|
state.tableCreateDialog.data = { edit: true, row, indexs, columns };
|
||||||
|
state.tableCreateDialog.parentKey = parentKey;
|
||||||
|
} else {
|
||||||
|
state.tableCreateDialog.title = useI18nCreateTitle('db.table');
|
||||||
|
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||||
|
state.tableCreateDialog.parentKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.tableCreateDialog.activeName = '1';
|
||||||
|
state.tableCreateDialog.dbId = id;
|
||||||
|
state.tableCreateDialog.version = version;
|
||||||
|
state.tableCreateDialog.db = db;
|
||||||
|
state.tableCreateDialog.dbType = type;
|
||||||
|
state.tableCreateDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey, schema } = data.params;
|
||||||
|
await useI18nDeleteConfirm(tableName);
|
||||||
|
|
||||||
|
// 执行sql
|
||||||
|
let dialect = getDbDialect(state.nowDbInst.type);
|
||||||
|
let schemaStr = schema ? `${dialect.quoteIdentifier(schema)}.` : '';
|
||||||
|
|
||||||
|
dbApi.sqlExec.request({ id, db, sql: `drop table ${schemaStr + dialect.quoteIdentifier(tableName)}` }).then((res) => {
|
||||||
|
let success = true;
|
||||||
|
for (let re of res) {
|
||||||
|
if (re.errorMsg) {
|
||||||
|
success = false;
|
||||||
|
ElMessage.error(`${re.sql} -> ${re.errorMsg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
useI18nDeleteSuccessMsg();
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onGenDdl = async (data: any) => {
|
||||||
|
let { db, id, tableName, type } = data.params;
|
||||||
|
state.chooseTableName = tableName;
|
||||||
|
let res = await dbApi.tableDdl.request({ id, db, tableName });
|
||||||
|
state.ddlDialog.ddl = sqlFormatter(res, { language: getDbDialect(type).getInfo().formatSqlDialect as any });
|
||||||
|
state.ddlDialog.visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRenameTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey } = data.params;
|
||||||
|
let tableData = { db, oldTableName: tableName, tableName };
|
||||||
|
|
||||||
|
let value = ref(tableName);
|
||||||
|
// 弹出确认框
|
||||||
|
const promptValue = await ElMessageBox.prompt('', t('db.renamePrompt', { db, tableName }), {
|
||||||
|
inputValue: value.value,
|
||||||
|
confirmButtonText: t('common.confirm'),
|
||||||
|
cancelButtonText: t('common.cancel'),
|
||||||
|
});
|
||||||
|
|
||||||
|
tableData.tableName = promptValue.value;
|
||||||
|
let sql = nowDbInst.value.getDialect().getModifyTableInfoSql(tableData);
|
||||||
|
if (!sql) {
|
||||||
|
ElMessage.warning(t('db.noChange'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SqlExecBox({
|
||||||
|
sql: sql,
|
||||||
|
dbId: id as any,
|
||||||
|
db: db as any,
|
||||||
|
dbType: nowDbInst.value.getDialect().getInfo().formatSqlDialect,
|
||||||
|
runSuccessCallback: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCopyTable = async (data: any) => {
|
||||||
|
let { db, id, tableName, parentKey } = data.params;
|
||||||
|
|
||||||
|
let checked = ref(false);
|
||||||
|
|
||||||
|
// 弹出确认框,并选择是否复制数据
|
||||||
|
await ElMessageBox({
|
||||||
|
title: `${t('db.copyTable')}【${tableName}】`,
|
||||||
|
type: 'warning',
|
||||||
|
// icon: markRaw(Delete),
|
||||||
|
message: () =>
|
||||||
|
h(ElCheckbox, {
|
||||||
|
label: t('db.isCopyTableData'),
|
||||||
|
modelValue: checked.value,
|
||||||
|
'onUpdate:modelValue': (val: boolean | string | number) => {
|
||||||
|
if (typeof val === 'boolean') {
|
||||||
|
checked.value = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
callback: (action: string) => {
|
||||||
|
if (action === 'confirm') {
|
||||||
|
// 执行sql
|
||||||
|
dbApi.copyTable.request({ id, db, tableName, copyData: checked.value }).then(() => {
|
||||||
|
useI18nOperateSuccessMsg();
|
||||||
|
setTimeout(() => {
|
||||||
|
parentKey && reloadNode(parentKey);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitEditTableSql = () => {
|
||||||
|
state.tableCreateDialog.visible = false;
|
||||||
|
state.tableCreateDialog.data = { edit: false, row: {} };
|
||||||
|
reloadNode(state.tableCreateDialog.parentKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前操作的数据库信息
|
||||||
|
*/
|
||||||
|
const getNowDbInfo = () => {
|
||||||
|
const di = state.nowDbInst;
|
||||||
|
return {
|
||||||
|
tagPath: di.tagPath,
|
||||||
|
id: di.id,
|
||||||
|
name: di.name,
|
||||||
|
type: di.type,
|
||||||
|
host: di.host,
|
||||||
|
dbName: state.db,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadTables = async (dbInfo: any) => {
|
||||||
|
if (!dbInfo || !dbInfo.id) {
|
||||||
|
ElMessage.warning(t('db.noDbInstMsg'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let { id, db } = dbInfo;
|
||||||
|
// 获取当前库的所有表信息
|
||||||
|
let tables = await DbInst.getInst(id).loadTables(db, state.reloadStatus);
|
||||||
|
state.reloadStatus = !dbConfig.value.cacheTable;
|
||||||
|
return tables;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
onChangeDb: changeDb,
|
||||||
|
loadTables,
|
||||||
|
loadTableData,
|
||||||
|
onCopyTable,
|
||||||
|
onEditTable,
|
||||||
|
onDeleteTable,
|
||||||
|
onGenDdl,
|
||||||
|
onRenameTable,
|
||||||
|
onRemoveTab,
|
||||||
|
addQueryTab,
|
||||||
|
addTablesOpTab,
|
||||||
|
reloadSqls,
|
||||||
|
deleteSql,
|
||||||
|
reloadNode,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.db-sql-exec {
|
||||||
|
#data-exec {
|
||||||
|
::v-deep(.el-tabs) {
|
||||||
|
--el-tabs-header-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-tabs__header) {
|
||||||
|
margin: 0 0 5px;
|
||||||
|
|
||||||
|
.el-tabs__item {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep(.el-tabs__nav-next) {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
::v-deep(.el-tabs__nav-prev) {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update_field_active {
|
||||||
|
background-color: var(--el-color-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
13
frontend/src/views/ops/db/resource/NodeDb.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #suffix="{ data }">
|
||||||
|
<span v-if="data.params.username">{{ ` ${data.params.username}` }}</span>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
53
frontend/src/views/ops/db/resource/NodeDbInst.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #prefix="{ data }">
|
||||||
|
<el-popover @show="showDbInfo(data.params)" :show-after="500" placement="right-start" :title="$t('db.dbInstInfo')" trigger="hover" :width="250">
|
||||||
|
<template #reference>
|
||||||
|
<SvgIcon :name="getDbDialect(data.params.type).getInfo().icon" :size="18" />
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<el-descriptions :column="1" size="small">
|
||||||
|
<el-descriptions-item :label="$t('common.name')">
|
||||||
|
{{ data.params.name }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Host">
|
||||||
|
{{ `${data.params.host}:${data.params.port}` }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="version">
|
||||||
|
<span v-loading="loadingServerInfo"> {{ `${dbServerInfo?.version}` }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<!-- <el-descriptions-item :label="$t('db.acName')">
|
||||||
|
{{ data.params.authCertName }}
|
||||||
|
</el-descriptions-item> -->
|
||||||
|
<el-descriptions-item :label="$t('common.remark')">
|
||||||
|
{{ data.params.remark }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { dbApi } from '../api';
|
||||||
|
import { getDbDialect } from '../dialect/index';
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
|
||||||
|
const serverInfoReqParam = ref({
|
||||||
|
instanceId: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { execute: getDbServerInfo, isFetching: loadingServerInfo, data: dbServerInfo } = dbApi.getInstanceServerInfo.useApi<any>(serverInfoReqParam);
|
||||||
|
|
||||||
|
const showDbInfo = async (db: any) => {
|
||||||
|
if (dbServerInfo.value) {
|
||||||
|
dbServerInfo.value.version = '';
|
||||||
|
}
|
||||||
|
serverInfoReqParam.value.instanceId = db.id;
|
||||||
|
await getDbServerInfo();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
13
frontend/src/views/ops/db/resource/NodeDbTable.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<BaseTreeNode v-bind="$attrs">
|
||||||
|
<template #suffix="{ data }">
|
||||||
|
<span v-if="data.params.size">{{ ` ${data.params.size}` }}</span>
|
||||||
|
</template>
|
||||||
|
</BaseTreeNode>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
320
frontend/src/views/ops/db/resource/index.ts
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import { ContextmenuItem } from '@/components/contextmenu';
|
||||||
|
|
||||||
|
import { NodeType, TagTreeNode, ResourceConfig } from '../../component/tag';
|
||||||
|
import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import { dbApi } from '../api';
|
||||||
|
import { sleep } from '@/common/utils/loading';
|
||||||
|
import { DbInst } from '../db';
|
||||||
|
import { schemaDbTypes } from '../dialect/index';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { formatByteSize } from '@/common/utils/format';
|
||||||
|
|
||||||
|
const DbInstList = defineAsyncComponent(() => import('../InstanceList.vue'));
|
||||||
|
const DbDataOp = defineAsyncComponent(() => import('./DbDataOp.vue'));
|
||||||
|
const NodeDbInst = defineAsyncComponent(() => import('./NodeDbInst.vue'));
|
||||||
|
const NodeDb = defineAsyncComponent(() => import('./NodeDb.vue'));
|
||||||
|
const NodeDbTable = defineAsyncComponent(() => import('./NodeDbTable.vue'));
|
||||||
|
|
||||||
|
export const DbIcon = {
|
||||||
|
name: ResourceTypeEnum.Db.extra.icon,
|
||||||
|
color: ResourceTypeEnum.Db.extra.iconColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
// pgsql schema icon
|
||||||
|
export const SchemaIcon = {
|
||||||
|
name: 'List',
|
||||||
|
color: '#67c23a',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableIcon = {
|
||||||
|
name: 'icon db/table',
|
||||||
|
color: '#409eff',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SqlIcon = {
|
||||||
|
name: 'icon db/sql',
|
||||||
|
color: '#f56c6c',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbDataOpComp = {
|
||||||
|
name: 'tag.dbDataOp',
|
||||||
|
component: DbDataOp,
|
||||||
|
icon: DbIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
// node节点点击时,触发改变db事件
|
||||||
|
const nodeClickChangeDb = async (nodeData: TagTreeNode) => {
|
||||||
|
const params = nodeData.params;
|
||||||
|
if (params.db) {
|
||||||
|
const compRef = await nodeData.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
|
compRef.onChangeDb(
|
||||||
|
{
|
||||||
|
id: params.id,
|
||||||
|
host: `${params.host}`,
|
||||||
|
name: params.name,
|
||||||
|
type: params.type,
|
||||||
|
tagPath: params.tagPath,
|
||||||
|
databases: params.dbs,
|
||||||
|
},
|
||||||
|
params.db
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContextmenuItemRefresh = new ContextmenuItem('refresh', 'common.refresh')
|
||||||
|
.withIcon('RefreshRight')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).reloadNode(node.key));
|
||||||
|
|
||||||
|
// 数据库实例节点类型
|
||||||
|
const NodeTypeDbInst = new NodeType(TagResourceTypeEnum.DbInstance.value).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
|
const tagPath = parentNode.params.tagPath;
|
||||||
|
|
||||||
|
const dbInstancesRes = await dbApi.instances.request({ tagPath, pageSize: 100 });
|
||||||
|
const dbInstances = dbInstancesRes.list;
|
||||||
|
if (!dbInstances) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止过快加载会出现一闪而过,对眼睛不好
|
||||||
|
await sleep(100);
|
||||||
|
return dbInstances?.map((x: any) => {
|
||||||
|
x.tagPath = tagPath;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbConf).withParams(x).withNodeComponent(NodeDbInst);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据库配置节点类型
|
||||||
|
const NodeTypeDbConf = new NodeType(TagResourceTypeEnum.Db.value)
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
|
||||||
|
const tagPath = params.tagPath;
|
||||||
|
const authCerts = {} as any;
|
||||||
|
for (let authCert of params.authCerts) {
|
||||||
|
authCerts[authCert.name] = authCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbInfoRes = await dbApi.dbs.request({
|
||||||
|
tagPath: `${tagPath}${TagResourceTypeEnum.DbInstance.value}|${params.code}`,
|
||||||
|
});
|
||||||
|
const dbInfos = dbInfoRes.list;
|
||||||
|
if (!dbInfos) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbInfos?.map((x: any) => {
|
||||||
|
x.tagPath = tagPath;
|
||||||
|
x.username = authCerts[x.authCertName]?.username;
|
||||||
|
return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeDbs).withParams(x).withIcon(DbIcon).withNodeComponent(NodeDb);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.withContextMenuItems([ContextmenuItemRefresh]);
|
||||||
|
|
||||||
|
// 数据库列表名类型
|
||||||
|
const NodeTypeDbs = new NodeType(222).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
const dbs = (await DbInst.getDbNames(params))?.sort();
|
||||||
|
// 查询数据库版本信息
|
||||||
|
const version = await dbApi.getCompatibleDbVersion.request({ id: params.id, db: dbs[0] });
|
||||||
|
return dbs.map((x: any) => {
|
||||||
|
return TagTreeNode.new(parentNode, `${parentNode.key}.${x}`, x, NodeTypeDb)
|
||||||
|
.withParams({
|
||||||
|
tagPath: params.tagPath,
|
||||||
|
id: params.id,
|
||||||
|
name: params.name,
|
||||||
|
type: params.type,
|
||||||
|
version: version || 'unset',
|
||||||
|
host: `${params.host}:${params.port}`,
|
||||||
|
dbs: dbs,
|
||||||
|
db: x,
|
||||||
|
})
|
||||||
|
.withIcon(DbIcon);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据库节点
|
||||||
|
const NodeTypeDb = new NodeType(2)
|
||||||
|
.withContextMenuItems([ContextmenuItemRefresh])
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
|
// pg类数据库会多一层schema
|
||||||
|
if (schemaDbTypes.includes(params.type)) {
|
||||||
|
const { id, db } = params;
|
||||||
|
const schemaNames = await dbApi.pgSchemas.request({ id, db });
|
||||||
|
return schemaNames.map((sn: any) => {
|
||||||
|
// 将db变更为 db/schema;
|
||||||
|
const nParams = { ...params };
|
||||||
|
nParams.schema = sn;
|
||||||
|
nParams.db = nParams.db + '/' + sn;
|
||||||
|
nParams.dbs = schemaNames;
|
||||||
|
return TagTreeNode.new(parentNode, `${params.id}.${params.db}.schema.${sn}`, sn, NodeTypePostgresSchema)
|
||||||
|
.withParams(nParams)
|
||||||
|
.withIcon(SchemaIcon);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return getNodeTypeTables(parentNode);
|
||||||
|
})
|
||||||
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
|
const getNodeTypeTables = (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
let tableKey = `${params.id}.${params.db}.table-menu`;
|
||||||
|
let sqlKey = getSqlMenuNodeKey(params.id, params.db);
|
||||||
|
return [
|
||||||
|
TagTreeNode.new(parentNode, `${params.id}.${params.db}.table-menu`, i18n.global.t('db.table'), NodeTypeTableMenu)
|
||||||
|
.withParams({
|
||||||
|
...params,
|
||||||
|
key: tableKey,
|
||||||
|
})
|
||||||
|
.withIcon(TableIcon),
|
||||||
|
|
||||||
|
TagTreeNode.new(parentNode, sqlKey, 'SQL', NodeTypeSqlMenu)
|
||||||
|
.withParams({ ...params, key: sqlKey })
|
||||||
|
.withIcon(SqlIcon),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
// postgres schema模式
|
||||||
|
const NodeTypePostgresSchema = new NodeType(3)
|
||||||
|
.withContextMenuItems([ContextmenuItemRefresh])
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
params.parentKey = parentNode.key;
|
||||||
|
return getNodeTypeTables(parentNode);
|
||||||
|
})
|
||||||
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
|
// 数据库表菜单节点
|
||||||
|
const NodeTypeTableMenu = new NodeType(4)
|
||||||
|
.withContextMenuItems([
|
||||||
|
ContextmenuItemRefresh,
|
||||||
|
new ContextmenuItem('createTable', 'db.createTable').withIcon('Plus').withOnClick(async (parentNode: TagTreeNode) => {
|
||||||
|
(await parentNode.ctx?.addResourceComponent(DbDataOpComp))?.onEditTable(parentNode);
|
||||||
|
}),
|
||||||
|
new ContextmenuItem('tablesOp', 'db.tableOp').withIcon('Setting').withOnClick(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
(await parentNode.ctx?.addResourceComponent(DbDataOpComp)).addTablesOpTab({
|
||||||
|
id: params.id,
|
||||||
|
db: params.db,
|
||||||
|
type: params.type,
|
||||||
|
nodeKey: parentNode.key,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const compRef = await parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
|
const params = parentNode.params;
|
||||||
|
// // 获取当前库的所有表信息
|
||||||
|
const tables = await compRef.loadTables(params);
|
||||||
|
let { id, db, type, schema, version } = params;
|
||||||
|
let dbTableSize = 0;
|
||||||
|
const tablesNode = tables.map((x: any) => {
|
||||||
|
const tableSize = x.dataLength + x.indexLength;
|
||||||
|
dbTableSize += tableSize;
|
||||||
|
const key = `${id}.${db}.${x.tableName}`;
|
||||||
|
return TagTreeNode.new(parentNode, key, x.tableName, NodeTypeTable)
|
||||||
|
.withIsLeaf(true)
|
||||||
|
.withParams({
|
||||||
|
id,
|
||||||
|
db,
|
||||||
|
type,
|
||||||
|
schema,
|
||||||
|
version,
|
||||||
|
key: key,
|
||||||
|
parentKey: parentNode.key,
|
||||||
|
tableName: x.tableName,
|
||||||
|
tableComment: x.tableComment,
|
||||||
|
size: tableSize == 0 ? '' : formatByteSize(tableSize, 1),
|
||||||
|
})
|
||||||
|
.withIcon(TableIcon)
|
||||||
|
.withNodeComponent(NodeDbTable)
|
||||||
|
.withLabelRemark(`${x.tableName} ${x.tableComment ? '| ' + x.tableComment : ''}`);
|
||||||
|
});
|
||||||
|
// 设置父节点参数的表大小
|
||||||
|
parentNode.params.dbTableSize = dbTableSize == 0 ? '' : formatByteSize(dbTableSize);
|
||||||
|
return tablesNode;
|
||||||
|
});
|
||||||
|
// .withNodeDblclickFunc((node: TagTreeNode) => {
|
||||||
|
// const params = node.params;
|
||||||
|
// addTablesOpTab({ id: params.id, db: params.db, type: params.type, version: params.version, nodeKey: node.key });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 数据库sql模板菜单节点
|
||||||
|
const NodeTypeSqlMenu = new NodeType(5)
|
||||||
|
.withLoadNodesFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const params = parentNode.params;
|
||||||
|
const id = params.id;
|
||||||
|
const db = params.db;
|
||||||
|
const dbs = params.dbs;
|
||||||
|
// 加载用户保存的sql脚本
|
||||||
|
const sqls = await dbApi.getSqlNames.request({ id: id, db: db });
|
||||||
|
return sqls.map((x: any) => {
|
||||||
|
return TagTreeNode.new(parentNode, `${id}.${db}.${x.name}`, x.name, NodeTypeSql)
|
||||||
|
.withIsLeaf(true)
|
||||||
|
.withParams({ id, db, dbs, sqlName: x.name })
|
||||||
|
.withIcon(SqlIcon);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.withNodeClickFunc(nodeClickChangeDb);
|
||||||
|
|
||||||
|
// 表节点类型
|
||||||
|
const NodeTypeTable = new NodeType(6)
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('copyTable', 'db.copyTable')
|
||||||
|
.withIcon('copyDocument')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onCopyTable(node)),
|
||||||
|
new ContextmenuItem('renameTable', 'db.renameTable')
|
||||||
|
.withIcon('edit')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onRenameTable(node)),
|
||||||
|
new ContextmenuItem('editTable', 'db.editTable')
|
||||||
|
.withIcon('edit')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onEditTable(node)),
|
||||||
|
new ContextmenuItem('delTable', 'db.delTable')
|
||||||
|
.withIcon('Delete')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onDeleteTable(node)),
|
||||||
|
new ContextmenuItem('ddl', 'DDL')
|
||||||
|
.withIcon('Document')
|
||||||
|
.withOnClick(async (node: TagTreeNode) => (await node.ctx?.addResourceComponent(DbDataOpComp)).onGenDdl(node)),
|
||||||
|
])
|
||||||
|
.withNodeClickFunc(async (node: TagTreeNode) => {
|
||||||
|
const params = node.params;
|
||||||
|
(await node.ctx?.addResourceComponent(DbDataOpComp)).loadTableData({ id: params.id, nodeKey: node.key }, params.db, params.tableName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// sql模板节点类型
|
||||||
|
const NodeTypeSql = new NodeType(7)
|
||||||
|
.withNodeClickFunc(async (parentNode: TagTreeNode) => {
|
||||||
|
const compRef = await parentNode.ctx?.addResourceComponent(DbDataOpComp);
|
||||||
|
const params = parentNode.params;
|
||||||
|
compRef.addQueryTab({ id: params.id, nodeKey: parentNode.key, dbs: params.dbs }, params.db, params.sqlName);
|
||||||
|
})
|
||||||
|
.withContextMenuItems([
|
||||||
|
new ContextmenuItem('delSql', 'common.delete')
|
||||||
|
.withIcon('delete')
|
||||||
|
.withOnClick(async (node: TagTreeNode) =>
|
||||||
|
(await node.ctx?.addResourceComponent(DbDataOpComp)).deleteSql(node.params.id, node.params.db, node.params.sqlName)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getSqlMenuNodeKey = (dbId: number, db: string) => {
|
||||||
|
return `${dbId}.${db}.sql-menu`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
order: 2,
|
||||||
|
resourceType: ResourceTypeEnum.Db.value,
|
||||||
|
rootNodeType: NodeTypeDbInst,
|
||||||
|
manager: {
|
||||||
|
componentConf: {
|
||||||
|
component: DbInstList,
|
||||||
|
icon: DbIcon,
|
||||||
|
name: 'tag.db',
|
||||||
|
},
|
||||||
|
countKey: 'db',
|
||||||
|
permCode: 'db:instance',
|
||||||
|
},
|
||||||
|
} as ResourceConfig;
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
InstanceList: () => import('@/views/ops/db/InstanceList.vue'),
|
SyncTaskList: () => import('@/views/ops/db/sync/SyncTaskList.vue'),
|
||||||
SqlExec: () => import('@/views/ops/db/SqlExec.vue'),
|
DbTransferList: () => import('@/views/ops/db/transfer/DbTransferList.vue'),
|
||||||
SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
|
|
||||||
DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||||
<el-tabs v-model="tabActiveName">
|
<el-tabs v-model="tabActiveName">
|
||||||
<el-tab-pane :label="$t('common.basic')" :name="basicTab">
|
<el-tab-pane :label="$t('common.basic')" :name="basicTab">
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-form-item prop="status" :label="$t('common.status')" label-width="60" required>
|
<el-form-item prop="status" :label="$t('common.status')" label-position="left" label-width="60" required>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="form.status"
|
v-model="form.status"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
|
<monaco-editor height="200px" class="task-sql" language="sql" v-model="form.dataSql" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
|
<el-form-item prop="targetTableName" :label="$t('db.targetDbTable')" required>
|
||||||
<el-select v-model="form.targetTableName" filterable>
|
<el-select v-model="form.targetTableName" filterable>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTooltip :label="$t('db.updateField')" prop="updField" :tooltip="$t('db.updateFieldTips')">
|
<FormItemTooltip :label="$t('db.updateField')" prop="updField" :tooltip="$t('db.updateFieldTips')">
|
||||||
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
|
<el-input v-model.trim="form.updField" :placeholder="$t('db.updateFiledPlaceholder')" auto-complete="off" />
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row>
|
<el-row :gutter="10">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<FormItemTooltip :label="$t('db.fieldValueSrc')" prop="updFieldSrc" :tooltip="$t('db.fieldValueSrcTips')">
|
<FormItemTooltip :label="$t('db.fieldValueSrc')" prop="updFieldSrc" :tooltip="$t('db.fieldValueSrcTips')">
|
||||||
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
|
<el-input v-model.trim="form.updFieldSrc" :placeholder="$t('db.fieldValueSrcPlaceholder')" auto-complete="off" />
|
||||||
@@ -105,17 +105,32 @@
|
|||||||
|
|
||||||
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
|
<el-tab-pane :label="$t('db.fieldMap')" :name="fieldTab" :disabled="!baseFieldCompleted">
|
||||||
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
|
<el-form-item prop="fieldMap" :label="$t('db.fieldMap')" required>
|
||||||
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight" size="small">
|
<el-table :data="form.fieldMap" :max-height="fieldMapTableHeight">
|
||||||
<el-table-column prop="src" :label="$t('db.srcField')" :width="200" />
|
<el-table-column prop="src" :label="$t('db.srcField')" :width="200"></el-table-column>
|
||||||
<el-table-column prop="target" :label="$t('db.targetField')">
|
<el-table-column prop="target" :label="$t('db.targetField')">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-select v-model="scope.row.target" allow-create filterable>
|
<el-select v-model="scope.row.target" allow-create filterable>
|
||||||
|
<template #label="{ label, value }">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<el-text tag="b">{{ value }}</el-text>
|
||||||
|
<el-text size="small">{{ label }}</el-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in state.targetColumnList"
|
v-for="item in state.targetColumnList"
|
||||||
:key="item.columnName"
|
:key="item.columnName"
|
||||||
:label="item.columnName + ` ${item.columnType}` + (item.columnComment && ' - ' + item.columnComment)"
|
:label="`${item.columnType}${item.columnComment && ' - ' + item.columnComment}`"
|
||||||
:value="item.columnName"
|
:value="item.columnName"
|
||||||
/>
|
>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
{{ item.columnName }}
|
||||||
|
|
||||||
|
<el-text size="small">
|
||||||
|
{{ item.columnType }}{{ item.columnComment && ' - ' + item.columnComment }}
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -194,7 +209,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
import { computed, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
import MonacoEditor from '@/components/monaco/MonacoEditor.vue';
|
||||||
@@ -203,11 +217,13 @@ import { compatibleDuplicateStrategy, DbType, getDbDialect } from '@/views/ops/d
|
|||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
|
||||||
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
import EnumSelect from '@/components/enumselect/EnumSelect.vue';
|
||||||
import { DbDataSyncDuplicateStrategyEnum } from './enums';
|
|
||||||
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
import FormItemTooltip from '@/components/form/FormItemTooltip.vue';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { DbDataSyncDuplicateStrategyEnum } from '@/views/ops/db/sync/enums';
|
||||||
|
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||||
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -291,7 +307,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state);
|
const { tabActiveName, form, submitForm, fieldMapTableHeight } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDatasyncTask.useApi(submitForm);
|
const { isFetching: saveBtnLoading, execute: saveExec } = dbSyncApi.saveDatasyncTask.useApi(submitForm);
|
||||||
|
|
||||||
// 基础字段信息是否填写完整
|
// 基础字段信息是否填写完整
|
||||||
const baseFieldCompleted = computed(() => {
|
const baseFieldCompleted = computed(() => {
|
||||||
@@ -305,13 +321,13 @@ watch(dialogVisible, async (newValue: boolean) => {
|
|||||||
state.tabActiveName = 'basic';
|
state.tabActiveName = 'basic';
|
||||||
const propsData = props.data as any;
|
const propsData = props.data as any;
|
||||||
if (!propsData?.id) {
|
if (!propsData?.id) {
|
||||||
let d = {} as FormData;
|
let d = { taskCron: '' } as FormData;
|
||||||
Object.assign(d, basicFormData);
|
Object.assign(d, basicFormData);
|
||||||
state.form = d;
|
state.form = d;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = await dbApi.getDatasyncTask.request({ taskId: propsData?.id });
|
let data = await dbSyncApi.getDatasyncTask.request({ taskId: propsData?.id });
|
||||||
state.form = data;
|
state.form = data;
|
||||||
if (!state.form.duplicateStrategy) {
|
if (!state.form.duplicateStrategy) {
|
||||||
state.form.duplicateStrategy = -1;
|
state.form.duplicateStrategy = -1;
|
||||||
@@ -401,6 +417,7 @@ const refreshPreviewInsertSql = () => {
|
|||||||
const onSelectSrcDb = async (params: any) => {
|
const onSelectSrcDb = async (params: any) => {
|
||||||
// 初始化数据源
|
// 初始化数据源
|
||||||
params.databases = params.dbs; // 数据源里需要这个值
|
params.databases = params.dbs; // 数据源里需要这个值
|
||||||
|
console.log(params.dbs);
|
||||||
state.srcDbInst = await DbInst.getOrNewInst(params);
|
state.srcDbInst = await DbInst.getOrNewInst(params);
|
||||||
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
registerDbCompletionItemProvider(params.id, params.db, params.dbs, params.type);
|
||||||
};
|
};
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="dbApi.datasyncTasks"
|
:page-api="dbSyncApi.datasyncTasks"
|
||||||
:searchItems="searchItems"
|
:searchItems="searchItems"
|
||||||
v-model:query-form="query"
|
v-model:query-form="query"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
@@ -50,13 +50,13 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||||
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from './enums';
|
|
||||||
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nConfirm, useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
|
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||||
|
import { DbDataSyncRecentStateEnum, DbDataSyncRunningStateEnum } from '@/views/ops/db/sync/enums';
|
||||||
|
|
||||||
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
|
const DataSyncTaskEdit = defineAsyncComponent(() => import('./SyncTaskEdit.vue'));
|
||||||
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
|
const DataSyncTaskLog = defineAsyncComponent(() => import('./SyncTaskLog.vue'));
|
||||||
@@ -143,14 +143,14 @@ const edit = async (data: any) => {
|
|||||||
|
|
||||||
const run = async (id: any) => {
|
const run = async (id: any) => {
|
||||||
await useI18nConfirm('db.runConfirm');
|
await useI18nConfirm('db.runConfirm');
|
||||||
await dbApi.runDatasyncTask.request({ taskId: id });
|
await dbSyncApi.runDatasyncTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
setTimeout(search, 1000);
|
setTimeout(search, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stop = async (id: any) => {
|
const stop = async (id: any) => {
|
||||||
await useI18nConfirm('db.stopConfirm');
|
await useI18nConfirm('db.stopConfirm');
|
||||||
await dbApi.stopDatasyncTask.request({ taskId: id });
|
await dbSyncApi.stopDatasyncTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
@@ -163,7 +163,7 @@ const log = async (data: any) => {
|
|||||||
|
|
||||||
const updStatus = async (id: any, status: 1 | -1) => {
|
const updStatus = async (id: any, status: 1 | -1) => {
|
||||||
try {
|
try {
|
||||||
await dbApi.updateDatasyncTaskStatus.request({ taskId: id, status });
|
await dbSyncApi.updateDatasyncTaskStatus.request({ taskId: id, status });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -174,7 +174,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
|
|||||||
const del = async () => {
|
const del = async () => {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
||||||
await dbApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
await dbSyncApi.deleteDatasyncTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
useI18nDeleteSuccessMsg();
|
useI18nDeleteSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<el-switch v-model="realTime" @change="watchPolling" inline-prompt :active-text="$t('db.realTime')" :inactive-text="$t('db.noRealTime')" />
|
<el-switch v-model="realTime" @change="watchPolling" inline-prompt :active-text="$t('db.realTime')" :inactive-text="$t('db.noRealTime')" />
|
||||||
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button>
|
<el-button @click="search" icon="Refresh" circle size="small" :loading="realTime" class="ml-2"></el-button>
|
||||||
</template>
|
</template>
|
||||||
<page-table ref="logTableRef" :page-api="dbApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
|
<page-table ref="logTableRef" :page-api="dbSyncApi.datasyncLogs" v-model:query-form="query" :tool-button="false" :columns="columns" size="small">
|
||||||
</page-table>
|
</page-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, Ref, ref, toRefs, watch } from 'vue';
|
import { reactive, Ref, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { DbDataSyncLogStatusEnum } from './enums';
|
import { dbSyncApi } from '@/views/ops/db/sync/api';
|
||||||
|
import { DbDataSyncLogStatusEnum } from '@/views/ops/db/sync/enums';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
taskId: {
|
taskId: {
|
||||||
14
frontend/src/views/ops/db/sync/api.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Api from '@/common/Api';
|
||||||
|
import { encryptField } from '@/views/ops/db/api';
|
||||||
|
|
||||||
|
export const dbSyncApi = {
|
||||||
|
// 数据同步相关
|
||||||
|
datasyncTasks: Api.newGet('/datasync/tasks'),
|
||||||
|
saveDatasyncTask: Api.newPost('/datasync/tasks/save').withBeforeHandler(async (param: any) => await encryptField(param, 'dataSql')),
|
||||||
|
getDatasyncTask: Api.newGet('/datasync/tasks/{taskId}'),
|
||||||
|
deleteDatasyncTask: Api.newDelete('/datasync/tasks/{taskId}/del'),
|
||||||
|
updateDatasyncTaskStatus: Api.newPost('/datasync/tasks/{taskId}/status'),
|
||||||
|
runDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/run'),
|
||||||
|
stopDatasyncTask: Api.newPost('/datasync/tasks/{taskId}/stop'),
|
||||||
|
datasyncLogs: Api.newGet('/datasync/tasks/{taskId}/logs'),
|
||||||
|
};
|
||||||
24
frontend/src/views/ops/db/sync/enums.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { EnumValue } from '@/common/Enum';
|
||||||
|
|
||||||
|
export const DbDataSyncDuplicateStrategyEnum = {
|
||||||
|
None: EnumValue.of(-1, 'db.none'),
|
||||||
|
Ignore: EnumValue.of(1, 'db.ignore'),
|
||||||
|
Replace: EnumValue.of(2, 'db.replace'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbDataSyncRecentStateEnum = {
|
||||||
|
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
||||||
|
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbDataSyncLogStatusEnum = {
|
||||||
|
Success: EnumValue.of(1, 'common.success').setTagType('success'),
|
||||||
|
Running: EnumValue.of(2, 'db.running').setTagType('primary'),
|
||||||
|
Fail: EnumValue.of(-1, 'common.fail').setTagType('danger'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DbDataSyncRunningStateEnum = {
|
||||||
|
Running: EnumValue.of(1, 'db.running').setTagType('success'),
|
||||||
|
WaitRun: EnumValue.of(2, 'db.waitRun').setTagType('primary'),
|
||||||
|
Stop: EnumValue.of(3, 'db.stop').setTagType('danger'),
|
||||||
|
};
|
||||||
1
frontend/src/views/ops/db/sync/readme.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Db sync (数据库迁移模块)
|
||||||
@@ -5,44 +5,42 @@
|
|||||||
<DrawerHeader :header="title" :back="cancel" />
|
<DrawerHeader :header="title" :back="cancel" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-form :model="form" ref="dbForm" :rules="rules" label-width="auto">
|
<el-form :model="form" ref="dbForm" :rules="rules" label-position="top" label-width="auto">
|
||||||
<el-divider content-position="left">{{ $t('common.basic') }}</el-divider>
|
<el-divider content-position="left">{{ $t('common.basic') }}</el-divider>
|
||||||
|
|
||||||
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
<el-form-item prop="taskName" :label="$t('db.taskName')" required>
|
||||||
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
<el-input v-model.trim="form.taskName" auto-complete="off" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-row class="w-full!">
|
||||||
<el-row class="!w-full">
|
<el-col :span="12">
|
||||||
<el-col :span="12">
|
<el-form-item prop="status" :label="$t('common.status')" label-position="left">
|
||||||
<el-form-item prop="status" :label="$t('common.status')">
|
<el-switch
|
||||||
<el-switch
|
v-model="form.status"
|
||||||
v-model="form.status"
|
inline-prompt
|
||||||
inline-prompt
|
:active-text="$t('common.enable')"
|
||||||
:active-text="$t('common.enable')"
|
:inactive-text="$t('common.disable')"
|
||||||
:inactive-text="$t('common.disable')"
|
:active-value="1"
|
||||||
:active-value="1"
|
:inactive-value="-1"
|
||||||
:inactive-value="-1"
|
/>
|
||||||
/>
|
</el-form-item>
|
||||||
</el-form-item>
|
</el-col>
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required>
|
<el-form-item prop="cronAble" :label="$t('db.cronAble')" required label-position="left">
|
||||||
<el-radio-group v-model="form.cronAble">
|
<el-radio-group v-model="form.cronAble">
|
||||||
<el-radio :label="$t('common.yes')" :value="1" />
|
<el-radio :label="$t('common.yes')" :value="1" />
|
||||||
<el-radio :label="$t('common.no')" :value="-1" />
|
<el-radio :label="$t('common.no')" :value="-1" />
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
<el-form-item prop="cron" label="cron" :required="form.cronAble == 1">
|
||||||
<CrontabInput v-model="form.cron" />
|
<CrontabInput v-model="form.cron" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="!w-full" required>
|
<el-form-item prop="srcDbId" :label="$t('db.srcDb')" class="w-full!" required>
|
||||||
<db-select-tree
|
<db-select-tree
|
||||||
v-model:db-id="form.srcDbId"
|
v-model:db-id="form.srcDbId"
|
||||||
v-model:inst-name="form.srcInstName"
|
v-model:inst-name="form.srcInstName"
|
||||||
@@ -61,7 +59,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="form.mode === 2">
|
<el-form-item v-if="form.mode === 2">
|
||||||
<el-row class="!w-full">
|
<el-row class="w-full!">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
|
<el-form-item prop="targetFileDbType" :label="$t('db.dbFileType')" :required="form.mode === 2">
|
||||||
<el-select v-model="form.targetFileDbType" clearable filterable>
|
<el-select v-model="form.targetFileDbType" clearable filterable>
|
||||||
@@ -100,7 +98,7 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="!w-full" :required="form.mode === 1">
|
<el-form-item v-if="form.mode == 1" prop="targetDbId" :label="$t('db.targetDb')" class="w-full!" :required="form.mode === 1">
|
||||||
<db-select-tree
|
<db-select-tree
|
||||||
v-model:db-id="form.targetDbId"
|
v-model:db-id="form.targetDbId"
|
||||||
v-model:inst-name="form.targetInstName"
|
v-model:inst-name="form.targetInstName"
|
||||||
@@ -123,11 +121,11 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
|
<el-input v-model="state.filterSrcTableText" placeholder="filter table" size="small" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item class="!w-full">
|
<el-form-item class="w-full!">
|
||||||
<el-tree
|
<el-tree
|
||||||
ref="srcTreeRef"
|
ref="srcTreeRef"
|
||||||
class="!w-full"
|
class="w-full! overflow-y-auto"
|
||||||
style="max-height: 200px; overflow-y: auto"
|
style="max-height: 200px"
|
||||||
default-expand-all
|
default-expand-all
|
||||||
:expand-on-click-node="false"
|
:expand-on-click-node="false"
|
||||||
:data="state.srcTableTree"
|
:data="state.srcTableTree"
|
||||||
@@ -151,7 +149,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
|
import { nextTick, reactive, ref, toRefs, watch } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
import CrontabInput from '@/components/crontab/CrontabInput.vue';
|
||||||
@@ -162,6 +160,8 @@ import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
import { deepClone } from '@/common/utils/object';
|
import { deepClone } from '@/common/utils/object';
|
||||||
|
import { dbApi } from '@/views/ops/db/api';
|
||||||
|
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const { form, submitForm } = toRefs(state);
|
const { form, submitForm } = toRefs(state);
|
||||||
|
|
||||||
const { isFetching: saveBtnLoading, execute: saveExec } = dbApi.saveDbTransferTask.useApi(submitForm);
|
const { isFetching: saveBtnLoading, execute: saveExec } = dbTransferApi.saveDbTransferTask.useApi(submitForm);
|
||||||
|
|
||||||
watch(dialogVisible, async (newValue: boolean) => {
|
watch(dialogVisible, async (newValue: boolean) => {
|
||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
v-model:query-form="state.query"
|
v-model:query-form="state.query"
|
||||||
:page-api="dbApi.dbTransferFileList"
|
:page-api="dbTransferApi.dbTransferFileList"
|
||||||
:lazy="true"
|
:lazy="true"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
v-model:selection-data="state.selectionData"
|
v-model:selection-data="state.selectionData"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #fileKey="{ data }">
|
<template #fileKey="{ data }">
|
||||||
<FileInfo :fileKey="data.fileKey" :canDownload="actionBtns[perms.down] && data.status === 2" />
|
<FileInfo :fileKey="data.fileKey" show-file-size :canDownload="actionBtns[perms.down] && data.status === 2" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #fileDbType="{ data }">
|
<template #fileDbType="{ data }">
|
||||||
@@ -79,7 +79,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
|
import { onMounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue';
|
||||||
import { dbApi } from '@/views/ops/db/api';
|
|
||||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
@@ -89,10 +88,11 @@ import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
|||||||
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
import DbSelectTree from '@/views/ops/db/component/DbSelectTree.vue';
|
||||||
import { getClientId } from '@/common/utils/storage';
|
import { getClientId } from '@/common/utils/storage';
|
||||||
import FileInfo from '@/components/file/FileInfo.vue';
|
import FileInfo from '@/components/file/FileInfo.vue';
|
||||||
import { DbTransferFileStatusEnum } from './enums';
|
|
||||||
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Rules } from '@/common/rule';
|
import { Rules } from '@/common/rule';
|
||||||
|
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||||
|
import { DbTransferFileStatusEnum } from '@/views/ops/db/transfer/enums';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ const state = reactive({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
state.runDialog.runForm.clientId = getClientId();
|
state.runDialog.runForm.clientId = getClientId();
|
||||||
await dbApi.dbTransferFileRun.request(state.runDialog.runForm);
|
await dbTransferApi.dbTransferFileRun.request(state.runDialog.runForm);
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
state.runDialog.onCancel();
|
state.runDialog.onCancel();
|
||||||
await search();
|
await search();
|
||||||
@@ -196,7 +196,7 @@ const state = reactive({
|
|||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
pageTableRef.value?.search();
|
pageTableRef.value?.search();
|
||||||
// const { total, list } = await dbApi.dbTransferFileList.request(state.query);
|
// const { total, list } = await dbTransferApi.dbTransferFileList.request(state.query);
|
||||||
// state.tableData = list;
|
// state.tableData = list;
|
||||||
// pageTableRef.value.total = total;
|
// pageTableRef.value.total = total;
|
||||||
};
|
};
|
||||||
@@ -204,7 +204,7 @@ const search = async () => {
|
|||||||
const onDel = async function () {
|
const onDel = async function () {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
|
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.fileKey).join('、'));
|
||||||
await dbApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
|
await dbTransferApi.dbTransferFileDel.request({ fileId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
useI18nDeleteSuccessMsg();
|
useI18nDeleteSuccessMsg();
|
||||||
await search();
|
await search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="h-full">
|
<div class="h-full">
|
||||||
<page-table
|
<page-table
|
||||||
ref="pageTableRef"
|
ref="pageTableRef"
|
||||||
:page-api="dbApi.dbTransferTasks"
|
:page-api="dbTransferApi.dbTransferTasks"
|
||||||
:searchItems="searchItems"
|
:searchItems="searchItems"
|
||||||
v-model:query-form="query"
|
v-model:query-form="query"
|
||||||
:show-selection="true"
|
:show-selection="true"
|
||||||
@@ -84,19 +84,19 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
|
||||||
import { dbApi } from './api';
|
|
||||||
import PageTable from '@/components/pagetable/PageTable.vue';
|
import PageTable from '@/components/pagetable/PageTable.vue';
|
||||||
import { TableColumn } from '@/components/pagetable';
|
import { TableColumn } from '@/components/pagetable';
|
||||||
import { hasPerms } from '@/components/auth/auth';
|
import { hasPerms } from '@/components/auth/auth';
|
||||||
import { SearchItem } from '@/components/pagetable/SearchForm';
|
import { SearchItem } from '@/components/pagetable/SearchForm';
|
||||||
import { getDbDialect } from '@/views/ops/db/dialect';
|
import { getDbDialect } from '@/views/ops/db/dialect';
|
||||||
import { DbTransferRunningStateEnum } from './enums';
|
|
||||||
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
import TerminalLog from '@/components/terminal/TerminalLog.vue';
|
||||||
import DbTransferFile from './DbTransferFile.vue';
|
|
||||||
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
import { useI18nConfirm, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { dbTransferApi } from '@/views/ops/db/transfer/api';
|
||||||
|
import { DbTransferRunningStateEnum } from '@/views/ops/db/transfer/enums';
|
||||||
|
|
||||||
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
const DbTransferEdit = defineAsyncComponent(() => import('./DbTransferEdit.vue'));
|
||||||
|
const DbTransferFile = defineAsyncComponent(() => import('./DbTransferFile.vue'));
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ const edit = async (data: any) => {
|
|||||||
|
|
||||||
const stop = async (id: any) => {
|
const stop = async (id: any) => {
|
||||||
await useI18nConfirm('db.stopConfirm');
|
await useI18nConfirm('db.stopConfirm');
|
||||||
await dbApi.stopDbTransferTask.request({ taskId: id });
|
await dbTransferApi.stopDbTransferTask.request({ taskId: id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
};
|
};
|
||||||
@@ -204,7 +204,7 @@ const onOpenLog = (data: any) => {
|
|||||||
const onReRun = async (data: any) => {
|
const onReRun = async (data: any) => {
|
||||||
await useI18nConfirm('db.runConfirm');
|
await useI18nConfirm('db.runConfirm');
|
||||||
try {
|
try {
|
||||||
let res = await dbApi.runDbTransferTask.request({ taskId: data.id });
|
let res = await dbTransferApi.runDbTransferTask.request({ taskId: data.id });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
// 拿到日志id之后,弹出日志弹窗
|
// 拿到日志id之后,弹出日志弹窗
|
||||||
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
|
onOpenLog({ logId: res, state: DbTransferRunningStateEnum.Running.value });
|
||||||
@@ -225,7 +225,7 @@ const openFiles = async (data: any) => {
|
|||||||
};
|
};
|
||||||
const updStatus = async (id: any, status: 1 | -1) => {
|
const updStatus = async (id: any, status: 1 | -1) => {
|
||||||
try {
|
try {
|
||||||
await dbApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
await dbTransferApi.updateDbTransferTaskStatus.request({ taskId: id, status });
|
||||||
useI18nOperateSuccessMsg();
|
useI18nOperateSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -236,7 +236,7 @@ const updStatus = async (id: any, status: 1 | -1) => {
|
|||||||
const del = async () => {
|
const del = async () => {
|
||||||
try {
|
try {
|
||||||
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.taskName).join('、'));
|
||||||
await dbApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
await dbTransferApi.deleteDbTransferTask.request({ taskId: state.selectionData.map((x: any) => x.id).join(',') });
|
||||||
useI18nDeleteSuccessMsg();
|
useI18nDeleteSuccessMsg();
|
||||||
search();
|
search();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
16
frontend/src/views/ops/db/transfer/api.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Api from '@/common/Api';
|
||||||
|
|
||||||
|
export const dbTransferApi = {
|
||||||
|
// 数据库迁移相关
|
||||||
|
dbTransferTasks: Api.newGet('/dbTransfer'),
|
||||||
|
saveDbTransferTask: Api.newPost('/dbTransfer/save'),
|
||||||
|
deleteDbTransferTask: Api.newDelete('/dbTransfer/{taskId}/del'),
|
||||||
|
updateDbTransferTaskStatus: Api.newPost('/dbTransfer/{taskId}/status'),
|
||||||
|
runDbTransferTask: Api.newPost('/dbTransfer/{taskId}/run'),
|
||||||
|
stopDbTransferTask: Api.newPost('/dbTransfer/{taskId}/stop'),
|
||||||
|
dbTransferTaskLogs: Api.newGet('/dbTransfer/{taskId}/logs'),
|
||||||
|
dbTransferFileList: Api.newGet('/dbTransfer/files/{taskId}'),
|
||||||
|
dbTransferFileDel: Api.newPost('/dbTransfer/files/del/{fileId}'),
|
||||||
|
dbTransferFileRun: Api.newPost('/dbTransfer/files/run'),
|
||||||
|
dbTransferFileDown: Api.newGet('/dbTransfer/files/down/{fileUuid}'),
|
||||||
|
};
|
||||||