Compare commits
	
		
			12 Commits
		
	
	
		
			614a144f60
			...
			dev
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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.3",
 | 
				
			||||||
        "@logicflow/extension": "^2.0.21",
 | 
					        "@logicflow/extension": "^2.1.5",
 | 
				
			||||||
        "@vueuse/core": "^13.6.0",
 | 
					        "@vueuse/core": "^13.9.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.11.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.18",
 | 
				
			||||||
        "echarts": "^6.0.0",
 | 
					        "echarts": "^6.0.0",
 | 
				
			||||||
        "element-plus": "^2.10.5",
 | 
					        "element-plus": "^2.11.4",
 | 
				
			||||||
        "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.54.0",
 | 
				
			||||||
        "monaco-sql-languages": "^0.15.1",
 | 
					        "monaco-sql-languages": "^0.15.1",
 | 
				
			||||||
        "monaco-themes": "^0.4.6",
 | 
					        "monaco-themes": "^0.4.7",
 | 
				
			||||||
        "nprogress": "^0.2.0",
 | 
					        "nprogress": "^0.2.0",
 | 
				
			||||||
        "pinia": "^3.0.3",
 | 
					        "pinia": "^3.0.3",
 | 
				
			||||||
        "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.8",
 | 
				
			||||||
        "trzsz": "^1.1.5",
 | 
					        "trzsz": "^1.1.5",
 | 
				
			||||||
        "uuid": "^11.1.0",
 | 
					        "uuid": "^13.0.0",
 | 
				
			||||||
        "vue": "^v3.6.0-alpha.2",
 | 
					        "vue": "^v3.5.22",
 | 
				
			||||||
        "vue-i18n": "^11.1.11",
 | 
					        "vue-i18n": "^11.1.12",
 | 
				
			||||||
        "vue-router": "^4.5.1",
 | 
					        "vue-router": "^4.6.3",
 | 
				
			||||||
        "vuedraggable": "^4.1.0"
 | 
					        "vuedraggable": "^4.1.0"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "devDependencies": {
 | 
					    "devDependencies": {
 | 
				
			||||||
        "@tailwindcss/vite": "^4.1.11",
 | 
					        "@tailwindcss/vite": "^4.1.14",
 | 
				
			||||||
        "@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",
 | 
				
			||||||
@@ -56,11 +56,11 @@
 | 
				
			|||||||
        "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.93.2",
 | 
				
			||||||
        "tailwindcss": "^4.1.11",
 | 
					        "tailwindcss": "^4.1.14",
 | 
				
			||||||
        "typescript": "^5.9.2",
 | 
					        "typescript": "^5.9.2",
 | 
				
			||||||
        "vite": "npm:rolldown-vite@latest",
 | 
					        "vite": "npm:rolldown-vite@latest",
 | 
				
			||||||
        "vite-plugin-progress": "0.0.7",
 | 
					        "vite-plugin-progress": "0.0.7",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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.4',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default config;
 | 
					export default config;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -155,6 +155,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');
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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>
 | 
				
			||||||
@@ -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!`,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,7 +65,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',
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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',
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -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正在执行, 请使用新标签执行',
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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: '索引详情',
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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: '授权凭证密文查看',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -654,8 +654,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() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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') {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,38 +5,36 @@
 | 
				
			|||||||
                <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" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
@@ -305,7 +320,7 @@ 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;
 | 
				
			||||||
@@ -401,6 +416,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);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,7 +201,7 @@
 | 
				
			|||||||
</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';
 | 
				
			||||||
@@ -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  **/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -438,6 +456,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 +536,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 +647,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(); // 阻止默认的右击菜单行为
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -851,6 +918,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"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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'),
 | 
					 | 
				
			||||||
    SqlExec: () => import('@/views/ops/db/SqlExec.vue'),
 | 
					 | 
				
			||||||
    SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
 | 
					    SyncTaskList: () => import('@/views/ops/db/SyncTaskList.vue'),
 | 
				
			||||||
    DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
 | 
					    DbTransferList: () => import('@/views/ops/db/DbTransferList.vue'),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										166
									
								
								frontend/src/views/ops/docker/ContainerConfList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,166 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="h-full">
 | 
				
			||||||
 | 
					        <page-table
 | 
				
			||||||
 | 
					            ref="pageTableRef"
 | 
				
			||||||
 | 
					            :page-api="dockerApi.page"
 | 
				
			||||||
 | 
					            :before-query-fn="checkRouteTagPath"
 | 
				
			||||||
 | 
					            :searchItems="searchItems"
 | 
				
			||||||
 | 
					            v-model:query-form="query"
 | 
				
			||||||
 | 
					            :show-selection="true"
 | 
				
			||||||
 | 
					            v-model:selection-data="selectionData"
 | 
				
			||||||
 | 
					            :columns="columns"
 | 
				
			||||||
 | 
					            lazy
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <template #tableHeader>
 | 
				
			||||||
 | 
					                <el-button type="primary" icon="plus" @click="editContainerConf(false)" plain>{{ $t('common.create') }}</el-button>
 | 
				
			||||||
 | 
					                <el-button type="danger" icon="delete" :disabled="selectionData.length < 1" @click="deleteConf" plain>{{ $t('common.delete') }}</el-button>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template #tagPath="{ data }">
 | 
				
			||||||
 | 
					                <resource-tags :tags="data.tags" />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template #action="{ data }">
 | 
				
			||||||
 | 
					                <el-button @click="showDetail(data)" link>{{ $t('common.detail') }}</el-button>
 | 
				
			||||||
 | 
					                <el-button type="primary" link @click="editContainerConf(data)">{{ $t('common.edit') }}</el-button>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </page-table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-dialog v-if="detailDialog.visible" v-model="detailDialog.visible">
 | 
				
			||||||
 | 
					            <el-descriptions :title="$t('common.detail')" :column="3" border>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1.5" label="id">{{ detailDialog.data.id }}</el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1.5" :label="$t('common.name')">{{ detailDialog.data.name }}</el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="3" :label="$t('tag.relateTag')"><ResourceTags :tags="detailDialog.data.tags" /></el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="3" :label="$t('docker.addr')">{{ detailDialog.data.addr }}</el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="3" :label="$t('common.remark')">{{ detailDialog.data.remark }}</el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="2" :label="$t('common.createTime')">{{ formatDate(detailDialog.data.createTime) }} </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1" :label="$t('common.creator')">{{ detailDialog.data.creator }}</el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="2" :label="$t('common.updateTime')">{{ formatDate(detailDialog.data.updateTime) }} </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1" :label="$t('common.modifier')">{{ detailDialog.data.modifier }}</el-descriptions-item>
 | 
				
			||||||
 | 
					            </el-descriptions>
 | 
				
			||||||
 | 
					        </el-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ContainerConfEdit
 | 
				
			||||||
 | 
					            @val-change="search()"
 | 
				
			||||||
 | 
					            :title="containerConfEditDialog.title"
 | 
				
			||||||
 | 
					            v-model:visible="containerConfEditDialog.visible"
 | 
				
			||||||
 | 
					            v-model:container="containerConfEditDialog.data"
 | 
				
			||||||
 | 
					        ></ContainerConfEdit>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { dockerApi } from './api';
 | 
				
			||||||
 | 
					import { defineAsyncComponent, onMounted, reactive, ref, Ref, toRefs } from 'vue';
 | 
				
			||||||
 | 
					import { formatDate } from '@/common/utils/format';
 | 
				
			||||||
 | 
					import ResourceTags from '../component/ResourceTags.vue';
 | 
				
			||||||
 | 
					import PageTable from '@/components/pagetable/PageTable.vue';
 | 
				
			||||||
 | 
					import { TableColumn } from '@/components/pagetable';
 | 
				
			||||||
 | 
					import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
				
			||||||
 | 
					import { useRoute } from 'vue-router';
 | 
				
			||||||
 | 
					import { getTagPathSearchItem } from '../component/tag';
 | 
				
			||||||
 | 
					import { SearchItem } from '@/components/pagetable/SearchForm';
 | 
				
			||||||
 | 
					import { useI18nCreateTitle, useI18nDeleteConfirm, useI18nDeleteSuccessMsg, useI18nEditTitle } from '@/hooks/useI18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContainerConfEdit = defineAsyncComponent(() => import('./CotainerConfEdit.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					    lazy: {
 | 
				
			||||||
 | 
					        type: [Boolean],
 | 
				
			||||||
 | 
					        default: false,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const route = useRoute();
 | 
				
			||||||
 | 
					const pageTableRef: Ref<any> = ref(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const searchItems = [
 | 
				
			||||||
 | 
					    SearchItem.input('keyword', 'common.keyword').withPlaceholder('redis.keywordPlaceholder'),
 | 
				
			||||||
 | 
					    getTagPathSearchItem(TagResourceTypeEnum.Container.value),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const columns = ref([
 | 
				
			||||||
 | 
					    TableColumn.new('tags[0].tagPath', 'tag.relateTag').isSlot('tagPath').setAddWidth(20),
 | 
				
			||||||
 | 
					    TableColumn.new('name', 'common.name'),
 | 
				
			||||||
 | 
					    TableColumn.new('addr', 'docker.addr'),
 | 
				
			||||||
 | 
					    TableColumn.new('remark', 'common.remark'),
 | 
				
			||||||
 | 
					    TableColumn.new('code', 'common.code'),
 | 
				
			||||||
 | 
					    TableColumn.new('action', 'common.operation').isSlot().setMinWidth(200).fixedRight().alignCenter(),
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    selectionData: [],
 | 
				
			||||||
 | 
					    query: {
 | 
				
			||||||
 | 
					        tagPath: '',
 | 
				
			||||||
 | 
					        pageNum: 1,
 | 
				
			||||||
 | 
					        pageSize: 0,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    detailDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        data: null as any,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    containerConfEditDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        data: null as any,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { selectionData, query, detailDialog, containerConfEditDialog } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					    if (!props.lazy) {
 | 
				
			||||||
 | 
					        search();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const checkRouteTagPath = (query: any) => {
 | 
				
			||||||
 | 
					    if (route.query.tagPath) {
 | 
				
			||||||
 | 
					        query.tagPath = route.query.tagPath as string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return query;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showDetail = (detail: any) => {
 | 
				
			||||||
 | 
					    state.detailDialog.data = detail;
 | 
				
			||||||
 | 
					    state.detailDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const deleteConf = async () => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        await useI18nDeleteConfirm(state.selectionData.map((x: any) => x.name).join('、'));
 | 
				
			||||||
 | 
					        await dockerApi.delConf.request({ id: state.selectionData.map((x: any) => x.id).join(',') });
 | 
				
			||||||
 | 
					        useI18nDeleteSuccessMsg();
 | 
				
			||||||
 | 
					        search();
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const search = async (tagPath: string = '') => {
 | 
				
			||||||
 | 
					    if (tagPath) {
 | 
				
			||||||
 | 
					        state.query.tagPath = tagPath;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pageTableRef.value.search();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const editContainerConf = async (data: any) => {
 | 
				
			||||||
 | 
					    if (!data) {
 | 
				
			||||||
 | 
					        state.containerConfEditDialog.data = null;
 | 
				
			||||||
 | 
					        state.containerConfEditDialog.title = useI18nCreateTitle('docker.containerConf');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        state.containerConfEditDialog.data = data;
 | 
				
			||||||
 | 
					        state.containerConfEditDialog.title = useI18nEditTitle('docker.containerConf');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    state.containerConfEditDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineExpose({ search });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style></style>
 | 
				
			||||||
							
								
								
									
										115
									
								
								frontend/src/views/ops/docker/CotainerConfEdit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,115 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					        <el-drawer :title="title" v-model="dialogVisible" :before-close="onCancel" :destroy-on-close="true" :close-on-click-modal="false" size="40%">
 | 
				
			||||||
 | 
					            <template #header>
 | 
				
			||||||
 | 
					                <DrawerHeader :header="title" :back="onCancel" />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form :model="form" ref="formRef" :rules="rules" label-width="auto">
 | 
				
			||||||
 | 
					                <el-form-item prop="tagCodePaths" :label="$t('tag.relateTag')" required>
 | 
				
			||||||
 | 
					                    <tag-tree-select multiple v-model="form.tagCodePaths" />
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item prop="name" :label="$t('common.name')" required>
 | 
				
			||||||
 | 
					                    <el-input v-model.trim="form.name" auto-complete="off"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item prop="addr" :label="$t('docker.addr')" required>
 | 
				
			||||||
 | 
					                    <el-input v-model.trim="form.addr" :placeholder="$t('docker.addrTips')" auto-complete="off" type="textarea"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item prop="remark" :label="$t('common.remark')">
 | 
				
			||||||
 | 
					                    <el-input v-model.trim="form.remark" auto-complete="off" type="textarea"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					            </el-form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <template #footer>
 | 
				
			||||||
 | 
					                <!-- <el-button @click="onTestConn" :loading="testConnBtnLoading" type="success">{{ $t('ac.testConn') }}</el-button> -->
 | 
				
			||||||
 | 
					                <el-button @click="onCancel()">{{ $t('common.cancel') }}</el-button>
 | 
				
			||||||
 | 
					                <el-button type="primary" :loading="saveBtnLoading" @click="onConfirm">{{ $t('common.confirm') }}</el-button>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-drawer>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { toRefs, reactive, watch, useTemplateRef } from 'vue';
 | 
				
			||||||
 | 
					import { dockerApi } from './api';
 | 
				
			||||||
 | 
					import { ElMessage } from 'element-plus';
 | 
				
			||||||
 | 
					import TagTreeSelect from '../component/TagTreeSelect.vue';
 | 
				
			||||||
 | 
					import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
				
			||||||
 | 
					import { useI18nFormValidate, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
 | 
				
			||||||
 | 
					import { useI18n } from 'vue-i18n';
 | 
				
			||||||
 | 
					import { Rules } from '@/common/rule';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { t } = useI18n();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					    container: {
 | 
				
			||||||
 | 
					        type: [Boolean, Object],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    title: {
 | 
				
			||||||
 | 
					        type: String,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dialogVisible = defineModel<boolean>('visible', { default: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits(['val-change', 'cancel']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rules = {
 | 
				
			||||||
 | 
					    tagCodePaths: [Rules.requiredSelect('tag.relateTag')],
 | 
				
			||||||
 | 
					    name: [Rules.requiredInput('common.name')],
 | 
				
			||||||
 | 
					    addr: [Rules.requiredInput('addr')],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const formRef: any = useTemplateRef('formRef');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    form: {
 | 
				
			||||||
 | 
					        id: null,
 | 
				
			||||||
 | 
					        code: '',
 | 
				
			||||||
 | 
					        tagCodePaths: [],
 | 
				
			||||||
 | 
					        name: null,
 | 
				
			||||||
 | 
					        addr: '',
 | 
				
			||||||
 | 
					        remark: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    dbList: [0],
 | 
				
			||||||
 | 
					    pwd: '',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { form } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { isFetching: saveBtnLoading, execute: saveConfExec } = dockerApi.saveConf.useApi(form);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(dialogVisible, () => {
 | 
				
			||||||
 | 
					    if (!dialogVisible.value) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const container: any = props.container;
 | 
				
			||||||
 | 
					    if (container) {
 | 
				
			||||||
 | 
					        state.form = { ...container };
 | 
				
			||||||
 | 
					        state.form.tagCodePaths = container.tags.map((t: any) => t.codePath);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        state.form = {} as any;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onTestConn = async () => {
 | 
				
			||||||
 | 
					    await useI18nFormValidate(formRef);
 | 
				
			||||||
 | 
					    // await testConnExec();
 | 
				
			||||||
 | 
					    ElMessage.success(t('ac.connSuccess'));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onConfirm = async () => {
 | 
				
			||||||
 | 
					    await useI18nFormValidate(formRef);
 | 
				
			||||||
 | 
					    await saveConfExec();
 | 
				
			||||||
 | 
					    useI18nSaveSuccessMsg();
 | 
				
			||||||
 | 
					    emit('val-change', state.form);
 | 
				
			||||||
 | 
					    onCancel();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onCancel = () => {
 | 
				
			||||||
 | 
					    dialogVisible.value = false;
 | 
				
			||||||
 | 
					    emit('cancel');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					<style lang="scss"></style>
 | 
				
			||||||
							
								
								
									
										42
									
								
								frontend/src/views/ops/docker/DockerPanel.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="card h-full">
 | 
				
			||||||
 | 
					        <el-tabs v-model="activeName" @tab-change="handleTabChange">
 | 
				
			||||||
 | 
					            <el-tab-pane :label="$t('docker.container')" :name="containerTab">
 | 
				
			||||||
 | 
					                <ContainerList :host="props.host" />
 | 
				
			||||||
 | 
					            </el-tab-pane>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-tab-pane :label="$t('docker.image')" :name="imageTab">
 | 
				
			||||||
 | 
					                <ImageList v-if="activeName == imageTab" :host="props.host" />
 | 
				
			||||||
 | 
					            </el-tab-pane>
 | 
				
			||||||
 | 
					        </el-tabs>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { toRefs, reactive, onMounted, defineAsyncComponent } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContainerList = defineAsyncComponent(() => import('./container/ContainerList.vue'));
 | 
				
			||||||
 | 
					const ImageList = defineAsyncComponent(() => import('./image/ImageList.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					    host: {
 | 
				
			||||||
 | 
					        type: String,
 | 
				
			||||||
 | 
					        required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const containerTab = 'containerTab';
 | 
				
			||||||
 | 
					const imageTab = 'imageTab';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    activeName: containerTab,
 | 
				
			||||||
 | 
					    cmdConfs: [],
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { activeName } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(async () => {
 | 
				
			||||||
 | 
					    state.activeName = containerTab;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleTabChange = (tabName: any) => {};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										39
									
								
								frontend/src/views/ops/docker/DockerPanelDrawer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					        <el-drawer title="Docker" v-model="dialogVisible" :before-close="cancel" :destroy-on-close="true" :close-on-click-modal="true" size="80%">
 | 
				
			||||||
 | 
					            <template #header>
 | 
				
			||||||
 | 
					                <DrawerHeader :header="props.host" :back="cancel">
 | 
				
			||||||
 | 
					                    <template #extra>
 | 
				
			||||||
 | 
					                        <div class="mr20"></div>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                </DrawerHeader>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <DockerPanel :host="props.host" />
 | 
				
			||||||
 | 
					        </el-drawer>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { defineAsyncComponent, ref, Ref } from 'vue';
 | 
				
			||||||
 | 
					import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DockerPanel = defineAsyncComponent(() => import('./DockerPanel.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					    host: {
 | 
				
			||||||
 | 
					        type: String,
 | 
				
			||||||
 | 
					        required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dialogVisible = defineModel<boolean>('visible');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits(['cancel']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const cancel = () => {
 | 
				
			||||||
 | 
					    dialogVisible.value = false;
 | 
				
			||||||
 | 
					    emit('cancel');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					<style lang="scss"></style>
 | 
				
			||||||
							
								
								
									
										31
									
								
								frontend/src/views/ops/docker/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					import Api from '@/common/Api';
 | 
				
			||||||
 | 
					import config from '@/common/config';
 | 
				
			||||||
 | 
					import { joinClientParams } from '@/common/request';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const dockerApi = {
 | 
				
			||||||
 | 
					    page: Api.newGet('/docker/container-conf/page'),
 | 
				
			||||||
 | 
					    saveConf: Api.newPost('/docker/container-conf/save'),
 | 
				
			||||||
 | 
					    delConf: Api.newDelete('/docker/container-conf/del/{id}'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    info: Api.newGet('/docker/{id}/info'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    containers: Api.newGet('/docker/{id}/containers'),
 | 
				
			||||||
 | 
					    containersStats: Api.newGet('/docker/{id}/containers/stats'),
 | 
				
			||||||
 | 
					    containerStop: Api.newPost('/docker/{id}/containers/stop'),
 | 
				
			||||||
 | 
					    containerRemove: Api.newPost('/docker/{id}/containers/remove'),
 | 
				
			||||||
 | 
					    containerRestart: Api.newPost('/docker/{id}/containers/restart'),
 | 
				
			||||||
 | 
					    containerCreate: Api.newPost('/docker/{id}/containers/create'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    images: Api.newGet('/docker/{id}/images'),
 | 
				
			||||||
 | 
					    imageRemove: Api.newPost('/docker/{id}/images/remove'),
 | 
				
			||||||
 | 
					    imageSave: Api.newPost('/docker/{id}/images/save'),
 | 
				
			||||||
 | 
					    imageUpload: Api.newPost('/docker/{id}/images/load'),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getDockerExecSocketUrl(id: number, containerId: string) {
 | 
				
			||||||
 | 
					    return `/docker/${id}/containers/exec?id=${id}&containerId=${containerId}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getContainerLogSocketUrl(id: number, containerId: string) {
 | 
				
			||||||
 | 
					    return `${config.baseWsUrl}/docker/${id}/containers/logs?${joinClientParams()}&id=${id}&containerId=${containerId}`;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										449
									
								
								frontend/src/views/ops/docker/container/ContainerCreate.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,449 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <el-drawer v-model="dialogVisible" :append-to-body="true" :destroy-on-close="true" :close-on-click-modal="false" :before-close="cancel" size="40%">
 | 
				
			||||||
 | 
					        <template #header>
 | 
				
			||||||
 | 
					            <DrawerHeader :header="$t('docker.createContainer')" :back="cancel">
 | 
				
			||||||
 | 
					                <template #extra>
 | 
				
			||||||
 | 
					                    <div class="mr20"></div>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					            </DrawerHeader>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-form :model="form" ref="formRef" label-position="top" :rules="rules" scroll-to-error>
 | 
				
			||||||
 | 
					            <el-form-item prop="name" :label="$t('common.name')" clearable>
 | 
				
			||||||
 | 
					                <el-input v-model.trim="form.name" auto-complete="off"></el-input>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item prop="image" :label="$t('docker.image')">
 | 
				
			||||||
 | 
					                <template #label>
 | 
				
			||||||
 | 
					                    {{ $t('docker.image') }}
 | 
				
			||||||
 | 
					                    <el-tooltip :content="$t('docker.imageTips')" placement="top">
 | 
				
			||||||
 | 
					                        <SvgIcon class="mb-1" name="question-filled" />
 | 
				
			||||||
 | 
					                    </el-tooltip>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-select v-model="form.image" filterable allow-create>
 | 
				
			||||||
 | 
					                    <el-option v-for="item in state.images" :key="item.id" :label="item.tags[0]" :value="item.tags[0]"></el-option>
 | 
				
			||||||
 | 
					                </el-select>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item>
 | 
				
			||||||
 | 
					                <el-checkbox v-model="form.forcePull">{{ $t('docker.forcePull') }}</el-checkbox>
 | 
				
			||||||
 | 
					                <el-tooltip :content="$t('docker.forcePullTips')" placement="top">
 | 
				
			||||||
 | 
					                    <SvgIcon class="ml-2" name="question-filled" />
 | 
				
			||||||
 | 
					                </el-tooltip>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item prop="cmdStr" :label="$t('Command')">
 | 
				
			||||||
 | 
					                <el-input v-model="form.cmdStr" />
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.port')">
 | 
				
			||||||
 | 
					                <el-card class="w-full">
 | 
				
			||||||
 | 
					                    <el-table v-if="form.exposedPorts.length !== 0" :data="form.exposedPorts">
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.server')" min-width="100">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-input-number v-model="row.hostPort" :min="10000" :max="20000" />
 | 
				
			||||||
 | 
					                                <!-- <el-input v-model="row.hostPort" :placeholder="$t('docker.hostPortPlaceholder')" /> -->
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.container')" min-width="100">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-input v-model="row.containerPort" />
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.protocol')" min-width="50">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-select v-model="row.protocol" style="width: 100%" :placeholder="$t('container.serverExample')">
 | 
				
			||||||
 | 
					                                    <el-option label="tcp" value="tcp" />
 | 
				
			||||||
 | 
					                                    <el-option label="udp" value="udp" />
 | 
				
			||||||
 | 
					                                </el-select>
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column min-width="35">
 | 
				
			||||||
 | 
					                            <template #default="scope">
 | 
				
			||||||
 | 
					                                <el-button link type="primary" @click="handlePortsDelete(scope.$index)">
 | 
				
			||||||
 | 
					                                    {{ $t('common.delete') }}
 | 
				
			||||||
 | 
					                                </el-button>
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					                    </el-table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <el-button class="ml-1 mt-1" size="small" @click="handlePortsAdd()">
 | 
				
			||||||
 | 
					                        {{ $t('common.add') }}
 | 
				
			||||||
 | 
					                    </el-button>
 | 
				
			||||||
 | 
					                </el-card>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item prop="mount" :label="$t('docker.mount')">
 | 
				
			||||||
 | 
					                <el-card class="mb-1 w-full">
 | 
				
			||||||
 | 
					                    <el-table v-if="form.volumes.length !== 0" :data="form.volumes">
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.hostDir')" min-width="120">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-input v-model="row.hostDir" />
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.permission')" :width="100">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-select v-model="row.mode">
 | 
				
			||||||
 | 
					                                    <el-option value="rw" :label="$t('docker.rw')" />
 | 
				
			||||||
 | 
					                                    <el-option value="ro" :label="$t('docker.ro')" />
 | 
				
			||||||
 | 
					                                </el-select>
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.containerDir')" min-width="120">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-input v-model="row.containerDir" />
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column min-width="40">
 | 
				
			||||||
 | 
					                            <template #default="scope">
 | 
				
			||||||
 | 
					                                <el-button link type="primary" @click="handleVolumesDelete(scope.$index)">
 | 
				
			||||||
 | 
					                                    {{ $t('common.delete') }}
 | 
				
			||||||
 | 
					                                </el-button>
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					                    </el-table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <el-button @click="handleVolumesAdd()" size="small">
 | 
				
			||||||
 | 
					                        {{ $t('common.add') }}
 | 
				
			||||||
 | 
					                    </el-button>
 | 
				
			||||||
 | 
					                </el-card>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.networkMode')">
 | 
				
			||||||
 | 
					                <el-select v-model="form.networkMode" filterable allow-create>
 | 
				
			||||||
 | 
					                    <el-option label="default" value="default"></el-option>
 | 
				
			||||||
 | 
					                    <el-option label="host" value="host"></el-option>
 | 
				
			||||||
 | 
					                    <el-option label="bridge" value="bridge"></el-option>
 | 
				
			||||||
 | 
					                    <el-option label="none" value="none"></el-option>
 | 
				
			||||||
 | 
					                </el-select>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.otherOption')">
 | 
				
			||||||
 | 
					                <el-checkbox v-model="form.tty">{{ $t('docker.tty') }}</el-checkbox>
 | 
				
			||||||
 | 
					                <el-checkbox v-model="form.openStdin">
 | 
				
			||||||
 | 
					                    {{ $t('docker.openStdin') }}
 | 
				
			||||||
 | 
					                </el-checkbox>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-checkbox v-model="form.privileged">
 | 
				
			||||||
 | 
					                    {{ $t('docker.privileged') }}
 | 
				
			||||||
 | 
					                </el-checkbox>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.restartPolicy')" prop="restartPolicy">
 | 
				
			||||||
 | 
					                <el-radio-group v-model="form.restartPolicy">
 | 
				
			||||||
 | 
					                    <el-radio value="no">{{ $t('docker.noRestart') }}</el-radio>
 | 
				
			||||||
 | 
					                    <el-radio value="always">{{ $t('docker.alwaysRestart') }}</el-radio>
 | 
				
			||||||
 | 
					                    <el-radio value="on-failure">{{ $t('docker.onFailure') }}</el-radio>
 | 
				
			||||||
 | 
					                    <el-radio value="unless-stopped">{{ $t('docker.unlessStopped') }}</el-radio>
 | 
				
			||||||
 | 
					                </el-radio-group>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.cpuShare')" prop="cpuShares">
 | 
				
			||||||
 | 
					                <template #label>
 | 
				
			||||||
 | 
					                    <el-row>
 | 
				
			||||||
 | 
					                        {{ $t('docker.cpuShare') }}
 | 
				
			||||||
 | 
					                        <el-tooltip :content="$t('docker.cpuShareTips')" placement="top">
 | 
				
			||||||
 | 
					                            <SvgIcon class="ml-2" name="question-filled" />
 | 
				
			||||||
 | 
					                        </el-tooltip>
 | 
				
			||||||
 | 
					                    </el-row>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					                <el-input v-model.number="form.cpuShares" />
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item prop="nanoCPUs">
 | 
				
			||||||
 | 
					                <template #label>
 | 
				
			||||||
 | 
					                    <el-row>
 | 
				
			||||||
 | 
					                        {{ $t('docker.cpuQuota') }}
 | 
				
			||||||
 | 
					                        <el-tooltip :content="$t('docker.cpuLimitTips')" placement="top">
 | 
				
			||||||
 | 
					                            <SvgIcon class="ml-2" name="question-filled" />
 | 
				
			||||||
 | 
					                        </el-tooltip>
 | 
				
			||||||
 | 
					                        <el-text class="ml-2" size="small">{{ $t('docker.cpuCanUseTips', { cpuTotal: dockerInfo.NCPU }) }}</el-text>
 | 
				
			||||||
 | 
					                    </el-row>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-input v-model.number="form.nanoCpus">
 | 
				
			||||||
 | 
					                    <template #append>
 | 
				
			||||||
 | 
					                        <div style="width: 35px">{{ $t('docker.core') }}</div>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                </el-input>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.memoryLimit')" prop="memory">
 | 
				
			||||||
 | 
					                <template #label>
 | 
				
			||||||
 | 
					                    <el-row>
 | 
				
			||||||
 | 
					                        {{ $t('docker.memoryLimit') }}
 | 
				
			||||||
 | 
					                        <el-tooltip :content="$t('docker.memoryLimitTips')" placement="top">
 | 
				
			||||||
 | 
					                            <SvgIcon class="ml-2" name="question-filled" />
 | 
				
			||||||
 | 
					                        </el-tooltip>
 | 
				
			||||||
 | 
					                        <el-text class="ml-2" size="small">{{ $t('docker.memoryCanUseTips', { memTotal: formatByteSize(dockerInfo.MemTotal) }) }}</el-text>
 | 
				
			||||||
 | 
					                    </el-row>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-input v-model.number="form.memory">
 | 
				
			||||||
 | 
					                    <template #append><div style="width: 35px">GB</div></template>
 | 
				
			||||||
 | 
					                </el-input>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.shmSize')" prop="memory">
 | 
				
			||||||
 | 
					                <el-input v-model.number="form.shmSize">
 | 
				
			||||||
 | 
					                    <template #append><div style="width: 35px">GB</div></template>
 | 
				
			||||||
 | 
					                </el-input>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item prop="device" :label="$t('docker.device')">
 | 
				
			||||||
 | 
					                <el-card class="mb-1 w-full">
 | 
				
			||||||
 | 
					                    <el-table v-if="form.devices.length !== 0" :data="form.devices">
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.driver')" min-width="100">
 | 
				
			||||||
 | 
					                            <template #header>
 | 
				
			||||||
 | 
					                                {{ $t('docker.driver') }}
 | 
				
			||||||
 | 
					                                <el-tooltip :content="$t('docker.driverTips')" placement="top">
 | 
				
			||||||
 | 
					                                    <SvgIcon class="ml-2 mb-2" name="question-filled" />
 | 
				
			||||||
 | 
					                                </el-tooltip>
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-select v-model="row.driver" filterable allow-create>
 | 
				
			||||||
 | 
					                                    <el-option v-for="item in runtimeSelect" :key="item" :label="item" :value="item"></el-option>
 | 
				
			||||||
 | 
					                                </el-select>
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.count')" :width="100">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-input v-model.number="row.count" />
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.capabilitie')" min-width="100">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-input-tag v-model="row.capabilities" :placeholder="$t('docker.capabilitiePlaceholder')" />
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column :label="$t('docker.deviceId')" min-width="100">
 | 
				
			||||||
 | 
					                            <template #default="{ row }">
 | 
				
			||||||
 | 
					                                <el-input-tag v-model="row.deviceIds" />
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-table-column min-width="35">
 | 
				
			||||||
 | 
					                            <template #default="scope">
 | 
				
			||||||
 | 
					                                <el-button class="mt-1" link type="primary" @click="handleDevicesDelete(scope.$index)">
 | 
				
			||||||
 | 
					                                    {{ $t('common.delete') }}
 | 
				
			||||||
 | 
					                                </el-button>
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					                        </el-table-column>
 | 
				
			||||||
 | 
					                    </el-table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <el-button @click="handleDevicesAdd()" size="small">
 | 
				
			||||||
 | 
					                        {{ $t('common.add') }}
 | 
				
			||||||
 | 
					                    </el-button>
 | 
				
			||||||
 | 
					                </el-card>
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('capAdd')" prop="capAdd">
 | 
				
			||||||
 | 
					                <el-input-tag v-model="form.capAdd" />
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.tag')" prop="labelsStr">
 | 
				
			||||||
 | 
					                <el-input type="textarea" :placeholder="$t('docker.tagTips')" :rows="3" v-model="form.labelsStr" />
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-form-item :label="$t('docker.envParam')" prop="envStr">
 | 
				
			||||||
 | 
					                <el-input type="textarea" :placeholder="$t('docker.envParamTips')" :rows="3" v-model="form.envsStr" />
 | 
				
			||||||
 | 
					            </el-form-item>
 | 
				
			||||||
 | 
					        </el-form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <template #footer>
 | 
				
			||||||
 | 
					            <el-button @click="cancel()">{{ $t('common.cancel') }}</el-button>
 | 
				
			||||||
 | 
					            <el-button type="primary" :loading="createLoading" @click="btnOk">{{ $t('common.confirm') }}</el-button>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					    </el-drawer>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { useI18nFormValidate, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
 | 
				
			||||||
 | 
					import { computed, reactive, toRefs, useTemplateRef, watch } from 'vue';
 | 
				
			||||||
 | 
					import { dockerApi } from '../api';
 | 
				
			||||||
 | 
					import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
				
			||||||
 | 
					import { Rules } from '@/common/rule';
 | 
				
			||||||
 | 
					import { formatByteSize } from '@/common/utils/format';
 | 
				
			||||||
 | 
					import { deepClone } from '@/common/utils/object';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rules = {
 | 
				
			||||||
 | 
					    name: [Rules.requiredInput('common.name')],
 | 
				
			||||||
 | 
					    image: [Rules.requiredSelect('docker.image')],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					    id: {
 | 
				
			||||||
 | 
					        type: Number,
 | 
				
			||||||
 | 
					        required: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultForm = {
 | 
				
			||||||
 | 
					    name: '',
 | 
				
			||||||
 | 
					    image: '',
 | 
				
			||||||
 | 
					    cmdStr: '',
 | 
				
			||||||
 | 
					    forcePull: false,
 | 
				
			||||||
 | 
					    exposedPorts: [] as any,
 | 
				
			||||||
 | 
					    networkMode: 'default',
 | 
				
			||||||
 | 
					    volumes: [] as any,
 | 
				
			||||||
 | 
					    devices: [] as any,
 | 
				
			||||||
 | 
					    capAdd: [] as any,
 | 
				
			||||||
 | 
					    tty: false,
 | 
				
			||||||
 | 
					    openStdin: false,
 | 
				
			||||||
 | 
					    privileged: false,
 | 
				
			||||||
 | 
					    restartPolicy: '',
 | 
				
			||||||
 | 
					    cpuShares: 1024,
 | 
				
			||||||
 | 
					    nanoCpus: 0,
 | 
				
			||||||
 | 
					    memory: 0,
 | 
				
			||||||
 | 
					    shmSize: 0,
 | 
				
			||||||
 | 
					    labelsStr: '',
 | 
				
			||||||
 | 
					    envsStr: '',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    dockerInfo: {} as any,
 | 
				
			||||||
 | 
					    images: [] as any,
 | 
				
			||||||
 | 
					    form: defaultForm,
 | 
				
			||||||
 | 
					    submitForm: {} as any,
 | 
				
			||||||
 | 
					    pwd: '',
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { dockerInfo, form, submitForm } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//定义事件
 | 
				
			||||||
 | 
					const emit = defineEmits(['cancel', 'success']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dialogVisible = defineModel<boolean>('visible', { default: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const formRef = useTemplateRef('formRef');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { isFetching: createLoading, execute: createExec } = dockerApi.containerCreate.useApi(submitForm);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// onMounted(async () => {
 | 
				
			||||||
 | 
					//     init();
 | 
				
			||||||
 | 
					// });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(dialogVisible, async (val) => {
 | 
				
			||||||
 | 
					    if (val) {
 | 
				
			||||||
 | 
					        init();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const runtimeSelect = computed(() => {
 | 
				
			||||||
 | 
					    return state.dockerInfo ? Object.keys(state.dockerInfo?.Runtimes) : [];
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const init = async () => {
 | 
				
			||||||
 | 
					    state.form = deepClone(defaultForm);
 | 
				
			||||||
 | 
					    state.submitForm = {};
 | 
				
			||||||
 | 
					    dockerApi.info.request({ id: props.id }).then((res) => {
 | 
				
			||||||
 | 
					        state.dockerInfo = res;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    state.images = await dockerApi.images.request({ id: props.id });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handlePortsAdd = () => {
 | 
				
			||||||
 | 
					    let item = {
 | 
				
			||||||
 | 
					        host: '',
 | 
				
			||||||
 | 
					        hostIP: '',
 | 
				
			||||||
 | 
					        containerPort: '',
 | 
				
			||||||
 | 
					        hostPort: '',
 | 
				
			||||||
 | 
					        protocol: 'tcp',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    state.form.exposedPorts.push(item);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handlePortsDelete = (index: number) => {
 | 
				
			||||||
 | 
					    state.form.exposedPorts.splice(index, 1);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleVolumesAdd = () => {
 | 
				
			||||||
 | 
					    let item = {
 | 
				
			||||||
 | 
					        hostDir: '',
 | 
				
			||||||
 | 
					        containerDir: '',
 | 
				
			||||||
 | 
					        mode: 'rw',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    state.form.volumes.push(item);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleVolumesDelete = (index: number) => {
 | 
				
			||||||
 | 
					    state.form.volumes.splice(index, 1);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleDevicesAdd = () => {
 | 
				
			||||||
 | 
					    let item = {
 | 
				
			||||||
 | 
					        count: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    state.form.devices.push(item);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleDevicesDelete = (index: number) => {
 | 
				
			||||||
 | 
					    state.form.devices.splice(index, 1);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const btnOk = async () => {
 | 
				
			||||||
 | 
					    await useI18nFormValidate(formRef);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    state.submitForm = { ...state.form };
 | 
				
			||||||
 | 
					    state.submitForm.id = props.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (state.submitForm.exposedPorts) {
 | 
				
			||||||
 | 
					        state.submitForm.exposedPorts = state.form.exposedPorts.map((item: any) => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                ...item,
 | 
				
			||||||
 | 
					                hostPort: item.hostPort + '', // 转为字符串
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (state.form.envsStr) {
 | 
				
			||||||
 | 
					        state.submitForm.envs = state.form.envsStr.split('\n');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (state.form.labelsStr) {
 | 
				
			||||||
 | 
					        state.submitForm.labels = state.form.labelsStr.split('\n');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (state.form.cmdStr) {
 | 
				
			||||||
 | 
					        let itemCmd = splitStringIgnoringQuotes(state.form.cmdStr);
 | 
				
			||||||
 | 
					        const cmds = [];
 | 
				
			||||||
 | 
					        for (const item of itemCmd) {
 | 
				
			||||||
 | 
					            cmds.push(item.replace(/(?<!\\)"/g, '').replaceAll('\\"', '"'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        state.submitForm.cmd = cmds;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await createExec();
 | 
				
			||||||
 | 
					    useI18nOperateSuccessMsg();
 | 
				
			||||||
 | 
					    emit('success', submitForm);
 | 
				
			||||||
 | 
					    cancel();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const cancel = () => {
 | 
				
			||||||
 | 
					    dialogVisible.value = false;
 | 
				
			||||||
 | 
					    emit('cancel');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const splitStringIgnoringQuotes = (input: string) => {
 | 
				
			||||||
 | 
					    input = input.replace(/\\"/g, '<quota>');
 | 
				
			||||||
 | 
					    const regex = /"([^"]*)"|(\S+)/g;
 | 
				
			||||||
 | 
					    const result = [];
 | 
				
			||||||
 | 
					    let match;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while ((match = regex.exec(input)) !== null) {
 | 
				
			||||||
 | 
					        if (match[1]) {
 | 
				
			||||||
 | 
					            result.push(match[1].replaceAll('<quota>', '\\"'));
 | 
				
			||||||
 | 
					        } else if (match[2]) {
 | 
				
			||||||
 | 
					            result.push(match[2].replaceAll('<quota>', '\\"'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										360
									
								
								frontend/src/views/ops/docker/container/ContainerList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,360 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="card !p-2">
 | 
				
			||||||
 | 
					        <el-row justify="space-between">
 | 
				
			||||||
 | 
					            <el-col :span="16">
 | 
				
			||||||
 | 
					                <el-row :gutter="5">
 | 
				
			||||||
 | 
					                    <el-col :span="6">
 | 
				
			||||||
 | 
					                        <el-input :placeholder="$t('docker.containerName')" v-model="params.name" plain clearable></el-input>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					                    <el-col :span="6">
 | 
				
			||||||
 | 
					                        <EnumSelect v-model="params.state" :enums="ContainerStateEnum" :placeholder="$t('docker.status')" clearable />
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					                    <el-col :span="8">
 | 
				
			||||||
 | 
					                        <el-button @click="getContainers" type="primary" icon="refresh" circle plain></el-button>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					                </el-row>
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-col :span="8">
 | 
				
			||||||
 | 
					                <el-row justify="end">
 | 
				
			||||||
 | 
					                    <el-button @click="openContainerCreate" type="success" icon="plus" plain>{{ $t('docker.createContainer') }}</el-button>
 | 
				
			||||||
 | 
					                </el-row>
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					        </el-row>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <el-table :data="filterTableDatas" v-loading="state.loadingContainers">
 | 
				
			||||||
 | 
					        <el-table-column prop="name" :label="$t('docker.name')" :min-width="120" show-overflow-tooltip> </el-table-column>
 | 
				
			||||||
 | 
					        <el-table-column prop="imageName" :label="$t('docker.image')" :min-width="150" show-overflow-tooltip> </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="state" :label="$t('common.status')" :min-width="110">
 | 
				
			||||||
 | 
					            <template #default="{ row }">
 | 
				
			||||||
 | 
					                <el-dropdown @command="handleCommand">
 | 
				
			||||||
 | 
					                    <el-button :type="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.tag.type" round plain size="small">
 | 
				
			||||||
 | 
					                        {{ $t(EnumValue.getLabelByValue(ContainerStateEnum, row.state)) || '-' }}
 | 
				
			||||||
 | 
					                        <SvgIcon class="ml-1" :name="EnumValue.getEnumByValue(ContainerStateEnum, row.state)?.extra.icon" />
 | 
				
			||||||
 | 
					                    </el-button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <template #dropdown>
 | 
				
			||||||
 | 
					                        <el-dropdown-menu>
 | 
				
			||||||
 | 
					                            <el-dropdown-item :command="{ type: 'restart', row }">
 | 
				
			||||||
 | 
					                                {{ $t('docker.restart') }}
 | 
				
			||||||
 | 
					                            </el-dropdown-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <el-dropdown-item :disabled="row.state == ContainerStateEnum.Stop.value" :command="{ type: 'stop', row }">
 | 
				
			||||||
 | 
					                                {{ $t('docker.stop') }}
 | 
				
			||||||
 | 
					                            </el-dropdown-item>
 | 
				
			||||||
 | 
					                        </el-dropdown-menu>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                </el-dropdown>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column v-loading="true" prop="stats" :label="$t('docker.stats')" :min-width="130">
 | 
				
			||||||
 | 
					            <template #default="{ row }">
 | 
				
			||||||
 | 
					                <SvgIcon v-if="getLoadingState(row.containerId)" class="is-loading" name="loading" color="var(--el-color-primary)" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <span v-else-if="row.stats">
 | 
				
			||||||
 | 
					                    <el-row>
 | 
				
			||||||
 | 
					                        <el-text size="small" class="font11">
 | 
				
			||||||
 | 
					                            CPU:
 | 
				
			||||||
 | 
					                            <span>{{ row.stats.cpuPercent.toFixed(2) }}%</span>
 | 
				
			||||||
 | 
					                        </el-text>
 | 
				
			||||||
 | 
					                    </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <el-row>
 | 
				
			||||||
 | 
					                        <el-text size="small" class="font11">
 | 
				
			||||||
 | 
					                            {{ $t('docker.memory') }}:
 | 
				
			||||||
 | 
					                            <span>{{ row.stats.memoryPercent.toFixed(2) }}%</span>
 | 
				
			||||||
 | 
					                        </el-text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <el-popover placement="right" :width="300" trigger="hover">
 | 
				
			||||||
 | 
					                            <template #reference>
 | 
				
			||||||
 | 
					                                <SvgIcon class="mt5 ml5" color="var(--el-color-primary)" name="MoreFilled" />
 | 
				
			||||||
 | 
					                            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <el-row>
 | 
				
			||||||
 | 
					                                <el-col :span="12">
 | 
				
			||||||
 | 
					                                    <el-statistic :title="$t('CPU使用')" :value="row.stats.cpuTotalUsage" :formatter="formatCpuValue" :precision="2">
 | 
				
			||||||
 | 
					                                    </el-statistic>
 | 
				
			||||||
 | 
					                                </el-col>
 | 
				
			||||||
 | 
					                                <el-col :span="12">
 | 
				
			||||||
 | 
					                                    <el-statistic :title="$t('CPU总计')" :value="row.stats.systemUsage" :formatter="formatCpuValue" :precision="2">
 | 
				
			||||||
 | 
					                                    </el-statistic>
 | 
				
			||||||
 | 
					                                </el-col>
 | 
				
			||||||
 | 
					                            </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <el-row>
 | 
				
			||||||
 | 
					                                <el-col :span="12">
 | 
				
			||||||
 | 
					                                    <el-statistic :title="$t('内存使用')" :value="row.stats.memoryUsage" :formatter="formatByteSize" :precision="2">
 | 
				
			||||||
 | 
					                                    </el-statistic>
 | 
				
			||||||
 | 
					                                </el-col>
 | 
				
			||||||
 | 
					                                <el-col :span="12">
 | 
				
			||||||
 | 
					                                    <el-statistic :title="$t('内存限额')" :value="row.stats.memoryLimit" :formatter="formatByteSize" :precision="2">
 | 
				
			||||||
 | 
					                                    </el-statistic>
 | 
				
			||||||
 | 
					                                </el-col>
 | 
				
			||||||
 | 
					                            </el-row>
 | 
				
			||||||
 | 
					                        </el-popover>
 | 
				
			||||||
 | 
					                    </el-row>
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <span v-else>-</span>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="networks" :label="$t('docker.ip')" :min-width="90">
 | 
				
			||||||
 | 
					            <template #default="scope">
 | 
				
			||||||
 | 
					                <el-tag v-for="network in scope.row.networks" :key="network" type="primary">{{ network || '-' }}</el-tag>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="ports" :label="$t('machine.port')" :min-width="160">
 | 
				
			||||||
 | 
					            <template #default="scope">
 | 
				
			||||||
 | 
					                <el-tag v-for="port in scope.row.ports" :key="port" type="primary">{{ port }}</el-tag>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="createTime" :label="$t('common.createTime')" width="160">
 | 
				
			||||||
 | 
					            <template #default="scope">
 | 
				
			||||||
 | 
					                {{ formatDate(scope.row.createTime) }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="status" label="运行时长" :min-width="120"> </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column :label="$t('common.operation')" :min-width="180">
 | 
				
			||||||
 | 
					            <template #default="{ row }">
 | 
				
			||||||
 | 
					                <el-row>
 | 
				
			||||||
 | 
					                    <el-button @click="openTerminal(row)" :disabled="row.state != ContainerStateEnum.Running.value" type="primary" link plain> SSH </el-button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <el-button @click="openLog(row)" type="success" link plain>{{ $t('docker.log') }}</el-button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <el-dropdown @command="handleCommand">
 | 
				
			||||||
 | 
					                        <el-button type="primary" link plain class="ml-3"> {{ $t('common.more') }} <SvgIcon name="arrow-down" :size="12" /> </el-button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <template #dropdown>
 | 
				
			||||||
 | 
					                            <el-dropdown-menu>
 | 
				
			||||||
 | 
					                                <el-dropdown-item :command="{ type: 'remove', row }">
 | 
				
			||||||
 | 
					                                    {{ $t('common.delete') }}
 | 
				
			||||||
 | 
					                                </el-dropdown-item>
 | 
				
			||||||
 | 
					                            </el-dropdown-menu>
 | 
				
			||||||
 | 
					                        </template>
 | 
				
			||||||
 | 
					                    </el-dropdown>
 | 
				
			||||||
 | 
					                </el-row>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					    </el-table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <el-dialog
 | 
				
			||||||
 | 
					        v-if="terminalDialog.visible"
 | 
				
			||||||
 | 
					        :title="terminalDialog.title"
 | 
				
			||||||
 | 
					        v-model="terminalDialog.visible"
 | 
				
			||||||
 | 
					        width="80%"
 | 
				
			||||||
 | 
					        body-class="h-[65vh]"
 | 
				
			||||||
 | 
					        :close-on-click-modal="false"
 | 
				
			||||||
 | 
					        :modal="false"
 | 
				
			||||||
 | 
					        @close="closeTerminal"
 | 
				
			||||||
 | 
					        draggable
 | 
				
			||||||
 | 
					        append-to-body
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					        <TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(props.id, terminalDialog.containerId)" />
 | 
				
			||||||
 | 
					    </el-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ContainerLog v-model:visible="logDialog.visible" :id="props.id" :container-id="logDialog.containerId" :title="logDialog.title" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <ContainerCreate v-model:visible="containerCreateDialog.visible" :id="props.id" @success="getContainers" />
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { computed, defineAsyncComponent, onMounted, reactive, toRefs, watch } from 'vue';
 | 
				
			||||||
 | 
					import { dockerApi, getDockerExecSocketUrl } from '../api';
 | 
				
			||||||
 | 
					import { formatByteSize, formatDate } from '@/common/utils/format';
 | 
				
			||||||
 | 
					import EnumSelect from '@/components/enumselect/EnumSelect.vue';
 | 
				
			||||||
 | 
					import { ContainerStateEnum } from '../enums';
 | 
				
			||||||
 | 
					import { fuzzyMatchField } from '@/common/utils/string';
 | 
				
			||||||
 | 
					import TerminalBody from '@/components/terminal/TerminalBody.vue';
 | 
				
			||||||
 | 
					import { useI18nConfirm, useI18nDeleteSuccessMsg, useI18nOperateSuccessMsg } from '@/hooks/useI18n';
 | 
				
			||||||
 | 
					import SvgIcon from '@/components/svgIcon/index.vue';
 | 
				
			||||||
 | 
					import { useDataState } from '@/hooks/useDataState';
 | 
				
			||||||
 | 
					import EnumValue from '@/common/Enum';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContainerLog = defineAsyncComponent(() => import('./ContainerLog.vue'));
 | 
				
			||||||
 | 
					const ContainerCreate = defineAsyncComponent(() => import('./ContainerCreate.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					    id: {
 | 
				
			||||||
 | 
					        type: Number,
 | 
				
			||||||
 | 
					        default: 0,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    params: {
 | 
				
			||||||
 | 
					        id: props.id,
 | 
				
			||||||
 | 
					        name: '',
 | 
				
			||||||
 | 
					        state: null,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    loadingContainers: false,
 | 
				
			||||||
 | 
					    containers: [],
 | 
				
			||||||
 | 
					    terminalDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        containerId: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    logDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        containerId: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    containerCreateDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        containerId: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { params, terminalDialog, logDialog, containerCreateDialog } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 容器状态加载状态,key -> containerId, value -> loading
 | 
				
			||||||
 | 
					const { setState: setLoadingState, getState: getLoadingState } = useDataState<string, boolean>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					    getContainers();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					    () => props.id,
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					        getContainers();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const filterTableDatas = computed(() => {
 | 
				
			||||||
 | 
					    let tables: any = state.containers;
 | 
				
			||||||
 | 
					    const nameSearch = state.params.name;
 | 
				
			||||||
 | 
					    const stateSearch = state.params.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (stateSearch) {
 | 
				
			||||||
 | 
					        tables = tables.filter((table: any) => {
 | 
				
			||||||
 | 
					            return table.state === stateSearch;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (nameSearch) {
 | 
				
			||||||
 | 
					        tables = fuzzyMatchField(nameSearch, tables, (table: any) => table.name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return tables;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getContainers = async () => {
 | 
				
			||||||
 | 
					    if (!props.id) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    state.params.id = props.id;
 | 
				
			||||||
 | 
					    state.loadingContainers = true;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        state.containers = await dockerApi.containers.request(state.params);
 | 
				
			||||||
 | 
					        setContainersStats();
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					        state.loadingContainers = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const setContainersStats = () => {
 | 
				
			||||||
 | 
					    if (state.containers.length === 0) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    state.containers.forEach((container: any) => {
 | 
				
			||||||
 | 
					        if (container.state === ContainerStateEnum.Running.value) {
 | 
				
			||||||
 | 
					            setLoadingState(container.containerId, true);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dockerApi.containersStats
 | 
				
			||||||
 | 
					        .request(state.params)
 | 
				
			||||||
 | 
					        .then((res) => {
 | 
				
			||||||
 | 
					            state.containers.forEach((container: any) => {
 | 
				
			||||||
 | 
					                const stats = res.find((stat: any) => stat.containerId === container.containerId);
 | 
				
			||||||
 | 
					                if (stats) {
 | 
				
			||||||
 | 
					                    container.stats = stats;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .finally(() => {
 | 
				
			||||||
 | 
					            state.containers.forEach((container: any) => {
 | 
				
			||||||
 | 
					                if (container.state === ContainerStateEnum.Running.value) {
 | 
				
			||||||
 | 
					                    setLoadingState(container.containerId, false);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const containerRestart = async (param: any) => {
 | 
				
			||||||
 | 
					    await dockerApi.containerRestart.request({ id: props.id, containerId: param.containerId });
 | 
				
			||||||
 | 
					    useI18nOperateSuccessMsg();
 | 
				
			||||||
 | 
					    getContainers();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const containerStop = async (param: any) => {
 | 
				
			||||||
 | 
					    await useI18nConfirm('docker.stopContainerConfirm', { name: param.name });
 | 
				
			||||||
 | 
					    await dockerApi.containerStop.request({ id: props.id, containerId: param.containerId });
 | 
				
			||||||
 | 
					    useI18nOperateSuccessMsg();
 | 
				
			||||||
 | 
					    getContainers();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const containerRemove = async (param: any) => {
 | 
				
			||||||
 | 
					    await useI18nConfirm('docker.removeContainerConfirm', { name: param.name });
 | 
				
			||||||
 | 
					    await dockerApi.containerRemove.request({ id: props.id, containerId: param.containerId });
 | 
				
			||||||
 | 
					    useI18nDeleteSuccessMsg();
 | 
				
			||||||
 | 
					    getContainers();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const openTerminal = (row: any) => {
 | 
				
			||||||
 | 
					    state.terminalDialog.containerId = row.containerId;
 | 
				
			||||||
 | 
					    state.terminalDialog.title = `Terminal - ${row.name}`;
 | 
				
			||||||
 | 
					    state.terminalDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const closeTerminal = () => {
 | 
				
			||||||
 | 
					    state.terminalDialog.visible = false;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const openLog = (row: any) => {
 | 
				
			||||||
 | 
					    state.logDialog.containerId = row.containerId;
 | 
				
			||||||
 | 
					    state.logDialog.title = `Log - ${row.name}`;
 | 
				
			||||||
 | 
					    state.logDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleCommand = async (commond: any) => {
 | 
				
			||||||
 | 
					    const row = commond.row;
 | 
				
			||||||
 | 
					    const type = commond.type;
 | 
				
			||||||
 | 
					    switch (type) {
 | 
				
			||||||
 | 
					        case 'restart': {
 | 
				
			||||||
 | 
					            containerRestart({ containerId: row.containerId, name: row.name });
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case 'stop': {
 | 
				
			||||||
 | 
					            containerStop({ containerId: row.containerId, name: row.name });
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        case 'remove': {
 | 
				
			||||||
 | 
					            containerRemove({ containerId: row.containerId, name: row.name });
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const openContainerCreate = () => {
 | 
				
			||||||
 | 
					    state.containerCreateDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatCpuValue(t: number) {
 | 
				
			||||||
 | 
					    const num = 1000;
 | 
				
			||||||
 | 
					    if (t < num) return t + ' ns';
 | 
				
			||||||
 | 
					    if (t < Math.pow(num, 2)) return Number((t / num).toFixed(2)) + ' μs';
 | 
				
			||||||
 | 
					    if (t < Math.pow(num, 3)) return Number((t / Math.pow(num, 2)).toFixed(2)) + ' ms';
 | 
				
			||||||
 | 
					    return Number((t / Math.pow(num, 3)).toFixed(2)) + ' s';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										83
									
								
								frontend/src/views/ops/docker/container/ContainerLog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					        <el-drawer title="logs" v-model="visible" @close="close" :destroy-on-close="true" :close-on-click-modal="true" size="60%">
 | 
				
			||||||
 | 
					            <template #header>
 | 
				
			||||||
 | 
					                <DrawerHeader :header="`${props.title}`" :back="() => (visible = false)">
 | 
				
			||||||
 | 
					                    <template #extra>
 | 
				
			||||||
 | 
					                        <div class="mr20"></div>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                </DrawerHeader>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div class="flex flex-col flex-1">
 | 
				
			||||||
 | 
					                <el-row :gutter="10" class="mb-2">
 | 
				
			||||||
 | 
					                    <el-col :span="6">
 | 
				
			||||||
 | 
					                        <el-select @change="searchLog" v-model.number="state.tail">
 | 
				
			||||||
 | 
					                            <template #prefix>{{ $t('docker.lines') }}</template>
 | 
				
			||||||
 | 
					                            <el-option :value="100" :label="100" />
 | 
				
			||||||
 | 
					                            <el-option :value="200" :label="200" />
 | 
				
			||||||
 | 
					                            <el-option :value="500" :label="500" />
 | 
				
			||||||
 | 
					                            <el-option :value="1000" :label="1000" />
 | 
				
			||||||
 | 
					                        </el-select>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <el-col :span="6">
 | 
				
			||||||
 | 
					                        <el-checkbox @change="searchLog" border v-model="state.isWatch">
 | 
				
			||||||
 | 
					                            {{ $t('docker.follow') }}
 | 
				
			||||||
 | 
					                        </el-checkbox>
 | 
				
			||||||
 | 
					                    </el-col>
 | 
				
			||||||
 | 
					                </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <RealLogViewer ref="realLogViewerRef" :ws-url="wsUrl" height="calc(100vh - 200px)" />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </el-drawer>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { computed, reactive, useTemplateRef } from 'vue';
 | 
				
			||||||
 | 
					import RealLogViewer from '@/components/monaco/RealLogViewer.vue';
 | 
				
			||||||
 | 
					import { getContainerLogSocketUrl } from '../api';
 | 
				
			||||||
 | 
					import DrawerHeader from '@/components/drawer-header/DrawerHeader.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					    id: {
 | 
				
			||||||
 | 
					        type: Number,
 | 
				
			||||||
 | 
					        default: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    title: {
 | 
				
			||||||
 | 
					        type: String,
 | 
				
			||||||
 | 
					        default: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    containerId: {
 | 
				
			||||||
 | 
					        type: String,
 | 
				
			||||||
 | 
					        default: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const visible = defineModel<boolean>('visible');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const realLogViewerRef = useTemplateRef('realLogViewerRef');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    since: '',
 | 
				
			||||||
 | 
					    tail: 100,
 | 
				
			||||||
 | 
					    isWatch: true,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const wsUrl = computed(
 | 
				
			||||||
 | 
					    () => `${getContainerLogSocketUrl(props.id, props.containerId)}&tail=${state.tail}&follow=${state.isWatch ? '1' : '0'}&since=${state.since}`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const searchLog = () => {
 | 
				
			||||||
 | 
					    realLogViewerRef.value?.reload(wsUrl.value);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const close = () => {
 | 
				
			||||||
 | 
					    state.tail = 100;
 | 
				
			||||||
 | 
					    state.since = '';
 | 
				
			||||||
 | 
					    state.isWatch = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped></style>
 | 
				
			||||||
							
								
								
									
										11
									
								
								frontend/src/views/ops/docker/enums.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { EnumValue } from '@/common/Enum';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ContainerStateEnum = {
 | 
				
			||||||
 | 
					    Running: EnumValue.of('running', 'docker.running').tagTypeSuccess().setExtra({ icon: 'VideoPlay' }),
 | 
				
			||||||
 | 
					    Stop: EnumValue.of('exited', 'docker.stopped').tagTypeDanger().setExtra({ icon: 'VideoPause' }),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ImageStateEnum = {
 | 
				
			||||||
 | 
					    Used: EnumValue.of(true, '已使用').tagTypeSuccess(),
 | 
				
			||||||
 | 
					    UnUsed: EnumValue.of(false, '未使用').tagTypeInfo(),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										213
									
								
								frontend/src/views/ops/docker/image/ImageList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,213 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="card !p-2">
 | 
				
			||||||
 | 
					        <el-row :gutter="5">
 | 
				
			||||||
 | 
					            <el-col :span="4">
 | 
				
			||||||
 | 
					                <el-input :placeholder="$t('docker.imageName')" v-model="params.name" plain clearable></el-input>
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-col :span="4">
 | 
				
			||||||
 | 
					                <EnumSelect v-model="params.state" :enums="ImageStateEnum" :placeholder="$t('docker.status')" clearable />
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-col :span="4">
 | 
				
			||||||
 | 
					                <div class="flex items-center gap-2">
 | 
				
			||||||
 | 
					                    <el-button @click="getImages" type="primary" icon="refresh" circle plain></el-button>
 | 
				
			||||||
 | 
					                    <el-upload :on-success="uploadSuccess" action="" :http-request="uploadImage" :headers="{ token }" :show-file-list="false" name="file">
 | 
				
			||||||
 | 
					                        <el-button type="primary" icon="upload" circle plain></el-button>
 | 
				
			||||||
 | 
					                    </el-upload>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					        </el-row>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <el-table :data="filterTableDatas" v-loading="state.loadingImages">
 | 
				
			||||||
 | 
					        <el-table-column prop="id" label="ID" :min-width="100" show-overflow-tooltip>
 | 
				
			||||||
 | 
					            <template #default="{ row }">
 | 
				
			||||||
 | 
					                <el-link type="primary" :underline="false">
 | 
				
			||||||
 | 
					                    {{ row.id.split(':')[1].substring(0, 12) }}
 | 
				
			||||||
 | 
					                </el-link>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="tags" :label="$t('docker.tag')" :min-width="250">
 | 
				
			||||||
 | 
					            <template #default="{ row }">
 | 
				
			||||||
 | 
					                <el-tag v-for="tag in row.tags" :key="tag" type="primary">{{ tag || '-' }}</el-tag>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="size" :label="$t('docker.size')" :min-width="60">
 | 
				
			||||||
 | 
					            <template #default="{ row }">
 | 
				
			||||||
 | 
					                {{ formatByteSize(row.size) }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="createTime" :label="$t('common.createTime')" width="160">
 | 
				
			||||||
 | 
					            <template #default="scope">
 | 
				
			||||||
 | 
					                {{ formatDate(scope.row.createTime) }}
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column prop="isUse" :label="$t('common.status')" :min-width="50">
 | 
				
			||||||
 | 
					            <template #default="{ row }">
 | 
				
			||||||
 | 
					                <EnumTag :enums="ImageStateEnum" :value="row.isUse" />
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-table-column :label="$t('common.operation')" width="130">
 | 
				
			||||||
 | 
					            <template #default="{ row }">
 | 
				
			||||||
 | 
					                <el-button @click="exportImage(row)" type="warning" link plain>{{ $t('docker.export') }}</el-button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-popconfirm :title="$t('docker.stopImageConfirm')" @confirm="imageRemove(row)" width="170">
 | 
				
			||||||
 | 
					                    <template #reference>
 | 
				
			||||||
 | 
					                        <el-button :disabled="row.isUse == ImageStateEnum.Used.value" type="danger" link plain>
 | 
				
			||||||
 | 
					                            {{ $t('common.delete') }}
 | 
				
			||||||
 | 
					                        </el-button>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                </el-popconfirm>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-table-column>
 | 
				
			||||||
 | 
					    </el-table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <el-dialog
 | 
				
			||||||
 | 
					        v-if="terminalDialog.visible"
 | 
				
			||||||
 | 
					        :title="terminalDialog.title"
 | 
				
			||||||
 | 
					        v-model="terminalDialog.visible"
 | 
				
			||||||
 | 
					        width="80%"
 | 
				
			||||||
 | 
					        :close-on-click-modal="false"
 | 
				
			||||||
 | 
					        :modal="false"
 | 
				
			||||||
 | 
					        @close="closeTerminal"
 | 
				
			||||||
 | 
					        draggable
 | 
				
			||||||
 | 
					        append-to-body
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					        <TerminalBody ref="terminal" :socket-url="getDockerExecSocketUrl(props.id, terminalDialog.containerId)" height="560px" />
 | 
				
			||||||
 | 
					    </el-dialog>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { computed, onMounted, reactive, toRefs } from 'vue';
 | 
				
			||||||
 | 
					import { dockerApi, getDockerExecSocketUrl } from '../api';
 | 
				
			||||||
 | 
					import { formatByteSize, formatDate } from '@/common/utils/format';
 | 
				
			||||||
 | 
					import EnumSelect from '@/components/enumselect/EnumSelect.vue';
 | 
				
			||||||
 | 
					import { ImageStateEnum } from '../enums';
 | 
				
			||||||
 | 
					import EnumTag from '@/components/enumtag/EnumTag.vue';
 | 
				
			||||||
 | 
					import { fuzzyMatchField } from '@/common/utils/string';
 | 
				
			||||||
 | 
					import TerminalBody from '@/components/terminal/TerminalBody.vue';
 | 
				
			||||||
 | 
					import config from '@/common/config';
 | 
				
			||||||
 | 
					import { joinClientParams } from '@/common/request';
 | 
				
			||||||
 | 
					import { getToken } from '@/common/utils/storage';
 | 
				
			||||||
 | 
					import { ElMessage } from 'element-plus';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					    id: {
 | 
				
			||||||
 | 
					        type: Number,
 | 
				
			||||||
 | 
					        default: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    params: {
 | 
				
			||||||
 | 
					        id: 0,
 | 
				
			||||||
 | 
					        name: '',
 | 
				
			||||||
 | 
					        state: null,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    loadingImages: false,
 | 
				
			||||||
 | 
					    images: [],
 | 
				
			||||||
 | 
					    terminalDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        containerId: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { params, terminalDialog } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const token = getToken();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					    getImages();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const filterTableDatas = computed(() => {
 | 
				
			||||||
 | 
					    let tables: any = state.images;
 | 
				
			||||||
 | 
					    const nameSearch = state.params.name;
 | 
				
			||||||
 | 
					    const stateSearch = state.params.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (stateSearch != null) {
 | 
				
			||||||
 | 
					        tables = tables.filter((table: any) => {
 | 
				
			||||||
 | 
					            return table.isUse === stateSearch;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (nameSearch) {
 | 
				
			||||||
 | 
					        tables = fuzzyMatchField(nameSearch, tables, (table: any) => table.tags[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return tables;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getImages = async () => {
 | 
				
			||||||
 | 
					    if (!props.id) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    state.params.id = props.id;
 | 
				
			||||||
 | 
					    state.loadingImages = true;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        state.images = await dockerApi.images.request(state.params);
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					        state.loadingImages = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const exportImage = async (row: any) => {
 | 
				
			||||||
 | 
					    const a = document.createElement('a');
 | 
				
			||||||
 | 
					    a.setAttribute('href', `${config.baseApiUrl}/docker/${props.id}/images/save?id=${props.id}&tag=${row.tags[0]}&${joinClientParams()}`);
 | 
				
			||||||
 | 
					    a.setAttribute('target', '_blank');
 | 
				
			||||||
 | 
					    a.click();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uploadImage = (content: any) => {
 | 
				
			||||||
 | 
					    const params = new FormData();
 | 
				
			||||||
 | 
					    // const path = state.nowPath;
 | 
				
			||||||
 | 
					    params.append('file', content.file);
 | 
				
			||||||
 | 
					    params.append('id', props.id + '');
 | 
				
			||||||
 | 
					    params.append('token', token);
 | 
				
			||||||
 | 
					    dockerApi.imageUpload
 | 
				
			||||||
 | 
					        .xhrReq(params, {
 | 
				
			||||||
 | 
					            headers: { 'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundaryF1uyUD0tWdqmJqpl' },
 | 
				
			||||||
 | 
					            // onUploadProgress: onUploadProgress,
 | 
				
			||||||
 | 
					            timeout: 3 * 60 * 60 * 1000,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					            ElMessage.success(i18n.global.t('machine.uploadSuccess'));
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                getImages();
 | 
				
			||||||
 | 
					            }, 3000);
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch(() => {
 | 
				
			||||||
 | 
					            // state.uploadProgressShow = false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    ElMessage.info(i18n.global.t('docker.imageUploading'));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uploadSuccess = (res: any) => {
 | 
				
			||||||
 | 
					    if (res.code !== 200) {
 | 
				
			||||||
 | 
					        ElMessage.error(res.msg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const imageRemove = async (row: any) => {
 | 
				
			||||||
 | 
					    await dockerApi.imageRemove.request({ id: props.id, imageId: row.id });
 | 
				
			||||||
 | 
					    getImages();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const openTerminal = (row: any) => {
 | 
				
			||||||
 | 
					    state.terminalDialog.containerId = row.containerId;
 | 
				
			||||||
 | 
					    state.terminalDialog.title = `Terminal - ${row.name}`;
 | 
				
			||||||
 | 
					    state.terminalDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const closeTerminal = () => {
 | 
				
			||||||
 | 
					    state.terminalDialog.visible = false;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										47
									
								
								frontend/src/views/ops/docker/resource/ContainerOp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="card h-full">
 | 
				
			||||||
 | 
					        <el-tabs v-model="activeName" @tab-change="handleTabChange">
 | 
				
			||||||
 | 
					            <el-tab-pane :label="$t('docker.container')" :name="containerTab">
 | 
				
			||||||
 | 
					                <ContainerList :id="containerConfId" />
 | 
				
			||||||
 | 
					            </el-tab-pane>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-tab-pane :label="$t('docker.image')" :name="imageTab">
 | 
				
			||||||
 | 
					                <ImageList v-if="activeName == imageTab" :id="containerConfId" />
 | 
				
			||||||
 | 
					            </el-tab-pane>
 | 
				
			||||||
 | 
					        </el-tabs>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { ContainerOpComp } from '@/views/ops/docker/resource';
 | 
				
			||||||
 | 
					import { toRefs, reactive, onMounted, defineAsyncComponent, ref, getCurrentInstance } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContainerList = defineAsyncComponent(() => import('../container/ContainerList.vue'));
 | 
				
			||||||
 | 
					const ImageList = defineAsyncComponent(() => import('../image/ImageList.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emits = defineEmits(['init']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const containerTab = 'containerTab';
 | 
				
			||||||
 | 
					const imageTab = 'imageTab';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const containerConfId = ref<number>(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    activeName: containerTab,
 | 
				
			||||||
 | 
					    cmdConfs: [],
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { activeName } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(async () => {
 | 
				
			||||||
 | 
					    emits('init', { name: ContainerOpComp.name, ref: getCurrentInstance()?.exposed });
 | 
				
			||||||
 | 
					    state.activeName = containerTab;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleTabChange = (tabName: any) => {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
					    init: function (id: number) {
 | 
				
			||||||
 | 
					        containerConfId.value = id;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										46
									
								
								frontend/src/views/ops/docker/resource/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					import { ResourceTypeEnum } from '@/common/commonEnum';
 | 
				
			||||||
 | 
					import { defineAsyncComponent } from 'vue';
 | 
				
			||||||
 | 
					import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '@/views/ops/component/tag';
 | 
				
			||||||
 | 
					import { dockerApi } from '@/views/ops/docker/api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ContainerConfList = defineAsyncComponent(() => import('../ContainerConfList.vue'));
 | 
				
			||||||
 | 
					const ContainerOp = defineAsyncComponent(() => import('./ContainerOp.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Icon = {
 | 
				
			||||||
 | 
					    name: ResourceTypeEnum.Container.extra.icon,
 | 
				
			||||||
 | 
					    color: ResourceTypeEnum.Container.extra.iconColor,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ContainerOpComp: ResourceComponentConfig = {
 | 
				
			||||||
 | 
					    name: 'tag.containerOp',
 | 
				
			||||||
 | 
					    component: ContainerOp,
 | 
				
			||||||
 | 
					    icon: Icon,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const NodeTypeContainerTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					    // 加载标签树下的容器列表
 | 
				
			||||||
 | 
					    const res = await dockerApi.page.request({ tagPath: node.params.tagPath });
 | 
				
			||||||
 | 
					    // 把list 根据name字段排序
 | 
				
			||||||
 | 
					    return res?.list
 | 
				
			||||||
 | 
					        .sort((a: any, b: any) => a.name.localeCompare(b.name))
 | 
				
			||||||
 | 
					        .map((x: any) => TagTreeNode.new(node, x.code, x.name, NodeTypeContainer).withIsLeaf(true).withParams(x).withIcon(Icon));
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeTypeContainer = new NodeType(11).withNodeClickFunc(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					    (await node.ctx?.addResourceComponent(ContainerOpComp)).init(node.params.id);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					    order: 1.5,
 | 
				
			||||||
 | 
					    resourceType: ResourceTypeEnum.Container.value,
 | 
				
			||||||
 | 
					    rootNodeType: NodeTypeContainerTag,
 | 
				
			||||||
 | 
					    manager: {
 | 
				
			||||||
 | 
					        componentConf: {
 | 
				
			||||||
 | 
					            component: ContainerConfList,
 | 
				
			||||||
 | 
					            icon: Icon,
 | 
				
			||||||
 | 
					            name: 'tag.container',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        permCode: 'container',
 | 
				
			||||||
 | 
					        countKey: 'container',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					} as ResourceConfig;
 | 
				
			||||||
							
								
								
									
										3
									
								
								frontend/src/views/ops/docker/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					export default {
 | 
				
			||||||
 | 
					    Container: () => import('@/views/ops/docker/DockerPanel.vue'),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -19,6 +19,13 @@
 | 
				
			|||||||
                <el-form-item prop="version" :label="t('common.version')">
 | 
					                <el-form-item prop="version" :label="t('common.version')">
 | 
				
			||||||
                    <el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
 | 
					                    <el-input v-model.trim="form.version" auto-complete="off" disabled></el-input>
 | 
				
			||||||
                </el-form-item>
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <!-- 增加协议下拉框 http和https,默认http-->
 | 
				
			||||||
 | 
					                 <el-form-item prop="protocol" :label="t('es.protocol')">
 | 
				
			||||||
 | 
					                    <el-select v-model="form.protocol" placeholder="http">
 | 
				
			||||||
 | 
					                        <el-option label="http" value="http"></el-option>
 | 
				
			||||||
 | 
					                        <el-option label="https" value="https"></el-option>
 | 
				
			||||||
 | 
					                    </el-select>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <el-form-item prop="host" label="Host" required>
 | 
					                <el-form-item prop="host" label="Host" required>
 | 
				
			||||||
                    <el-col :span="18">
 | 
					                    <el-col :span="18">
 | 
				
			||||||
@@ -105,6 +112,7 @@ const DefaultForm = {
 | 
				
			|||||||
    id: null,
 | 
					    id: null,
 | 
				
			||||||
    code: '',
 | 
					    code: '',
 | 
				
			||||||
    name: null,
 | 
					    name: null,
 | 
				
			||||||
 | 
					    protocol: 'http',
 | 
				
			||||||
    host: '',
 | 
					    host: '',
 | 
				
			||||||
    version: '',
 | 
					    version: '',
 | 
				
			||||||
    port: 9200,
 | 
					    port: 9200,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@
 | 
				
			|||||||
                {{ t('es.dashboard.nodes') }}
 | 
					                {{ t('es.dashboard.nodes') }}
 | 
				
			||||||
                <el-button v-if="state.tabName === 'nodesStats'" icon="refresh" @click="fetchNodesStats" link type="primary" />
 | 
					                <el-button v-if="state.tabName === 'nodesStats'" icon="refresh" @click="fetchNodesStats" link type="primary" />
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
            <el-descriptions class="nodes-num" column="3" border>
 | 
					            <el-descriptions class="nodes-num" :column="3" border>
 | 
				
			||||||
                <el-descriptions-item label="total">
 | 
					                <el-descriptions-item label="total">
 | 
				
			||||||
                    {{ state.nodesStats._nodes?.total }}
 | 
					                    {{ state.nodesStats._nodes?.total }}
 | 
				
			||||||
                </el-descriptions-item>
 | 
					                </el-descriptions-item>
 | 
				
			||||||
@@ -52,8 +52,8 @@
 | 
				
			|||||||
                                <el-progress
 | 
					                                <el-progress
 | 
				
			||||||
                                    striped
 | 
					                                    striped
 | 
				
			||||||
                                    striped-flow
 | 
					                                    striped-flow
 | 
				
			||||||
                                    duration="50"
 | 
					                                    :duration="50"
 | 
				
			||||||
                                    style="width: 100%"
 | 
					                                    class="w-full"
 | 
				
			||||||
                                    :percentage="node.os.mem.used_percent"
 | 
					                                    :percentage="node.os.mem.used_percent"
 | 
				
			||||||
                                    :color="getPercentColor(node.os.mem.used_percent)"
 | 
					                                    :color="getPercentColor(node.os.mem.used_percent)"
 | 
				
			||||||
                                />
 | 
					                                />
 | 
				
			||||||
@@ -64,8 +64,8 @@
 | 
				
			|||||||
                                <el-progress
 | 
					                                <el-progress
 | 
				
			||||||
                                    striped
 | 
					                                    striped
 | 
				
			||||||
                                    striped-flow
 | 
					                                    striped-flow
 | 
				
			||||||
                                    duration="50"
 | 
					                                    :duration="50"
 | 
				
			||||||
                                    style="width: 100%"
 | 
					                                    class="w-full"
 | 
				
			||||||
                                    :percentage="node.jvm.mem.heap_used_percent"
 | 
					                                    :percentage="node.jvm.mem.heap_used_percent"
 | 
				
			||||||
                                    :color="getPercentColor(node.jvm.mem.heap_used_percent)"
 | 
					                                    :color="getPercentColor(node.jvm.mem.heap_used_percent)"
 | 
				
			||||||
                                />
 | 
					                                />
 | 
				
			||||||
@@ -75,8 +75,8 @@
 | 
				
			|||||||
                                <el-progress
 | 
					                                <el-progress
 | 
				
			||||||
                                    striped
 | 
					                                    striped
 | 
				
			||||||
                                    striped-flow
 | 
					                                    striped-flow
 | 
				
			||||||
                                    duration="50"
 | 
					                                    :duration="50"
 | 
				
			||||||
                                    style="width: 100%"
 | 
					                                    class="w-full"
 | 
				
			||||||
                                    :percentage="node.os.cpu.percent"
 | 
					                                    :percentage="node.os.cpu.percent"
 | 
				
			||||||
                                    :color="getPercentColor(node.os.cpu.percent)"
 | 
					                                    :color="getPercentColor(node.os.cpu.percent)"
 | 
				
			||||||
                                />
 | 
					                                />
 | 
				
			||||||
@@ -88,8 +88,8 @@
 | 
				
			|||||||
                                <el-progress
 | 
					                                <el-progress
 | 
				
			||||||
                                    striped
 | 
					                                    striped
 | 
				
			||||||
                                    striped-flow
 | 
					                                    striped-flow
 | 
				
			||||||
                                    duration="50"
 | 
					                                    :duration="50"
 | 
				
			||||||
                                    style="width: 100%"
 | 
					                                    class="w-full"
 | 
				
			||||||
                                    :percentage="
 | 
					                                    :percentage="
 | 
				
			||||||
                                        Math.round(((node.fs.total.total_in_bytes - node.fs.total.free_in_bytes) * 100) / node.fs.total.total_in_bytes)
 | 
					                                        Math.round(((node.fs.total.total_in_bytes - node.fs.total.free_in_bytes) * 100) / node.fs.total.total_in_bytes)
 | 
				
			||||||
                                    "
 | 
					                                    "
 | 
				
			||||||
@@ -152,7 +152,7 @@
 | 
				
			|||||||
                        </el-select>
 | 
					                        </el-select>
 | 
				
			||||||
                    </el-form-item>
 | 
					                    </el-form-item>
 | 
				
			||||||
                    <el-form-item :label="t('es.dashboard.text')" required prop="text">
 | 
					                    <el-form-item :label="t('es.dashboard.text')" required prop="text">
 | 
				
			||||||
                        <el-input type="textarea" rows="5" v-model="state.analyze.text" />
 | 
					                        <el-input type="textarea" :rows="5" v-model="state.analyze.text" />
 | 
				
			||||||
                    </el-form-item>
 | 
					                    </el-form-item>
 | 
				
			||||||
                </el-form>
 | 
					                </el-form>
 | 
				
			||||||
                <el-button @click="onAnalyze" :loading="state.analyze.loading">{{ t('es.dashboard.startAnalyze') }}</el-button>
 | 
					                <el-button @click="onAnalyze" :loading="state.analyze.loading">{{ t('es.dashboard.startAnalyze') }}</el-button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -355,7 +355,7 @@ const defaultSearch = {
 | 
				
			|||||||
} as any;
 | 
					} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
    instId: string;
 | 
					    instId: number;
 | 
				
			||||||
    idxName: string;
 | 
					    idxName: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1034
									
								
								frontend/src/views/ops/es/resource/EsDataOp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										30
									
								
								frontend/src/views/ops/es/resource/NodeEs.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <BaseTreeNode v-bind="$attrs">
 | 
				
			||||||
 | 
					        <template #prefix="{ data }">
 | 
				
			||||||
 | 
					            <el-popover :show-after="500" placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
 | 
				
			||||||
 | 
					                <template #reference>
 | 
				
			||||||
 | 
					                    <SvgIcon name="icon es/es-color" />
 | 
				
			||||||
 | 
					                </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="version">
 | 
				
			||||||
 | 
					                            {{ data.params.version }}
 | 
				
			||||||
 | 
					                        </el-descriptions-item>
 | 
				
			||||||
 | 
					                        <el-descriptions-item label="uri">
 | 
				
			||||||
 | 
					                            {{ `${data.params.host}:${data.params.port}` }}
 | 
				
			||||||
 | 
					                        </el-descriptions-item>
 | 
				
			||||||
 | 
					                    </el-descriptions>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					            </el-popover>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					    </BaseTreeNode>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss"></style>
 | 
				
			||||||
							
								
								
									
										28
									
								
								frontend/src/views/ops/es/resource/NodeEsIndex.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <BaseTreeNode v-bind="$attrs">
 | 
				
			||||||
 | 
					        <template #prefix="{ data }">
 | 
				
			||||||
 | 
					            <el-popover placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
 | 
				
			||||||
 | 
					                <template #reference>
 | 
				
			||||||
 | 
					                    <SvgIcon name="Document" />
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					                <el-descriptions :column="1" size="small">
 | 
				
			||||||
 | 
					                    <el-descriptions-item :label="$t('common.name')">
 | 
				
			||||||
 | 
					                        {{ data.params.idxName }}
 | 
				
			||||||
 | 
					                    </el-descriptions-item>
 | 
				
			||||||
 | 
					                    <el-descriptions-item :label="$t('es.size')">
 | 
				
			||||||
 | 
					                        {{ data.params.size }}
 | 
				
			||||||
 | 
					                    </el-descriptions-item>
 | 
				
			||||||
 | 
					                    <el-descriptions-item :label="$t('es.docs')">
 | 
				
			||||||
 | 
					                        {{ data.params.idx['docs.count'] }}
 | 
				
			||||||
 | 
					                    </el-descriptions-item>
 | 
				
			||||||
 | 
					                </el-descriptions>
 | 
				
			||||||
 | 
					            </el-popover>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					    </BaseTreeNode>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss"></style>
 | 
				
			||||||
							
								
								
									
										175
									
								
								frontend/src/views/ops/es/resource/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					import { defineAsyncComponent } from 'vue';
 | 
				
			||||||
 | 
					import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
 | 
				
			||||||
 | 
					import { sleep } from '@/common/utils/loading';
 | 
				
			||||||
 | 
					import { ContextmenuItem } from '@/components/contextmenu';
 | 
				
			||||||
 | 
					import { esApi } from '@/views/ops/es/api';
 | 
				
			||||||
 | 
					import { i18n } from '@/i18n';
 | 
				
			||||||
 | 
					import { NodeType, TagTreeNode, ResourceComponentConfig } from '@/views/ops/component/tag';
 | 
				
			||||||
 | 
					import { ResourceConfig } from '../../component/tag';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Icon = {
 | 
				
			||||||
 | 
					    name: ResourceTypeEnum.Es.extra.icon,
 | 
				
			||||||
 | 
					    color: ResourceTypeEnum.Es.extra.iconColor,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const EsInstanceList = defineAsyncComponent(() => import('../EsInstanceList.vue'));
 | 
				
			||||||
 | 
					const EsDataOp = defineAsyncComponent(() => import('./EsDataOp.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeEs = defineAsyncComponent(() => import('./NodeEs.vue'));
 | 
				
			||||||
 | 
					const NodeEsIndex = defineAsyncComponent(() => import('./NodeEsIndex.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const EsOpComp: ResourceComponentConfig = {
 | 
				
			||||||
 | 
					    name: 'tag.esDataOp',
 | 
				
			||||||
 | 
					    component: EsDataOp,
 | 
				
			||||||
 | 
					    icon: Icon,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tagpath 节点类型
 | 
				
			||||||
 | 
					const NodeTypeEsTag = new NodeType(TagTreeNode.TagPath)
 | 
				
			||||||
 | 
					    .withContextMenuItems([
 | 
				
			||||||
 | 
					        new ContextmenuItem('refresh', 'common.refresh')
 | 
				
			||||||
 | 
					            .withIcon('refresh')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).reloadNode(nodeData.key)),
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
				
			||||||
 | 
					        parentNode.ctx?.addResourceComponent(EsOpComp);
 | 
				
			||||||
 | 
					        // 加载es实例列表
 | 
				
			||||||
 | 
					        const res = await esApi.instances.request({ tagPath: parentNode.params.tagPath });
 | 
				
			||||||
 | 
					        if (!res.total) {
 | 
				
			||||||
 | 
					            return [];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const insts = res.list;
 | 
				
			||||||
 | 
					        await sleep(100);
 | 
				
			||||||
 | 
					        return insts?.map((x: any) => {
 | 
				
			||||||
 | 
					            x.tagPath = parentNode.key;
 | 
				
			||||||
 | 
					            return TagTreeNode.new(parentNode, `es.inst.${x.code}`, x.name, NodeTypeInst).withNodeComponent(NodeEs).withParams(x);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 加载实例列表
 | 
				
			||||||
 | 
					const NodeTypeInst = new NodeType(1)
 | 
				
			||||||
 | 
					    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
				
			||||||
 | 
					        const params = parentNode.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let oiKey = `es.${params.id}.opIndex`;
 | 
				
			||||||
 | 
					        let bsKey = `es.${params.id}.opBasicSearch`;
 | 
				
			||||||
 | 
					        let ssKey = `es.${params.id}.opSeniorSearch`;
 | 
				
			||||||
 | 
					        let dbKey = `es.${params.id}.opDashboard`;
 | 
				
			||||||
 | 
					        let stKey = `es.${params.id}.opSettings`;
 | 
				
			||||||
 | 
					        let tpKey = `es.${params.id}.optemplates`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let nodeParams = { inst: params, instId: params.id };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            TagTreeNode.new(parentNode, oiKey, i18n.global.t('es.opIndex'), NodeTypeIndexs).withParams(nodeParams).withIcon({ name: 'Document' }),
 | 
				
			||||||
 | 
					            // new TagTreeNode(ssKey, t('es.opSeniorSearch'), NodeTypeSeniorSearch).withParams(nodeParams).withIsLeaf(true),
 | 
				
			||||||
 | 
					            // new TagTreeNode(dbKey, t('es.opDashboard'), NodeTypeDashboard).withParams(nodeParams).withIsLeaf(true),
 | 
				
			||||||
 | 
					            // new TagTreeNode(stKey, t('es.opSettings'), NodeTypeSettings).withParams(nodeParams),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .withNodeClickFunc(async (nodeData: TagTreeNode) => {
 | 
				
			||||||
 | 
					        // 添加一个dashboard tab
 | 
				
			||||||
 | 
					        (await nodeData.ctx?.addResourceComponent(EsOpComp)).onInstClick(nodeData);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeTypeIndexs = new NodeType(2)
 | 
				
			||||||
 | 
					    .withContextMenuItems([
 | 
				
			||||||
 | 
					        new ContextmenuItem('refresh', 'common.refresh')
 | 
				
			||||||
 | 
					            .withIcon('refresh')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) =>
 | 
				
			||||||
 | 
					                (await nodeData.ctx?.addResourceComponent(EsOpComp)).onRefreshIndices(nodeData.params.instId, nodeData.key)
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        new ContextmenuItem('addIndex', 'es.contextmenu.index.addIndex')
 | 
				
			||||||
 | 
					            .withIcon('plus')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onAddIndex(nodeData)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('showSys', 'es.contextmenu.index.showSys')
 | 
				
			||||||
 | 
					            .withIcon('View')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onShowSysIndex(nodeData)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('idxTemplate', 'es.templates')
 | 
				
			||||||
 | 
					            .withIcon('DocumentCopy')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onShowTemplate(nodeData)),
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					    .withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
				
			||||||
 | 
					        const params = parentNode.params;
 | 
				
			||||||
 | 
					        console.log(params);
 | 
				
			||||||
 | 
					        // 展示索引列表,显示索引名,文档总数
 | 
				
			||||||
 | 
					        // 加载索引列表
 | 
				
			||||||
 | 
					        let indicesRes = await (await parentNode.ctx?.addResourceComponent(EsOpComp)).loadIdxs(params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let idxNodes = [];
 | 
				
			||||||
 | 
					        for (let idx of indicesRes) {
 | 
				
			||||||
 | 
					            idxNodes.push(
 | 
				
			||||||
 | 
					                TagTreeNode.new(parentNode, idx.key, idx.idxName, NodeTypeIndex)
 | 
				
			||||||
 | 
					                    .withIsLeaf(true)
 | 
				
			||||||
 | 
					                    .withParams({
 | 
				
			||||||
 | 
					                        parentKey: parentNode.key,
 | 
				
			||||||
 | 
					                        ...idx,
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                    .withNodeComponent(NodeEsIndex)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return idxNodes;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 索引操作
 | 
				
			||||||
 | 
					const NodeTypeIndex = new NodeType(3)
 | 
				
			||||||
 | 
					    .withContextMenuItems([
 | 
				
			||||||
 | 
					        // 右键菜单支持:复制名字、新增别名、迁移索引、关闭、启用、删除、数据浏览、跳转基础查询、跳转高级查询
 | 
				
			||||||
 | 
					        new ContextmenuItem('copyName', 'es.contextmenu.index.copyName')
 | 
				
			||||||
 | 
					            .withIcon('copyDocument')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxCopyName(nodeData)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('refresh', 'es.contextmenu.index.refresh')
 | 
				
			||||||
 | 
					            .withIcon('refresh')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onRefreshIdx(nodeData)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('clearCache', 'es.contextmenu.index.clearCache')
 | 
				
			||||||
 | 
					            .withIcon('refresh')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onClearIdxCache(nodeData)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('flush', 'es.contextmenu.index.flush')
 | 
				
			||||||
 | 
					            .withIcon('refresh')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onFlushIdx(nodeData)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('Reindex', 'es.Reindex')
 | 
				
			||||||
 | 
					            .withIcon('Switch')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxReindex(nodeData)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('Close', 'es.contextmenu.index.Close')
 | 
				
			||||||
 | 
					            .withIcon('Close')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxClose(nodeData))
 | 
				
			||||||
 | 
					            .withHideFunc((data: any) => {
 | 
				
			||||||
 | 
					                return data.params.idx.status !== 'open';
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        new ContextmenuItem('Open', 'es.contextmenu.index.Open')
 | 
				
			||||||
 | 
					            .withIcon('Select')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxOpen(nodeData))
 | 
				
			||||||
 | 
					            .withHideFunc((data: any) => {
 | 
				
			||||||
 | 
					                return data.params.idx.status === 'open';
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        new ContextmenuItem('Delete', 'es.contextmenu.index.Delete')
 | 
				
			||||||
 | 
					            .withIcon('Delete')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxDelete(nodeData)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('BaseSearch', 'es.contextmenu.index.BaseSearch')
 | 
				
			||||||
 | 
					            .withIcon('Search')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIdxBaseSearch(nodeData)),
 | 
				
			||||||
 | 
					        // new ContextmenuItem('SeniorSearch', 'es.contextmenu.index.SeniorSearch').withIcon('Search').withOnClick((data: any) => onIdxSeniorSearch(data)),
 | 
				
			||||||
 | 
					        new ContextmenuItem('IndexDetail', 'es.indexDetail')
 | 
				
			||||||
 | 
					            .withIcon('InfoFilled')
 | 
				
			||||||
 | 
					            .withOnClick(async (nodeData: TagTreeNode) => (await nodeData.ctx?.addResourceComponent(EsOpComp)).onIndexDetail(nodeData)),
 | 
				
			||||||
 | 
					    ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .withNodeClickFunc(async (nodeData: TagTreeNode) => {
 | 
				
			||||||
 | 
					        const params = nodeData.params;
 | 
				
			||||||
 | 
					        (await nodeData.ctx?.addResourceComponent(EsOpComp)).loadIndexData(params.params.inst.id, params);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					    order: 5,
 | 
				
			||||||
 | 
					    resourceType: TagResourceTypeEnum.EsInstance.value,
 | 
				
			||||||
 | 
					    rootNodeType: NodeTypeEsTag,
 | 
				
			||||||
 | 
					    manager: {
 | 
				
			||||||
 | 
					        componentConf: {
 | 
				
			||||||
 | 
					            component: EsInstanceList,
 | 
				
			||||||
 | 
					            icon: Icon,
 | 
				
			||||||
 | 
					            name: 'tag.es',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        countKey: 'es',
 | 
				
			||||||
 | 
					        permCode: 'es:instance:save',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					} as ResourceConfig;
 | 
				
			||||||
@@ -1,4 +1 @@
 | 
				
			|||||||
export default {
 | 
					export default {};
 | 
				
			||||||
    EsInstanceList: () => import('@/views/ops/es/EsInstanceList.vue'),
 | 
					 | 
				
			||||||
    EsOperation: () => import('@/views/ops/es/EsOperation.vue'),
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
            <template #tableHeader>
 | 
					            <template #tableHeader>
 | 
				
			||||||
                <el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>{{ $t('common.create') }} </el-button>
 | 
					                <el-button v-auth="perms.addMachine" type="primary" icon="plus" @click="openFormDialog(false)" plain>{{ $t('common.create') }} </el-button>
 | 
				
			||||||
                <el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete">
 | 
					                <el-button v-auth="perms.delMachine" :disabled="selectionData.length < 1" @click="deleteMachine()" type="danger" icon="delete" plain>
 | 
				
			||||||
                    {{ $t('common.delete') }}
 | 
					                    {{ $t('common.delete') }}
 | 
				
			||||||
                </el-button>
 | 
					                </el-button>
 | 
				
			||||||
            </template>
 | 
					            </template>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,595 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
    <div class="h-full">
 | 
					 | 
				
			||||||
        <ResourceOpPanel @resize="onResizeTagTree">
 | 
					 | 
				
			||||||
            <template #left>
 | 
					 | 
				
			||||||
                <tag-tree
 | 
					 | 
				
			||||||
                    ref="tagTreeRef"
 | 
					 | 
				
			||||||
                    :resource-type="TagResourceTypePath.MachineAuthCert"
 | 
					 | 
				
			||||||
                    :tag-path-node-type="NodeTypeTagPath"
 | 
					 | 
				
			||||||
                    :default-expanded-keys="state.defaultExpendKey"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                    <template #prefix="{ data }">
 | 
					 | 
				
			||||||
                        <SvgIcon
 | 
					 | 
				
			||||||
                            v-if="data.icon && data.params.status == 1 && data.params.protocol == MachineProtocolEnum.Ssh.value"
 | 
					 | 
				
			||||||
                            :name="data.icon.name"
 | 
					 | 
				
			||||||
                            :color="data.icon.color"
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                        <SvgIcon
 | 
					 | 
				
			||||||
                            v-if="data.icon && data.params.status == -1 && data.params.protocol == MachineProtocolEnum.Ssh.value"
 | 
					 | 
				
			||||||
                            :name="data.icon.name"
 | 
					 | 
				
			||||||
                            color="var(--el-color-danger)"
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                        <SvgIcon v-if="data.icon && data.params.protocol != MachineProtocolEnum.Ssh.value" :name="data.icon.name" :color="data.icon.color" />
 | 
					 | 
				
			||||||
                    </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <template #suffix="{ data }">
 | 
					 | 
				
			||||||
                        <span v-if="data.type.value == MachineNodeType.AuthCert">{{
 | 
					 | 
				
			||||||
                            ` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}`
 | 
					 | 
				
			||||||
                        }}</span>
 | 
					 | 
				
			||||||
                    </template>
 | 
					 | 
				
			||||||
                </tag-tree>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <template #right>
 | 
					 | 
				
			||||||
                <el-card class="h-full" body-class="machine-terminal-tabs h-full !p-1 flex flex-col flex-1">
 | 
					 | 
				
			||||||
                    <el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" v-model="state.activeTermName" class="!h-full w-full">
 | 
					 | 
				
			||||||
                        <el-tab-pane class="!h-full flex flex-col" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
 | 
					 | 
				
			||||||
                            <template #label>
 | 
					 | 
				
			||||||
                                <el-popconfirm @confirm="handleReconnect(dt, true)" :title="$t('machine.reConnTips')">
 | 
					 | 
				
			||||||
                                    <template #reference>
 | 
					 | 
				
			||||||
                                        <el-icon
 | 
					 | 
				
			||||||
                                            class="mr-1"
 | 
					 | 
				
			||||||
                                            :color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
 | 
					 | 
				
			||||||
                                            :title="dt.status == TerminalStatusEnum.Connected.value ? '' : $t('machine.clickReConn')"
 | 
					 | 
				
			||||||
                                            ><Connection />
 | 
					 | 
				
			||||||
                                        </el-icon>
 | 
					 | 
				
			||||||
                                    </template>
 | 
					 | 
				
			||||||
                                </el-popconfirm>
 | 
					 | 
				
			||||||
                                <el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
 | 
					 | 
				
			||||||
                                    <template #reference>
 | 
					 | 
				
			||||||
                                        <div>
 | 
					 | 
				
			||||||
                                            <span class="machine-terminal-tab-label">{{ dt.label }}</span>
 | 
					 | 
				
			||||||
                                        </div>
 | 
					 | 
				
			||||||
                                    </template>
 | 
					 | 
				
			||||||
                                    <template #default>
 | 
					 | 
				
			||||||
                                        <el-descriptions :column="1" size="small">
 | 
					 | 
				
			||||||
                                            <el-descriptions-item :label="$t('common.name')"> {{ dt.params?.name }} </el-descriptions-item>
 | 
					 | 
				
			||||||
                                            <el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
 | 
					 | 
				
			||||||
                                            <el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
 | 
					 | 
				
			||||||
                                            <el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
 | 
					 | 
				
			||||||
                                        </el-descriptions>
 | 
					 | 
				
			||||||
                                    </template>
 | 
					 | 
				
			||||||
                                </el-popover>
 | 
					 | 
				
			||||||
                            </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            <div :ref="(el: any) => setTerminalWrapperRef(el, dt.key)" class="terminal-wrapper flex-1 h-[calc(100vh-155px)]">
 | 
					 | 
				
			||||||
                                <TerminalBody
 | 
					 | 
				
			||||||
                                    v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
 | 
					 | 
				
			||||||
                                    :mount-init="false"
 | 
					 | 
				
			||||||
                                    @status-change="terminalStatusChange(dt.key, $event)"
 | 
					 | 
				
			||||||
                                    :ref="(el: any) => setTerminalRef(el, dt.key)"
 | 
					 | 
				
			||||||
                                    :socket-url="dt.socketUrl"
 | 
					 | 
				
			||||||
                                />
 | 
					 | 
				
			||||||
                                <machine-rdp
 | 
					 | 
				
			||||||
                                    v-if="dt.params.protocol != MachineProtocolEnum.Ssh.value"
 | 
					 | 
				
			||||||
                                    :machine-id="dt.params.id"
 | 
					 | 
				
			||||||
                                    :auth-cert="dt.authCert"
 | 
					 | 
				
			||||||
                                    :ref="(el: any) => setTerminalRef(el, dt.key)"
 | 
					 | 
				
			||||||
                                    @status-change="terminalStatusChange(dt.key, $event)"
 | 
					 | 
				
			||||||
                                />
 | 
					 | 
				
			||||||
                            </div>
 | 
					 | 
				
			||||||
                        </el-tab-pane>
 | 
					 | 
				
			||||||
                    </el-tabs>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible">
 | 
					 | 
				
			||||||
                        <el-descriptions :title="$t('common.detail')" :column="3" border>
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="1.5" label="ID">{{ infoDialog.data.id }}</el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="1.5" :label="$t('common.name')">{{ infoDialog.data.name }}</el-descriptions-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="3" :label="$t('tag.relateTag')">
 | 
					 | 
				
			||||||
                                <ResourceTags :tags="infoDialog.data.tags" />
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="1" :label="$t('machine.port')">{{ infoDialog.data.port }}</el-descriptions-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="3" :label="$t('common.remark')">{{ infoDialog.data.remark }}</el-descriptions-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="1.5" :label="$t('machine.sshTunnel')"
 | 
					 | 
				
			||||||
                                >{{ infoDialog.data.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="1.5" :label="$t('machine.terminalPlayback')"
 | 
					 | 
				
			||||||
                                >{{ infoDialog.data.enableRecorder == 1 ? $t('common.yes') : $t('common.no') }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="2" :label="$t('common.createTime')">
 | 
					 | 
				
			||||||
                                {{ formatDate(infoDialog.data.createTime) }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="1" :label="$t('common.creator')">
 | 
					 | 
				
			||||||
                                {{ infoDialog.data.creator }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="2" :label="$t('common.updateTime')">
 | 
					 | 
				
			||||||
                                {{ formatDate(infoDialog.data.updateTime) }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item :span="1" :label="$t('common.modifier')">
 | 
					 | 
				
			||||||
                                {{ infoDialog.data.modifier }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                        </el-descriptions>
 | 
					 | 
				
			||||||
                    </el-dialog>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <script-manage
 | 
					 | 
				
			||||||
                        :title="serviceDialog.title"
 | 
					 | 
				
			||||||
                        v-model:visible="serviceDialog.visible"
 | 
					 | 
				
			||||||
                        v-model:machineId="serviceDialog.machineId"
 | 
					 | 
				
			||||||
                        :auth-cert-name="serviceDialog.authCertName"
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <file-conf-list
 | 
					 | 
				
			||||||
                        :title="fileDialog.title"
 | 
					 | 
				
			||||||
                        :auth-cert-name="fileDialog.authCertName"
 | 
					 | 
				
			||||||
                        v-model:visible="fileDialog.visible"
 | 
					 | 
				
			||||||
                        v-model:machineId="fileDialog.machineId"
 | 
					 | 
				
			||||||
                        :protocol="fileDialog.protocol"
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <el-dialog
 | 
					 | 
				
			||||||
                        destroy-on-close
 | 
					 | 
				
			||||||
                        :title="state.filesystemDialog.title"
 | 
					 | 
				
			||||||
                        v-model="state.filesystemDialog.visible"
 | 
					 | 
				
			||||||
                        :close-on-click-modal="false"
 | 
					 | 
				
			||||||
                        width="70%"
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                        <machine-file
 | 
					 | 
				
			||||||
                            :machine-id="state.filesystemDialog.machineId"
 | 
					 | 
				
			||||||
                            :auth-cert-name="state.filesystemDialog.authCertName"
 | 
					 | 
				
			||||||
                            :protocol="state.filesystemDialog.protocol"
 | 
					 | 
				
			||||||
                            :file-id="state.filesystemDialog.fileId"
 | 
					 | 
				
			||||||
                            :path="state.filesystemDialog.path"
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                    </el-dialog>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
 | 
					 | 
				
			||||||
                </el-card>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
        </ResourceOpPanel>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts" setup>
 | 
					 | 
				
			||||||
import { defineAsyncComponent, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
 | 
					 | 
				
			||||||
import { useRouter } from 'vue-router';
 | 
					 | 
				
			||||||
import { getMachineTerminalSocketUrl, machineApi } from './api';
 | 
					 | 
				
			||||||
import { formatDate } from '@/common/utils/format';
 | 
					 | 
				
			||||||
import { hasPerms } from '@/components/auth/auth';
 | 
					 | 
				
			||||||
import { TagResourceTypeEnum, TagResourceTypePath } from '@/common/commonEnum';
 | 
					 | 
				
			||||||
import { NodeType, TagTreeNode, getTagTypeCodeByPath } from '../component/tag';
 | 
					 | 
				
			||||||
import TagTree from '../component/TagTree.vue';
 | 
					 | 
				
			||||||
import { ContextmenuItem } from '@/components/contextmenu/index';
 | 
					 | 
				
			||||||
import TerminalBody from '@/components/terminal/TerminalBody.vue';
 | 
					 | 
				
			||||||
import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
 | 
					 | 
				
			||||||
import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
 | 
					 | 
				
			||||||
import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
 | 
					 | 
				
			||||||
import ResourceTags from '../component/ResourceTags.vue';
 | 
					 | 
				
			||||||
import { MachineProtocolEnum } from './enums';
 | 
					 | 
				
			||||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
 | 
					 | 
				
			||||||
import { storeToRefs } from 'pinia';
 | 
					 | 
				
			||||||
import EnumValue from '@/common/Enum';
 | 
					 | 
				
			||||||
import { useI18n } from 'vue-i18n';
 | 
					 | 
				
			||||||
import ResourceOpPanel from '../component/ResourceOpPanel.vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 组件
 | 
					 | 
				
			||||||
const ScriptManage = defineAsyncComponent(() => import('./ScriptManage.vue'));
 | 
					 | 
				
			||||||
const FileConfList = defineAsyncComponent(() => import('./file/FileConfList.vue'));
 | 
					 | 
				
			||||||
const MachineStats = defineAsyncComponent(() => import('./MachineStats.vue'));
 | 
					 | 
				
			||||||
const MachineRec = defineAsyncComponent(() => import('./MachineRec.vue'));
 | 
					 | 
				
			||||||
const ProcessList = defineAsyncComponent(() => import('./ProcessList.vue'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { t } = useI18n();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const router = useRouter();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const perms = {
 | 
					 | 
				
			||||||
    addMachine: 'machine:add',
 | 
					 | 
				
			||||||
    updateMachine: 'machine:update',
 | 
					 | 
				
			||||||
    delMachine: 'machine:del',
 | 
					 | 
				
			||||||
    terminal: 'machine:terminal',
 | 
					 | 
				
			||||||
    closeCli: 'machine:close-cli',
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
 | 
					 | 
				
			||||||
const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class MachineNodeType {
 | 
					 | 
				
			||||||
    static Machine = 1;
 | 
					 | 
				
			||||||
    static AuthCert = 2;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const state = reactive({
 | 
					 | 
				
			||||||
    defaultExpendKey: [] as any,
 | 
					 | 
				
			||||||
    params: {
 | 
					 | 
				
			||||||
        pageNum: 1,
 | 
					 | 
				
			||||||
        pageSize: 0,
 | 
					 | 
				
			||||||
        ip: null,
 | 
					 | 
				
			||||||
        name: null,
 | 
					 | 
				
			||||||
        tagPath: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    infoDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        data: null as any,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    serviceDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        machineId: 0,
 | 
					 | 
				
			||||||
        authCertName: '',
 | 
					 | 
				
			||||||
        title: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    processDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        machineId: 0,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    fileDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        machineId: 0,
 | 
					 | 
				
			||||||
        protocol: 1,
 | 
					 | 
				
			||||||
        title: '',
 | 
					 | 
				
			||||||
        authCertName: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    filesystemDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        machineId: 0,
 | 
					 | 
				
			||||||
        authCertName: '',
 | 
					 | 
				
			||||||
        protocol: 1,
 | 
					 | 
				
			||||||
        title: '',
 | 
					 | 
				
			||||||
        fileId: 0,
 | 
					 | 
				
			||||||
        path: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    machineStatsDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        stats: null,
 | 
					 | 
				
			||||||
        title: '',
 | 
					 | 
				
			||||||
        machineId: 0,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    machineRecDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        machineId: 0,
 | 
					 | 
				
			||||||
        title: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    activeTermName: '',
 | 
					 | 
				
			||||||
    tabs: new Map<string, any>(),
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const tagTreeRef: any = ref(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const autoOpenResourceStore = useAutoOpenResource();
 | 
					 | 
				
			||||||
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
let openIds: any = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
 | 
					 | 
				
			||||||
    // 加载标签树下的机器列表
 | 
					 | 
				
			||||||
    state.params.tagPath = node.key;
 | 
					 | 
				
			||||||
    state.params.pageNum = 1;
 | 
					 | 
				
			||||||
    state.params.pageSize = 1000;
 | 
					 | 
				
			||||||
    const res = await search();
 | 
					 | 
				
			||||||
    // 把list 根据name字段排序
 | 
					 | 
				
			||||||
    res.list = res.list.sort((a: any, b: any) => a.name.localeCompare(b.name));
 | 
					 | 
				
			||||||
    return res.list.map((x: any) =>
 | 
					 | 
				
			||||||
        new TagTreeNode(x.code, x.name, NodeTypeMachine)
 | 
					 | 
				
			||||||
            .withParams(x)
 | 
					 | 
				
			||||||
            .withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
 | 
					 | 
				
			||||||
            .withIcon({
 | 
					 | 
				
			||||||
                name: 'Monitor',
 | 
					 | 
				
			||||||
                color: '#409eff',
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const NodeTypeMachine = new NodeType(MachineNodeType.Machine)
 | 
					 | 
				
			||||||
    .withLoadNodesFunc((node: TagTreeNode) => {
 | 
					 | 
				
			||||||
        const machine = node.params;
 | 
					 | 
				
			||||||
        // 获取授权凭证列表
 | 
					 | 
				
			||||||
        const authCerts = machine.authCerts;
 | 
					 | 
				
			||||||
        return authCerts.map((x: any) =>
 | 
					 | 
				
			||||||
            new TagTreeNode(x.name, x.username, NodeTypeAuthCert)
 | 
					 | 
				
			||||||
                .withParams({ ...machine, selectAuthCert: x })
 | 
					 | 
				
			||||||
                .withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
 | 
					 | 
				
			||||||
                .withIcon({
 | 
					 | 
				
			||||||
                    name: 'Ticket',
 | 
					 | 
				
			||||||
                    color: '#409eff',
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                .withIsLeaf(true)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .withContextMenuItems([
 | 
					 | 
				
			||||||
        new ContextmenuItem('detail', 'common.detail').withIcon('More').withOnClick((node: any) => showInfo(node.params)),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new ContextmenuItem('status', 'common.status')
 | 
					 | 
				
			||||||
            .withIcon('Compass')
 | 
					 | 
				
			||||||
            .withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
 | 
					 | 
				
			||||||
            .withOnClick((node: any) => showMachineStats(node.params)),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new ContextmenuItem('process', 'machine.process')
 | 
					 | 
				
			||||||
            .withIcon('DataLine')
 | 
					 | 
				
			||||||
            .withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
 | 
					 | 
				
			||||||
            .withOnClick((node: any) => showProcess(node.params)),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new ContextmenuItem('edit', 'machine.terminalPlayback')
 | 
					 | 
				
			||||||
            .withIcon('Compass')
 | 
					 | 
				
			||||||
            .withOnClick((node: any) => showRec(node.params))
 | 
					 | 
				
			||||||
            .withHideFunc((node: any) => actionBtns[perms.updateMachine] && node.params.enableRecorder == 1),
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const NodeTypeAuthCert = new NodeType(MachineNodeType.AuthCert)
 | 
					 | 
				
			||||||
    .withNodeDblclickFunc((node: TagTreeNode) => {
 | 
					 | 
				
			||||||
        openTerminal(node.params);
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .withContextMenuItems([
 | 
					 | 
				
			||||||
        new ContextmenuItem('term', 'machine.openTerminal').withIcon('Monitor').withOnClick((node: any) => openTerminal(node.params)),
 | 
					 | 
				
			||||||
        new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal').withIcon('Monitor').withOnClick((node: any) => openTerminal(node.params, true)),
 | 
					 | 
				
			||||||
        new ContextmenuItem('files', 'machine.fileManage').withIcon('FolderOpened').withOnClick((node: any) => showFileManage(node.params)),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        new ContextmenuItem('scripts', 'machine.scriptManage')
 | 
					 | 
				
			||||||
            .withIcon('Files')
 | 
					 | 
				
			||||||
            .withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
 | 
					 | 
				
			||||||
            .withOnClick((node: any) => serviceManager(node.params)),
 | 
					 | 
				
			||||||
    ]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
    () => autoOpenResource.value.machineCodePath,
 | 
					 | 
				
			||||||
    (codePath: any) => {
 | 
					 | 
				
			||||||
        autoOpenTerminal(codePath);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
    () => state.activeTermName,
 | 
					 | 
				
			||||||
    (newValue, oldValue) => {
 | 
					 | 
				
			||||||
        fitTerminal();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
 | 
					 | 
				
			||||||
        terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const nowTab = state.tabs.get(state.activeTermName);
 | 
					 | 
				
			||||||
        tagTreeRef.value.setCurrentKey(nowTab?.authCert);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					 | 
				
			||||||
    autoOpenTerminal(autoOpenResource.value.machineCodePath);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const autoOpenTerminal = (codePath: string) => {
 | 
					 | 
				
			||||||
    if (!codePath) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const typeAndCodes = getTagTypeCodeByPath(codePath);
 | 
					 | 
				
			||||||
    const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const machineCode = typeAndCodes[TagResourceTypeEnum.Machine.value][0];
 | 
					 | 
				
			||||||
    state.defaultExpendKey = [tagPath, machineCode];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const authCertName = typeAndCodes[TagResourceTypeEnum.AuthCert.value][0];
 | 
					 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
        // 置空
 | 
					 | 
				
			||||||
        autoOpenResourceStore.setMachineCodePath('');
 | 
					 | 
				
			||||||
        tagTreeRef.value.setCurrentKey(authCertName);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const acNode = tagTreeRef.value.getNode(authCertName);
 | 
					 | 
				
			||||||
        openTerminal(acNode.data.params);
 | 
					 | 
				
			||||||
    }, 1000);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const openTerminal = (machine: any, ex?: boolean) => {
 | 
					 | 
				
			||||||
    // 授权凭证名
 | 
					 | 
				
			||||||
    const ac = machine.selectAuthCert.name;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 新窗口打开
 | 
					 | 
				
			||||||
    if (ex) {
 | 
					 | 
				
			||||||
        if (machine.protocol == MachineProtocolEnum.Ssh.value) {
 | 
					 | 
				
			||||||
            const { href } = router.resolve({
 | 
					 | 
				
			||||||
                path: `/machine/terminal`,
 | 
					 | 
				
			||||||
                query: {
 | 
					 | 
				
			||||||
                    ac,
 | 
					 | 
				
			||||||
                    name: machine.name,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            window.open(href, '_blank');
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (machine.protocol == MachineProtocolEnum.Rdp.value) {
 | 
					 | 
				
			||||||
            const { href } = router.resolve({
 | 
					 | 
				
			||||||
                path: `/machine/terminal-rdp`,
 | 
					 | 
				
			||||||
                query: {
 | 
					 | 
				
			||||||
                    machineId: machine.id,
 | 
					 | 
				
			||||||
                    ac: ac,
 | 
					 | 
				
			||||||
                    name: machine.name,
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            window.open(href, '_blank');
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let { name } = machine;
 | 
					 | 
				
			||||||
    const labelName = `${machine.selectAuthCert.username}@${name}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 同一个机器的终端打开多次,key后添加下划线和数字区分
 | 
					 | 
				
			||||||
    openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
 | 
					 | 
				
			||||||
    let sameIndex = openIds[ac];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let key = `${ac}_${sameIndex}`;
 | 
					 | 
				
			||||||
    // 只保留name的15个字,超出部分只保留前后10个字符,中间用省略号代替
 | 
					 | 
				
			||||||
    const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let tab = {
 | 
					 | 
				
			||||||
        key,
 | 
					 | 
				
			||||||
        label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
 | 
					 | 
				
			||||||
        params: machine,
 | 
					 | 
				
			||||||
        authCert: ac,
 | 
					 | 
				
			||||||
        socketUrl: getMachineTerminalSocketUrl(ac),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    state.tabs.set(key, tab);
 | 
					 | 
				
			||||||
    state.activeTermName = key;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    nextTick(() => {
 | 
					 | 
				
			||||||
        handleReconnect(tab);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const serviceManager = (row: any) => {
 | 
					 | 
				
			||||||
    const authCert = row.selectAuthCert;
 | 
					 | 
				
			||||||
    state.serviceDialog.machineId = row.id;
 | 
					 | 
				
			||||||
    state.serviceDialog.visible = true;
 | 
					 | 
				
			||||||
    state.serviceDialog.authCertName = authCert.name;
 | 
					 | 
				
			||||||
    state.serviceDialog.title = `${row.name} => ${authCert.username}@${row.ip}`;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 显示机器状态统计信息
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const showMachineStats = async (machine: any) => {
 | 
					 | 
				
			||||||
    state.machineStatsDialog.machineId = machine.id;
 | 
					 | 
				
			||||||
    state.machineStatsDialog.title = `${t('machine.machineState')}: ${machine.name} => ${machine.ip}`;
 | 
					 | 
				
			||||||
    state.machineStatsDialog.visible = true;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const search = async () => {
 | 
					 | 
				
			||||||
    const res = await machineApi.list.request(state.params);
 | 
					 | 
				
			||||||
    return res;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showFileManage = (selectionData: any) => {
 | 
					 | 
				
			||||||
    const authCert = selectionData.selectAuthCert;
 | 
					 | 
				
			||||||
    if (selectionData.protocol == 1) {
 | 
					 | 
				
			||||||
        state.fileDialog.visible = true;
 | 
					 | 
				
			||||||
        state.fileDialog.protocol = selectionData.protocol;
 | 
					 | 
				
			||||||
        state.fileDialog.machineId = selectionData.id;
 | 
					 | 
				
			||||||
        state.fileDialog.authCertName = authCert.name;
 | 
					 | 
				
			||||||
        state.fileDialog.title = `${selectionData.name} => ${authCert.username}@${selectionData.ip}`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (selectionData.protocol == 2) {
 | 
					 | 
				
			||||||
        state.filesystemDialog.protocol = 2;
 | 
					 | 
				
			||||||
        state.filesystemDialog.machineId = selectionData.id;
 | 
					 | 
				
			||||||
        state.filesystemDialog.authCertName = authCert.name;
 | 
					 | 
				
			||||||
        state.filesystemDialog.fileId = selectionData.id;
 | 
					 | 
				
			||||||
        state.filesystemDialog.path = '/';
 | 
					 | 
				
			||||||
        state.filesystemDialog.title = t('machine.remoteFileDesktopManage');
 | 
					 | 
				
			||||||
        state.filesystemDialog.visible = true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showInfo = (info: any) => {
 | 
					 | 
				
			||||||
    state.infoDialog.data = info;
 | 
					 | 
				
			||||||
    state.infoDialog.visible = true;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showProcess = (row: any) => {
 | 
					 | 
				
			||||||
    state.processDialog.machineId = row.id;
 | 
					 | 
				
			||||||
    state.processDialog.visible = true;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showRec = (row: any) => {
 | 
					 | 
				
			||||||
    state.machineRecDialog.title = `${row.name}[${row.ip}]-${t('machine.terminalPlayback')}`;
 | 
					 | 
				
			||||||
    state.machineRecDialog.machineId = row.id;
 | 
					 | 
				
			||||||
    state.machineRecDialog.visible = true;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const onRemoveTab = (targetName: string) => {
 | 
					 | 
				
			||||||
    let activeTermName = state.activeTermName;
 | 
					 | 
				
			||||||
    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);
 | 
					 | 
				
			||||||
        let info = state.tabs.get(targetName);
 | 
					 | 
				
			||||||
        if (info) {
 | 
					 | 
				
			||||||
            terminalRefs[info.key]?.close();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (activeTermName != targetName) {
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
 | 
					 | 
				
			||||||
        const nextTab = tabNames[i + 1] || tabNames[i - 1];
 | 
					 | 
				
			||||||
        if (nextTab) {
 | 
					 | 
				
			||||||
            activeTermName = nextTab;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            activeTermName = '';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        state.activeTermName = activeTermName;
 | 
					 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const terminalStatusChange = (key: string, status: TerminalStatus) => {
 | 
					 | 
				
			||||||
    state.tabs.get(key).status = status;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const terminalRefs: any = {};
 | 
					 | 
				
			||||||
const setTerminalRef = (el: any, key: any) => {
 | 
					 | 
				
			||||||
    if (key) {
 | 
					 | 
				
			||||||
        terminalRefs[key] = el;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const terminalWrapperRefs: any = {};
 | 
					 | 
				
			||||||
const setTerminalWrapperRef = (el: any, key: any) => {
 | 
					 | 
				
			||||||
    if (key) {
 | 
					 | 
				
			||||||
        terminalWrapperRefs[key] = el;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const onResizeTagTree = () => {
 | 
					 | 
				
			||||||
    fitTerminal();
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const fitTerminal = () => {
 | 
					 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
        let info = state.tabs.get(state.activeTermName);
 | 
					 | 
				
			||||||
        if (info) {
 | 
					 | 
				
			||||||
            terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const handleReconnect = (tab: any, force = false) => {
 | 
					 | 
				
			||||||
    let width = terminalWrapperRefs[tab.key].offsetWidth;
 | 
					 | 
				
			||||||
    let height = terminalWrapperRefs[tab.key].offsetHeight;
 | 
					 | 
				
			||||||
    terminalRefs[tab.key]?.init(width, height, force);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="scss">
 | 
					 | 
				
			||||||
.machine-terminal-tabs {
 | 
					 | 
				
			||||||
    --el-tabs-header-height: 30px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .el-tabs {
 | 
					 | 
				
			||||||
        --el-tabs-header-height: 30px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .machine-terminal-tab-label {
 | 
					 | 
				
			||||||
        font-size: 12px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .el-tabs__header {
 | 
					 | 
				
			||||||
        margin-bottom: 5px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .el-tabs__item {
 | 
					 | 
				
			||||||
        padding: 0 8px !important;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
@@ -1,76 +1,74 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
    <div>
 | 
					    <el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="true" :destroy-on-close="true" :before-close="cancel" width="1050px">
 | 
				
			||||||
        <el-dialog :title="title" v-model="dialogVisible" :close-on-click-modal="true" :destroy-on-close="true" :before-close="cancel" width="1050px">
 | 
					        <el-row :gutter="20">
 | 
				
			||||||
            <el-row :gutter="20">
 | 
					            <el-col :lg="12" :md="12">
 | 
				
			||||||
                <el-col :lg="12" :md="12">
 | 
					                <el-descriptions size="small" :title="$t('machine.basicInfo')" :column="2" border>
 | 
				
			||||||
                    <el-descriptions size="small" :title="$t('machine.basicInfo')" :column="2" border>
 | 
					                    <template #extra>
 | 
				
			||||||
                        <template #extra>
 | 
					                        <el-link @click="onRefresh" icon="refresh" underline="never" type="success"></el-link>
 | 
				
			||||||
                            <el-link @click="onRefresh" icon="refresh" underline="never" type="success"></el-link>
 | 
					                    </template>
 | 
				
			||||||
 | 
					                    <el-descriptions-item :label="$t('machine.hostname')">
 | 
				
			||||||
 | 
					                        {{ stats.hostname }}
 | 
				
			||||||
 | 
					                    </el-descriptions-item>
 | 
				
			||||||
 | 
					                    <el-descriptions-item :label="$t('machine.runTime')">
 | 
				
			||||||
 | 
					                        {{ stats.uptime }}
 | 
				
			||||||
 | 
					                    </el-descriptions-item>
 | 
				
			||||||
 | 
					                    <el-descriptions-item :label="$t('machine.totalTask')">
 | 
				
			||||||
 | 
					                        {{ stats.totalProcs }}
 | 
				
			||||||
 | 
					                    </el-descriptions-item>
 | 
				
			||||||
 | 
					                    <el-descriptions-item :label="$t('machine.runningTask')">
 | 
				
			||||||
 | 
					                        {{ stats.runningProcs }}
 | 
				
			||||||
 | 
					                    </el-descriptions-item>
 | 
				
			||||||
 | 
					                    <el-descriptions-item :label="$t('machine.load')"> {{ stats.load1 }} {{ stats.load5 }} {{ stats.load10 }} </el-descriptions-item>
 | 
				
			||||||
 | 
					                </el-descriptions>
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-col :lg="6" :md="6">
 | 
				
			||||||
 | 
					                <ECharts height="200" :option="state.memOption" />
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <el-col :lg="6" :md="6">
 | 
				
			||||||
 | 
					                <ECharts height="200" :option="state.cpuOption" />
 | 
				
			||||||
 | 
					            </el-col>
 | 
				
			||||||
 | 
					        </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-row :gutter="20">
 | 
				
			||||||
 | 
					            <el-col :lg="8" :md="8">
 | 
				
			||||||
 | 
					                <span style="font-size: 16px; font-weight: 700">{{ $t('machine.disk') }}</span>
 | 
				
			||||||
 | 
					                <el-table :data="stats.fSInfos" stripe max-height="250" style="width: 100%" border>
 | 
				
			||||||
 | 
					                    <el-table-column prop="mountPoint" :label="$t('machine.mountPoint')" min-width="100" show-overflow-tooltip> </el-table-column>
 | 
				
			||||||
 | 
					                    <el-table-column :label="$t('machine.available')" min-width="70" show-overflow-tooltip>
 | 
				
			||||||
 | 
					                        <template #default="scope">
 | 
				
			||||||
 | 
					                            {{ formatByteSize(scope.row.free) }}
 | 
				
			||||||
                        </template>
 | 
					                        </template>
 | 
				
			||||||
                        <el-descriptions-item :label="$t('machine.hostname')">
 | 
					                    </el-table-column>
 | 
				
			||||||
                            {{ stats.hostname }}
 | 
					                    <el-table-column prop="Used" :label="$t('machine.used')" min-width="70" show-overflow-tooltip>
 | 
				
			||||||
                        </el-descriptions-item>
 | 
					                        <template #default="scope">
 | 
				
			||||||
                        <el-descriptions-item :label="$t('machine.runTime')">
 | 
					                            {{ formatByteSize(scope.row.used) }}
 | 
				
			||||||
                            {{ stats.uptime }}
 | 
					                        </template>
 | 
				
			||||||
                        </el-descriptions-item>
 | 
					                    </el-table-column>
 | 
				
			||||||
                        <el-descriptions-item :label="$t('machine.totalTask')">
 | 
					                </el-table>
 | 
				
			||||||
                            {{ stats.totalProcs }}
 | 
					            </el-col>
 | 
				
			||||||
                        </el-descriptions-item>
 | 
					 | 
				
			||||||
                        <el-descriptions-item :label="$t('machine.runningTask')">
 | 
					 | 
				
			||||||
                            {{ stats.runningProcs }}
 | 
					 | 
				
			||||||
                        </el-descriptions-item>
 | 
					 | 
				
			||||||
                        <el-descriptions-item :label="$t('machine.load')"> {{ stats.load1 }} {{ stats.load5 }} {{ stats.load10 }} </el-descriptions-item>
 | 
					 | 
				
			||||||
                    </el-descriptions>
 | 
					 | 
				
			||||||
                </el-col>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <el-col :lg="6" :md="6">
 | 
					            <el-col :lg="16" :md="16">
 | 
				
			||||||
                    <ECharts height="200" :option="state.memOption" />
 | 
					                <span style="font-size: 16px; font-weight: 700">{{ $t('machine.networkCard') }}</span>
 | 
				
			||||||
                </el-col>
 | 
					                <el-table :data="netInter" stripe max-height="250" style="width: 100%" border>
 | 
				
			||||||
 | 
					                    <el-table-column prop="name" :label="$t('machine.networkCard')" min-width="120" show-overflow-tooltip></el-table-column>
 | 
				
			||||||
                <el-col :lg="6" :md="6">
 | 
					                    <el-table-column prop="ipv4" label="IPv4" min-width="130" show-overflow-tooltip> </el-table-column>
 | 
				
			||||||
                    <ECharts height="200" :option="state.cpuOption" />
 | 
					                    <el-table-column prop="ipv6" label="IPv6" min-width="130" show-overflow-tooltip> </el-table-column>
 | 
				
			||||||
                </el-col>
 | 
					                    <el-table-column prop="rx" :label="`${$t('machine.receive')}(rx)`" min-width="110" show-overflow-tooltip>
 | 
				
			||||||
            </el-row>
 | 
					                        <template #default="scope">
 | 
				
			||||||
 | 
					                            {{ formatByteSize(scope.row.rx) }}
 | 
				
			||||||
            <el-row :gutter="20">
 | 
					                        </template>
 | 
				
			||||||
                <el-col :lg="8" :md="8">
 | 
					                    </el-table-column>
 | 
				
			||||||
                    <span style="font-size: 16px; font-weight: 700">{{ $t('machine.disk') }}</span>
 | 
					                    <el-table-column prop="tx" :label="`${$t('machine.send')}(tx)`" min-width="110" show-overflow-tooltip>
 | 
				
			||||||
                    <el-table :data="stats.fSInfos" stripe max-height="250" style="width: 100%" border>
 | 
					                        <template #default="scope">
 | 
				
			||||||
                        <el-table-column prop="mountPoint" :label="$t('machine.mountPoint')" min-width="100" show-overflow-tooltip> </el-table-column>
 | 
					                            {{ formatByteSize(scope.row.tx) }}
 | 
				
			||||||
                        <el-table-column :label="$t('machine.available')" min-width="70" show-overflow-tooltip>
 | 
					                        </template>
 | 
				
			||||||
                            <template #default="scope">
 | 
					                    </el-table-column>
 | 
				
			||||||
                                {{ formatByteSize(scope.row.free) }}
 | 
					                </el-table>
 | 
				
			||||||
                            </template>
 | 
					            </el-col>
 | 
				
			||||||
                        </el-table-column>
 | 
					        </el-row>
 | 
				
			||||||
                        <el-table-column prop="Used" :label="$t('machine.used')" min-width="70" show-overflow-tooltip>
 | 
					    </el-dialog>
 | 
				
			||||||
                            <template #default="scope">
 | 
					 | 
				
			||||||
                                {{ formatByteSize(scope.row.used) }}
 | 
					 | 
				
			||||||
                            </template>
 | 
					 | 
				
			||||||
                        </el-table-column>
 | 
					 | 
				
			||||||
                    </el-table>
 | 
					 | 
				
			||||||
                </el-col>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <el-col :lg="16" :md="16">
 | 
					 | 
				
			||||||
                    <span style="font-size: 16px; font-weight: 700">{{ $t('machine.networkCard') }}</span>
 | 
					 | 
				
			||||||
                    <el-table :data="netInter" stripe max-height="250" style="width: 100%" border>
 | 
					 | 
				
			||||||
                        <el-table-column prop="name" :label="$t('machine.networkCard')" min-width="120" show-overflow-tooltip></el-table-column>
 | 
					 | 
				
			||||||
                        <el-table-column prop="ipv4" label="IPv4" min-width="130" show-overflow-tooltip> </el-table-column>
 | 
					 | 
				
			||||||
                        <el-table-column prop="ipv6" label="IPv6" min-width="130" show-overflow-tooltip> </el-table-column>
 | 
					 | 
				
			||||||
                        <el-table-column prop="rx" :label="`${$t('machine.receive')}(rx)`" min-width="110" show-overflow-tooltip>
 | 
					 | 
				
			||||||
                            <template #default="scope">
 | 
					 | 
				
			||||||
                                {{ formatByteSize(scope.row.rx) }}
 | 
					 | 
				
			||||||
                            </template>
 | 
					 | 
				
			||||||
                        </el-table-column>
 | 
					 | 
				
			||||||
                        <el-table-column prop="tx" :label="`${$t('machine.send')}(tx)`" min-width="110" show-overflow-tooltip>
 | 
					 | 
				
			||||||
                            <template #default="scope">
 | 
					 | 
				
			||||||
                                {{ formatByteSize(scope.row.tx) }}
 | 
					 | 
				
			||||||
                            </template>
 | 
					 | 
				
			||||||
                        </el-table-column>
 | 
					 | 
				
			||||||
                    </el-table>
 | 
					 | 
				
			||||||
                </el-col>
 | 
					 | 
				
			||||||
            </el-row>
 | 
					 | 
				
			||||||
        </el-dialog>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@
 | 
				
			|||||||
            :destroy-on-close="true"
 | 
					            :destroy-on-close="true"
 | 
				
			||||||
            :show-close="true"
 | 
					            :show-close="true"
 | 
				
			||||||
            :before-close="handleClose"
 | 
					            :before-close="handleClose"
 | 
				
			||||||
            width="55%"
 | 
					            width="60%"
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
            <page-table
 | 
					            <page-table
 | 
				
			||||||
                ref="pageTableRef"
 | 
					                ref="pageTableRef"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@
 | 
				
			|||||||
            <el-row class="mb-2 ml-4">
 | 
					            <el-row class="mb-2 ml-4">
 | 
				
			||||||
                <el-breadcrumb separator-icon="ArrowRight">
 | 
					                <el-breadcrumb separator-icon="ArrowRight">
 | 
				
			||||||
                    <el-breadcrumb-item v-for="path in filePathNav" :key="path">
 | 
					                    <el-breadcrumb-item v-for="path in filePathNav" :key="path">
 | 
				
			||||||
                        <el-link @click="setFiles(path.path)" style="font-weight: bold">{{ path.name }}</el-link>
 | 
					                        <el-link @click="setFiles(path.path)" class="!cursor-pointer !font-bold">{{ path.name }}</el-link>
 | 
				
			||||||
                    </el-breadcrumb-item>
 | 
					                    </el-breadcrumb-item>
 | 
				
			||||||
                </el-breadcrumb>
 | 
					                </el-breadcrumb>
 | 
				
			||||||
            </el-row>
 | 
					            </el-row>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										440
									
								
								frontend/src/views/ops/machine/resource/MachineOp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,440 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="h-full machine-terminal-tabs">
 | 
				
			||||||
 | 
					        <el-tabs v-if="state.tabs.size > 0" type="card" @tab-remove="onRemoveTab" v-model="state.activeTermName" class="!h-full w-full">
 | 
				
			||||||
 | 
					            <el-tab-pane class="!h-full flex flex-col" closable v-for="dt in state.tabs.values()" :label="dt.label" :name="dt.key" :key="dt.key">
 | 
				
			||||||
 | 
					                <template #label>
 | 
				
			||||||
 | 
					                    <el-popconfirm @confirm="handleReconnect(dt, true)" :title="$t('machine.reConnTips')">
 | 
				
			||||||
 | 
					                        <template #reference>
 | 
				
			||||||
 | 
					                            <el-icon
 | 
				
			||||||
 | 
					                                class="mr-1"
 | 
				
			||||||
 | 
					                                :color="EnumValue.getEnumByValue(TerminalStatusEnum, dt.status)?.extra?.iconColor"
 | 
				
			||||||
 | 
					                                :title="dt.status == TerminalStatusEnum.Connected.value ? '' : $t('machine.clickReConn')"
 | 
				
			||||||
 | 
					                                ><Connection />
 | 
				
			||||||
 | 
					                            </el-icon>
 | 
				
			||||||
 | 
					                        </template>
 | 
				
			||||||
 | 
					                    </el-popconfirm>
 | 
				
			||||||
 | 
					                    <el-popover :show-after="1000" placement="bottom-start" trigger="hover" :width="250">
 | 
				
			||||||
 | 
					                        <template #reference>
 | 
				
			||||||
 | 
					                            <div>
 | 
				
			||||||
 | 
					                                <span class="machine-terminal-tab-label">{{ dt.label }}</span>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </template>
 | 
				
			||||||
 | 
					                        <template #default>
 | 
				
			||||||
 | 
					                            <el-descriptions :column="1" size="small">
 | 
				
			||||||
 | 
					                                <el-descriptions-item :label="$t('common.name')"> {{ dt.params?.name }} </el-descriptions-item>
 | 
				
			||||||
 | 
					                                <el-descriptions-item label="host"> {{ dt.params?.ip }} : {{ dt.params?.port }} </el-descriptions-item>
 | 
				
			||||||
 | 
					                                <el-descriptions-item label="username"> {{ dt.params?.selectAuthCert.username }} </el-descriptions-item>
 | 
				
			||||||
 | 
					                                <el-descriptions-item label="remark"> {{ dt.params?.remark }} </el-descriptions-item>
 | 
				
			||||||
 | 
					                            </el-descriptions>
 | 
				
			||||||
 | 
					                        </template>
 | 
				
			||||||
 | 
					                    </el-popover>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <div class="terminal-wrapper flex-1 h-[calc(100vh-155px)]">
 | 
				
			||||||
 | 
					                    <TerminalBody
 | 
				
			||||||
 | 
					                        v-if="dt.params.protocol == MachineProtocolEnum.Ssh.value"
 | 
				
			||||||
 | 
					                        :mount-init="false"
 | 
				
			||||||
 | 
					                        @status-change="terminalStatusChange(dt.key, $event)"
 | 
				
			||||||
 | 
					                        :ref="(el: any) => setTerminalRef(el, dt.key)"
 | 
				
			||||||
 | 
					                        :socket-url="dt.socketUrl"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                    <machine-rdp
 | 
				
			||||||
 | 
					                        v-if="dt.params.protocol != MachineProtocolEnum.Ssh.value"
 | 
				
			||||||
 | 
					                        :machine-id="dt.params.id"
 | 
				
			||||||
 | 
					                        :auth-cert="dt.authCert"
 | 
				
			||||||
 | 
					                        :ref="(el: any) => setTerminalRef(el, dt.key)"
 | 
				
			||||||
 | 
					                        @status-change="terminalStatusChange(dt.key, $event)"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </el-tab-pane>
 | 
				
			||||||
 | 
					        </el-tabs>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-dialog v-if="infoDialog.visible" v-model="infoDialog.visible">
 | 
				
			||||||
 | 
					            <el-descriptions :title="$t('common.detail')" :column="3" border>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1.5" label="ID">{{ infoDialog.data.id }}</el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1.5" :label="$t('common.name')">{{ infoDialog.data.name }}</el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="3" :label="$t('tag.relateTag')">
 | 
				
			||||||
 | 
					                    <ResourceTags :tags="infoDialog.data.tags" />
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="2" label="IP">{{ infoDialog.data.ip }}</el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1" :label="$t('machine.port')">{{ infoDialog.data.port }}</el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="3" :label="$t('common.remark')">{{ infoDialog.data.remark }}</el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1.5" :label="$t('machine.sshTunnel')"
 | 
				
			||||||
 | 
					                    >{{ infoDialog.data.sshTunnelMachineId > 0 ? $t('common.yes') : $t('common.no') }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1.5" :label="$t('machine.terminalPlayback')"
 | 
				
			||||||
 | 
					                    >{{ infoDialog.data.enableRecorder == 1 ? $t('common.yes') : $t('common.no') }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="2" :label="$t('common.createTime')">
 | 
				
			||||||
 | 
					                    {{ formatDate(infoDialog.data.createTime) }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1" :label="$t('common.creator')">
 | 
				
			||||||
 | 
					                    {{ infoDialog.data.creator }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="2" :label="$t('common.updateTime')">
 | 
				
			||||||
 | 
					                    {{ formatDate(infoDialog.data.updateTime) }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item :span="1" :label="$t('common.modifier')">
 | 
				
			||||||
 | 
					                    {{ infoDialog.data.modifier }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					            </el-descriptions>
 | 
				
			||||||
 | 
					        </el-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <process-list v-model:visible="processDialog.visible" v-model:machineId="processDialog.machineId" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <script-manage
 | 
				
			||||||
 | 
					            :title="serviceDialog.title"
 | 
				
			||||||
 | 
					            v-model:visible="serviceDialog.visible"
 | 
				
			||||||
 | 
					            v-model:machineId="serviceDialog.machineId"
 | 
				
			||||||
 | 
					            :auth-cert-name="serviceDialog.authCertName"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <file-conf-list
 | 
				
			||||||
 | 
					            :title="fileDialog.title"
 | 
				
			||||||
 | 
					            :auth-cert-name="fileDialog.authCertName"
 | 
				
			||||||
 | 
					            v-model:visible="fileDialog.visible"
 | 
				
			||||||
 | 
					            v-model:machineId="fileDialog.machineId"
 | 
				
			||||||
 | 
					            :protocol="fileDialog.protocol"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-dialog destroy-on-close :title="state.filesystemDialog.title" v-model="state.filesystemDialog.visible" :close-on-click-modal="false" width="70%">
 | 
				
			||||||
 | 
					            <machine-file
 | 
				
			||||||
 | 
					                :machine-id="state.filesystemDialog.machineId"
 | 
				
			||||||
 | 
					                :auth-cert-name="state.filesystemDialog.authCertName"
 | 
				
			||||||
 | 
					                :protocol="state.filesystemDialog.protocol"
 | 
				
			||||||
 | 
					                :file-id="state.filesystemDialog.fileId"
 | 
				
			||||||
 | 
					                :path="state.filesystemDialog.path"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        </el-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <machine-stats v-model:visible="machineStatsDialog.visible" :machineId="machineStatsDialog.machineId" :title="machineStatsDialog.title" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <machine-rec v-model:visible="machineRecDialog.visible" :machineId="machineRecDialog.machineId" :title="machineRecDialog.title" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { defineAsyncComponent, getCurrentInstance, inject, nextTick, onMounted, reactive, ref, toRefs, watch } from 'vue';
 | 
				
			||||||
 | 
					import { useRouter } from 'vue-router';
 | 
				
			||||||
 | 
					import { getMachineTerminalSocketUrl } from '../api';
 | 
				
			||||||
 | 
					import { formatDate } from '@/common/utils/format';
 | 
				
			||||||
 | 
					import { hasPerms } from '@/components/auth/auth';
 | 
				
			||||||
 | 
					import TerminalBody from '@/components/terminal/TerminalBody.vue';
 | 
				
			||||||
 | 
					import { TerminalStatus, TerminalStatusEnum } from '@/components/terminal/common';
 | 
				
			||||||
 | 
					import MachineRdp from '@/components/terminal-rdp/MachineRdp.vue';
 | 
				
			||||||
 | 
					import MachineFile from '@/views/ops/machine/file/MachineFile.vue';
 | 
				
			||||||
 | 
					import ResourceTags from '../../component/ResourceTags.vue';
 | 
				
			||||||
 | 
					import { MachineProtocolEnum } from '../enums';
 | 
				
			||||||
 | 
					import EnumValue from '@/common/Enum';
 | 
				
			||||||
 | 
					import { useI18n } from 'vue-i18n';
 | 
				
			||||||
 | 
					import { ResourceOpCtx } from '@/views/ops/component/tag';
 | 
				
			||||||
 | 
					import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
 | 
				
			||||||
 | 
					import { MachineOpComp } from '@/views/ops/machine/resource';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 组件
 | 
				
			||||||
 | 
					const ScriptManage = defineAsyncComponent(() => import('../ScriptManage.vue'));
 | 
				
			||||||
 | 
					const FileConfList = defineAsyncComponent(() => import('../file/FileConfList.vue'));
 | 
				
			||||||
 | 
					const MachineStats = defineAsyncComponent(() => import('../MachineStats.vue'));
 | 
				
			||||||
 | 
					const MachineRec = defineAsyncComponent(() => import('../MachineRec.vue'));
 | 
				
			||||||
 | 
					const ProcessList = defineAsyncComponent(() => import('../ProcessList.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { t } = useI18n();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const router = useRouter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const perms = {
 | 
				
			||||||
 | 
					    addMachine: 'machine:add',
 | 
				
			||||||
 | 
					    updateMachine: 'machine:update',
 | 
				
			||||||
 | 
					    delMachine: 'machine:del',
 | 
				
			||||||
 | 
					    terminal: 'machine:terminal',
 | 
				
			||||||
 | 
					    closeCli: 'machine:close-cli',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 该用户拥有的的操作列按钮权限,使用v-if进行判断,v-auth对el-dropdown-item无效
 | 
				
			||||||
 | 
					const actionBtns = hasPerms([perms.updateMachine, perms.closeCli]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emits = defineEmits(['init']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    defaultExpendKey: [] as any,
 | 
				
			||||||
 | 
					    params: {
 | 
				
			||||||
 | 
					        pageNum: 1,
 | 
				
			||||||
 | 
					        pageSize: 0,
 | 
				
			||||||
 | 
					        ip: null,
 | 
				
			||||||
 | 
					        name: null,
 | 
				
			||||||
 | 
					        tagPath: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    infoDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        data: null as any,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    serviceDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        machineId: 0,
 | 
				
			||||||
 | 
					        authCertName: '',
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    processDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        machineId: 0,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    fileDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        machineId: 0,
 | 
				
			||||||
 | 
					        protocol: 1,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        authCertName: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    filesystemDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        machineId: 0,
 | 
				
			||||||
 | 
					        authCertName: '',
 | 
				
			||||||
 | 
					        protocol: 1,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        fileId: 0,
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    machineStatsDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        stats: null,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					        machineId: 0,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    machineRecDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        machineId: 0,
 | 
				
			||||||
 | 
					        title: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    activeTermName: '',
 | 
				
			||||||
 | 
					    tabs: new Map<string, any>(),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { infoDialog, serviceDialog, processDialog, fileDialog, machineStatsDialog, machineRecDialog } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let openIds: any = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					    () => state.activeTermName,
 | 
				
			||||||
 | 
					    (newValue, oldValue) => {
 | 
				
			||||||
 | 
					        fitTerminal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        oldValue && terminalRefs[oldValue]?.blur && terminalRefs[oldValue]?.blur();
 | 
				
			||||||
 | 
					        terminalRefs[newValue]?.focus && terminalRefs[newValue]?.focus();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const nowTab = state.tabs.get(state.activeTermName);
 | 
				
			||||||
 | 
					        resourceOpCtx?.setCurrentTreeKey(nowTab?.authCert);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					    emits('init', { name: MachineOpComp.name, ref: getCurrentInstance()?.exposed });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const openTerminal = (machine: any, ex?: boolean) => {
 | 
				
			||||||
 | 
					    // 授权凭证名
 | 
				
			||||||
 | 
					    const ac = machine.selectAuthCert.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 新窗口打开
 | 
				
			||||||
 | 
					    if (ex) {
 | 
				
			||||||
 | 
					        if (machine.protocol == MachineProtocolEnum.Ssh.value) {
 | 
				
			||||||
 | 
					            const { href } = router.resolve({
 | 
				
			||||||
 | 
					                path: `/machine/terminal`,
 | 
				
			||||||
 | 
					                query: {
 | 
				
			||||||
 | 
					                    ac,
 | 
				
			||||||
 | 
					                    name: machine.name,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            window.open(href, '_blank');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (machine.protocol == MachineProtocolEnum.Rdp.value) {
 | 
				
			||||||
 | 
					            const { href } = router.resolve({
 | 
				
			||||||
 | 
					                path: `/machine/terminal-rdp`,
 | 
				
			||||||
 | 
					                query: {
 | 
				
			||||||
 | 
					                    machineId: machine.id,
 | 
				
			||||||
 | 
					                    ac: ac,
 | 
				
			||||||
 | 
					                    name: machine.name,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            window.open(href, '_blank');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let { name } = machine;
 | 
				
			||||||
 | 
					    const labelName = `${machine.selectAuthCert.username}@${name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 同一个机器的终端打开多次,key后添加下划线和数字区分
 | 
				
			||||||
 | 
					    openIds[ac] = openIds[ac] ? ++openIds[ac] : 1;
 | 
				
			||||||
 | 
					    let sameIndex = openIds[ac];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let key = `${ac}_${sameIndex}`;
 | 
				
			||||||
 | 
					    // 只保留name的15个字,超出部分只保留前后10个字符,中间用省略号代替
 | 
				
			||||||
 | 
					    const label = labelName.length > 15 ? labelName.slice(0, 10) + '...' + labelName.slice(-10) : labelName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let tab = {
 | 
				
			||||||
 | 
					        key,
 | 
				
			||||||
 | 
					        label: `${label}${sameIndex === 1 ? '' : ':' + sameIndex}`, // label组成为:总打开term次数+name+同一个机器打开的次数
 | 
				
			||||||
 | 
					        params: machine,
 | 
				
			||||||
 | 
					        authCert: ac,
 | 
				
			||||||
 | 
					        socketUrl: getMachineTerminalSocketUrl(ac),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    state.tabs.set(key, tab);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    nextTick(() => {
 | 
				
			||||||
 | 
					        handleReconnect(tab);
 | 
				
			||||||
 | 
					        state.activeTermName = key;
 | 
				
			||||||
 | 
					        setTimeout(() => fitTerminal(), 300);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const serviceManager = (row: any) => {
 | 
				
			||||||
 | 
					    const authCert = row.selectAuthCert;
 | 
				
			||||||
 | 
					    state.serviceDialog.machineId = row.id;
 | 
				
			||||||
 | 
					    state.serviceDialog.visible = true;
 | 
				
			||||||
 | 
					    state.serviceDialog.authCertName = authCert.name;
 | 
				
			||||||
 | 
					    state.serviceDialog.title = `${row.name} => ${authCert.username}@${row.ip}`;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 显示机器状态统计信息
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const showMachineStats = (machine: any) => {
 | 
				
			||||||
 | 
					    state.machineStatsDialog.machineId = machine.id;
 | 
				
			||||||
 | 
					    state.machineStatsDialog.title = `${t('machine.machineState')}: ${machine.name} => ${machine.ip}`;
 | 
				
			||||||
 | 
					    state.machineStatsDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showFileManage = (selectionData: any) => {
 | 
				
			||||||
 | 
					    const authCert = selectionData.selectAuthCert;
 | 
				
			||||||
 | 
					    if (selectionData.protocol == 1) {
 | 
				
			||||||
 | 
					        state.fileDialog.visible = true;
 | 
				
			||||||
 | 
					        state.fileDialog.protocol = selectionData.protocol;
 | 
				
			||||||
 | 
					        state.fileDialog.machineId = selectionData.id;
 | 
				
			||||||
 | 
					        state.fileDialog.authCertName = authCert.name;
 | 
				
			||||||
 | 
					        state.fileDialog.title = `${selectionData.name} => ${authCert.username}@${selectionData.ip}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (selectionData.protocol == 2) {
 | 
				
			||||||
 | 
					        state.filesystemDialog.protocol = 2;
 | 
				
			||||||
 | 
					        state.filesystemDialog.machineId = selectionData.id;
 | 
				
			||||||
 | 
					        state.filesystemDialog.authCertName = authCert.name;
 | 
				
			||||||
 | 
					        state.filesystemDialog.fileId = selectionData.id;
 | 
				
			||||||
 | 
					        state.filesystemDialog.path = '/';
 | 
				
			||||||
 | 
					        state.filesystemDialog.title = t('machine.remoteFileDesktopManage');
 | 
				
			||||||
 | 
					        state.filesystemDialog.visible = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showInfo = (info: any) => {
 | 
				
			||||||
 | 
					    state.infoDialog.data = info;
 | 
				
			||||||
 | 
					    state.infoDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showProcess = (row: any) => {
 | 
				
			||||||
 | 
					    state.processDialog.machineId = row.id;
 | 
				
			||||||
 | 
					    state.processDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showRec = (row: any) => {
 | 
				
			||||||
 | 
					    state.machineRecDialog.title = `${row.name}[${row.ip}]-${t('machine.terminalPlayback')}`;
 | 
				
			||||||
 | 
					    state.machineRecDialog.machineId = row.id;
 | 
				
			||||||
 | 
					    state.machineRecDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onRemoveTab = (targetName: string) => {
 | 
				
			||||||
 | 
					    let activeTermName = state.activeTermName;
 | 
				
			||||||
 | 
					    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);
 | 
				
			||||||
 | 
					        let info = state.tabs.get(targetName);
 | 
				
			||||||
 | 
					        if (info) {
 | 
				
			||||||
 | 
					            terminalRefs[info.key]?.close();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (activeTermName != targetName) {
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 如果删除的tab是当前激活的tab,则切换到前一个或后一个tab
 | 
				
			||||||
 | 
					        const nextTab = tabNames[i + 1] || tabNames[i - 1];
 | 
				
			||||||
 | 
					        if (nextTab) {
 | 
				
			||||||
 | 
					            activeTermName = nextTab;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            activeTermName = '';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        state.activeTermName = activeTermName;
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const terminalStatusChange = (key: string, status: TerminalStatus) => {
 | 
				
			||||||
 | 
					    state.tabs.get(key).status = status;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const terminalRefs: any = {};
 | 
				
			||||||
 | 
					const setTerminalRef = (el: any, key: any) => {
 | 
				
			||||||
 | 
					    if (key) {
 | 
				
			||||||
 | 
					        terminalRefs[key] = el;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fitTerminal = () => {
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					        let info = state.tabs.get(state.activeTermName);
 | 
				
			||||||
 | 
					        if (info) {
 | 
				
			||||||
 | 
					            terminalRefs[info.key]?.fitTerminal && terminalRefs[info.key]?.fitTerminal();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleReconnect = (tab: any, force = false) => {
 | 
				
			||||||
 | 
					    terminalRefs[tab.key]?.init();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
					    openTerminal,
 | 
				
			||||||
 | 
					    onResize: fitTerminal,
 | 
				
			||||||
 | 
					    showInfo,
 | 
				
			||||||
 | 
					    showProcess,
 | 
				
			||||||
 | 
					    showRec,
 | 
				
			||||||
 | 
					    showMachineStats,
 | 
				
			||||||
 | 
					    showFileManage,
 | 
				
			||||||
 | 
					    serviceManager,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					.machine-terminal-tabs {
 | 
				
			||||||
 | 
					    --el-tabs-header-height: 30px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .el-tabs {
 | 
				
			||||||
 | 
					        --el-tabs-header-height: 30px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .machine-terminal-tab-label {
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .el-tabs__header {
 | 
				
			||||||
 | 
					        margin-bottom: 5px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .el-tabs__item {
 | 
				
			||||||
 | 
					        padding: 0 8px !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										13
									
								
								frontend/src/views/ops/machine/resource/NodeMachineAc.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <BaseTreeNode v-bind="$attrs">
 | 
				
			||||||
 | 
					        <template #suffix="{ data }">
 | 
				
			||||||
 | 
					            <span>{{ ` ${data.params.selectAuthCert.username}@${data.params.ip}:${data.params.port}` }}</span>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					    </BaseTreeNode>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss"></style>
 | 
				
			||||||
							
								
								
									
										119
									
								
								frontend/src/views/ops/machine/resource/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					import { ContextmenuItem } from '@/components/contextmenu';
 | 
				
			||||||
 | 
					import { ResourceTypeEnum } from '@/common/commonEnum';
 | 
				
			||||||
 | 
					import { defineAsyncComponent } from 'vue';
 | 
				
			||||||
 | 
					import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '@/views/ops/component/tag';
 | 
				
			||||||
 | 
					import { machineApi } from '@/views/ops/machine/api';
 | 
				
			||||||
 | 
					import { MachineProtocolEnum } from '@/views/ops/machine/enums';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MachineList = defineAsyncComponent(() => import('../MachineList.vue'));
 | 
				
			||||||
 | 
					const MachineOp = defineAsyncComponent(() => import('./MachineOp.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeMachineAc = defineAsyncComponent(() => import('./NodeMachineAc.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MachineIcon = {
 | 
				
			||||||
 | 
					    name: ResourceTypeEnum.Machine.extra.icon,
 | 
				
			||||||
 | 
					    color: ResourceTypeEnum.Machine.extra.iconColor,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MachineOpComp: ResourceComponentConfig = {
 | 
				
			||||||
 | 
					    name: 'tag.machineOp',
 | 
				
			||||||
 | 
					    component: MachineOp,
 | 
				
			||||||
 | 
					    icon: MachineIcon,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const NodeTypeMachineTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					    node.ctx?.addResourceComponent(MachineOpComp);
 | 
				
			||||||
 | 
					    // 加载标签树下的机器列表
 | 
				
			||||||
 | 
					    const res = await machineApi.list.request({ tagPath: node.params.tagPath });
 | 
				
			||||||
 | 
					    // 把list 根据name字段排序
 | 
				
			||||||
 | 
					    return res?.list
 | 
				
			||||||
 | 
					        .sort((a: any, b: any) => a.name.localeCompare(b.name))
 | 
				
			||||||
 | 
					        .map((x: any) =>
 | 
				
			||||||
 | 
					            TagTreeNode.new(node, x.code, x.name, NodeTypeMachine)
 | 
				
			||||||
 | 
					                .withParams(x)
 | 
				
			||||||
 | 
					                .withDisabled(x.status == -1 && x.protocol == MachineProtocolEnum.Ssh.value)
 | 
				
			||||||
 | 
					                .withIcon(MachineIcon)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeTypeMachine = new NodeType(11)
 | 
				
			||||||
 | 
					    .withLoadNodesFunc((node: TagTreeNode) => {
 | 
				
			||||||
 | 
					        const machine = node.params;
 | 
				
			||||||
 | 
					        // 获取授权凭证列表
 | 
				
			||||||
 | 
					        const authCerts = machine.authCerts;
 | 
				
			||||||
 | 
					        return authCerts.map((x: any) =>
 | 
				
			||||||
 | 
					            TagTreeNode.new(node, x.name, x.username, NodeTypeAuthCert)
 | 
				
			||||||
 | 
					                .withNodeComponent(NodeMachineAc)
 | 
				
			||||||
 | 
					                .withParams({ ...machine, selectAuthCert: x })
 | 
				
			||||||
 | 
					                .withDisabled(machine.status == -1 && machine.protocol == MachineProtocolEnum.Ssh.value)
 | 
				
			||||||
 | 
					                .withIcon({
 | 
				
			||||||
 | 
					                    name: 'Ticket',
 | 
				
			||||||
 | 
					                    color: '#409eff',
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                .withIsLeaf(true)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .withContextMenuItems([
 | 
				
			||||||
 | 
					        new ContextmenuItem('detail', 'common.detail').withIcon('More').withOnClick(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					            (await node.ctx?.addResourceComponent(MachineOpComp)).showInfo(node.params);
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new ContextmenuItem('status', 'common.status')
 | 
				
			||||||
 | 
					            .withIcon('Compass')
 | 
				
			||||||
 | 
					            .withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
 | 
				
			||||||
 | 
					            .withOnClick(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					                (await node.ctx?.addResourceComponent(MachineOpComp)).showMachineStats(node.params);
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new ContextmenuItem('process', 'machine.process')
 | 
				
			||||||
 | 
					            .withIcon('DataLine')
 | 
				
			||||||
 | 
					            .withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
 | 
				
			||||||
 | 
					            .withOnClick(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					                (await node.ctx?.addResourceComponent(MachineOpComp)).showProcess(node.params);
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new ContextmenuItem('edit', 'machine.terminalPlayback')
 | 
				
			||||||
 | 
					            .withIcon('Compass')
 | 
				
			||||||
 | 
					            .withOnClick(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					                (await node.ctx?.addResourceComponent(MachineOpComp)).showRec(node.params);
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .withHideFunc((node: any) => node.params.enableRecorder == 1),
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeTypeAuthCert = new NodeType(12)
 | 
				
			||||||
 | 
					    .withNodeDblclickFunc(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					        (await node.ctx?.addResourceComponent(MachineOpComp)).openTerminal(node.params);
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .withContextMenuItems([
 | 
				
			||||||
 | 
					        new ContextmenuItem('term', 'machine.openTerminal').withIcon('Monitor').withOnClick(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					            (await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params);
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal').withIcon('Monitor').withOnClick(async (node: TagTreeNode) => {
 | 
				
			||||||
 | 
					            (await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params, true);
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        new ContextmenuItem('files', 'machine.fileManage').withIcon('FolderOpened').withOnClick(async (node: any) => {
 | 
				
			||||||
 | 
					            (await node.ctx?.addResourceComponent(MachineOpComp)).showFileManage(node.params);
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new ContextmenuItem('scripts', 'machine.scriptManage')
 | 
				
			||||||
 | 
					            .withIcon('Files')
 | 
				
			||||||
 | 
					            .withHideFunc((node: any) => node.params.protocol != MachineProtocolEnum.Ssh.value)
 | 
				
			||||||
 | 
					            .withOnClick(async (node: any) => {
 | 
				
			||||||
 | 
					                (await node.ctx?.addResourceComponent(MachineOpComp)).serviceManager(node.params);
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					    order: 1,
 | 
				
			||||||
 | 
					    resourceType: ResourceTypeEnum.Machine.value,
 | 
				
			||||||
 | 
					    rootNodeType: NodeTypeMachineTag,
 | 
				
			||||||
 | 
					    manager: {
 | 
				
			||||||
 | 
					        componentConf: {
 | 
				
			||||||
 | 
					            component: MachineList,
 | 
				
			||||||
 | 
					            icon: MachineIcon,
 | 
				
			||||||
 | 
					            name: 'tag.machine',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        permCode: 'machine',
 | 
				
			||||||
 | 
					        countKey: 'machine',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					} as ResourceConfig;
 | 
				
			||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
    MachineList: () => import('@/views/ops/machine/MachineList.vue'),
 | 
					 | 
				
			||||||
    MachineOp: () => import('@/views/ops/machine/MachineOp.vue'),
 | 
					 | 
				
			||||||
    CronJobList: () => import('@/views/ops/machine/cronjob/CronJobList.vue'),
 | 
					    CronJobList: () => import('@/views/ops/machine/cronjob/CronJobList.vue'),
 | 
				
			||||||
    SecurityConfList: () => import('@/views/ops/machine/security/SecurityConfList.vue'),
 | 
					    SecurityConfList: () => import('@/views/ops/machine/security/SecurityConfList.vue'),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,561 +0,0 @@
 | 
				
			|||||||
<template>
 | 
					 | 
				
			||||||
    <div class="flex-all-center h-full">
 | 
					 | 
				
			||||||
        <ResourceOpPanel>
 | 
					 | 
				
			||||||
            <template #left>
 | 
					 | 
				
			||||||
                <tag-tree
 | 
					 | 
				
			||||||
                    ref="tagTreeRef"
 | 
					 | 
				
			||||||
                    :default-expanded-keys="state.defaultExpendKey"
 | 
					 | 
				
			||||||
                    :resource-type="TagResourceTypeEnum.Mongo.value"
 | 
					 | 
				
			||||||
                    :tag-path-node-type="NodeTypeTagPath"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                    <template #prefix="{ data }">
 | 
					 | 
				
			||||||
                        <span v-if="data.type.value == MongoNodeType.Mongo">
 | 
					 | 
				
			||||||
                            <el-popover :show-after="500" placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
 | 
					 | 
				
			||||||
                                <template #reference>
 | 
					 | 
				
			||||||
                                    <SvgIcon name="icon mongo/mongo-color" :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="url">
 | 
					 | 
				
			||||||
                                            {{ data.params.uri }}
 | 
					 | 
				
			||||||
                                        </el-descriptions-item>
 | 
					 | 
				
			||||||
                                    </el-descriptions>
 | 
					 | 
				
			||||||
                                </template>
 | 
					 | 
				
			||||||
                            </el-popover>
 | 
					 | 
				
			||||||
                        </span>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        <SvgIcon v-if="data.type.value == MongoNodeType.Dbs" name="Coin" color="#67c23a" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        <SvgIcon
 | 
					 | 
				
			||||||
                            v-if="data.type.value == MongoNodeType.Coll || data.type.value == MongoNodeType.CollMenu"
 | 
					 | 
				
			||||||
                            name="Document"
 | 
					 | 
				
			||||||
                            class="color-primary"
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                    </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <template #suffix="{ data }">
 | 
					 | 
				
			||||||
                        <span v-if="data.type.value == MongoNodeType.Dbs">{{ formatByteSize(data.params.size) }}</span>
 | 
					 | 
				
			||||||
                    </template>
 | 
					 | 
				
			||||||
                </tag-tree>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <template #right>
 | 
					 | 
				
			||||||
                <div class="mongo-data-tab card h-full !p-1 w-full">
 | 
					 | 
				
			||||||
                    <el-row v-if="nowColl">
 | 
					 | 
				
			||||||
                        <el-descriptions class="!w-full" :column="10" size="small" border>
 | 
					 | 
				
			||||||
                            <!-- <el-descriptions-item label-align="right" label="tag">xxx</el-descriptions-item> -->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            <el-descriptions-item label="ns" label-align="right">
 | 
					 | 
				
			||||||
                                {{ nowColl.stats?.ns }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item label="count" label-align="right">
 | 
					 | 
				
			||||||
                                {{ nowColl.stats?.count }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item label="avgObjSize" label-align="right">
 | 
					 | 
				
			||||||
                                {{ formatByteSize(nowColl.stats?.avgObjSize) }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item label="size" label-align="right">
 | 
					 | 
				
			||||||
                                {{ formatByteSize(nowColl.stats?.size) }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item label="totalSize" label-align="right">
 | 
					 | 
				
			||||||
                                {{ formatByteSize(nowColl.stats?.totalSize) }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item label="storageSize" label-align="right">
 | 
					 | 
				
			||||||
                                {{ formatByteSize(nowColl.stats?.storageSize) }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                            <el-descriptions-item label="freeStorageSize" label-align="right">
 | 
					 | 
				
			||||||
                                {{ formatByteSize(nowColl.stats?.freeStorageSize) }}
 | 
					 | 
				
			||||||
                            </el-descriptions-item>
 | 
					 | 
				
			||||||
                        </el-descriptions>
 | 
					 | 
				
			||||||
                    </el-row>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <el-row type="flex">
 | 
					 | 
				
			||||||
                        <el-tabs @tab-remove="removeDataTab" class="!w-full ml-1" v-model="state.activeName">
 | 
					 | 
				
			||||||
                            <el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
 | 
					 | 
				
			||||||
                                <el-row>
 | 
					 | 
				
			||||||
                                    <el-col :span="2">
 | 
					 | 
				
			||||||
                                        <div class="mt-1">
 | 
					 | 
				
			||||||
                                            <el-link @click="findCommand(state.activeName)" icon="refresh" underline="never" class=""> </el-link>
 | 
					 | 
				
			||||||
                                            <el-divider direction="vertical" border-style="dashed" />
 | 
					 | 
				
			||||||
                                            <el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" underline="never"> </el-link>
 | 
					 | 
				
			||||||
                                        </div>
 | 
					 | 
				
			||||||
                                    </el-col>
 | 
					 | 
				
			||||||
                                    <el-col :span="22">
 | 
					 | 
				
			||||||
                                        <el-input
 | 
					 | 
				
			||||||
                                            ref="findParamInputRef"
 | 
					 | 
				
			||||||
                                            v-model="dt.findParamStr"
 | 
					 | 
				
			||||||
                                            :placeholder="$t('mongo.queryParamPlaceholder')"
 | 
					 | 
				
			||||||
                                            @focus="showFindDialog(dt.key)"
 | 
					 | 
				
			||||||
                                        >
 | 
					 | 
				
			||||||
                                            <template #prepend>{{ $t('mongo.queryParam') }}</template>
 | 
					 | 
				
			||||||
                                        </el-input>
 | 
					 | 
				
			||||||
                                    </el-col>
 | 
					 | 
				
			||||||
                                </el-row>
 | 
					 | 
				
			||||||
                                <el-scrollbar class="mongo-data-tab-data">
 | 
					 | 
				
			||||||
                                    <el-row>
 | 
					 | 
				
			||||||
                                        <el-col :span="6" v-for="item in dt.datas" :key="item">
 | 
					 | 
				
			||||||
                                            <el-card :body-style="{ padding: '0px', position: 'relative' }">
 | 
					 | 
				
			||||||
                                                <el-input type="textarea" v-model="item.value" :rows="10" />
 | 
					 | 
				
			||||||
                                                <div style="padding: 3px; float: right" class="mr-1 mongo-doc-btns">
 | 
					 | 
				
			||||||
                                                    <div>
 | 
					 | 
				
			||||||
                                                        <el-link @click="onEditDoc(item)" underline="never" type="success" icon="MagicStick"></el-link>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                                        <el-divider direction="vertical" border-style="dashed" />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                                                        <el-popconfirm @confirm="onDeleteDoc(item.value)" :title="$t('mongo.deleteDocConfirm')" width="160">
 | 
					 | 
				
			||||||
                                                            <template #reference>
 | 
					 | 
				
			||||||
                                                                <el-link v-auth="perms.delData" underline="never" type="danger" icon="DocumentDelete">
 | 
					 | 
				
			||||||
                                                                </el-link>
 | 
					 | 
				
			||||||
                                                            </template>
 | 
					 | 
				
			||||||
                                                        </el-popconfirm>
 | 
					 | 
				
			||||||
                                                    </div>
 | 
					 | 
				
			||||||
                                                </div>
 | 
					 | 
				
			||||||
                                            </el-card>
 | 
					 | 
				
			||||||
                                        </el-col>
 | 
					 | 
				
			||||||
                                    </el-row>
 | 
					 | 
				
			||||||
                                </el-scrollbar>
 | 
					 | 
				
			||||||
                            </el-tab-pane>
 | 
					 | 
				
			||||||
                        </el-tabs>
 | 
					 | 
				
			||||||
                    </el-row>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
        </ResourceOpPanel>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <el-dialog width="600px" title="find params" v-model="findDialog.visible">
 | 
					 | 
				
			||||||
            <el-form label-width="auto">
 | 
					 | 
				
			||||||
                <el-form-item label="filter">
 | 
					 | 
				
			||||||
                    <monaco-editor style="width: 100%" height="150px" ref="monacoEditorRef" v-model="findDialog.findParam.filter" language="json" />
 | 
					 | 
				
			||||||
                </el-form-item>
 | 
					 | 
				
			||||||
                <el-form-item label="sort">
 | 
					 | 
				
			||||||
                    <el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable auto-complete="off"></el-input>
 | 
					 | 
				
			||||||
                </el-form-item>
 | 
					 | 
				
			||||||
                <el-form-item label="limit">
 | 
					 | 
				
			||||||
                    <el-input v-model.number="findDialog.findParam.limit" type="number" auto-complete="off"></el-input>
 | 
					 | 
				
			||||||
                </el-form-item>
 | 
					 | 
				
			||||||
                <el-form-item label="skip">
 | 
					 | 
				
			||||||
                    <el-input v-model.number="findDialog.findParam.skip" type="number" auto-complete="off"></el-input>
 | 
					 | 
				
			||||||
                </el-form-item>
 | 
					 | 
				
			||||||
            </el-form>
 | 
					 | 
				
			||||||
            <template #footer>
 | 
					 | 
				
			||||||
                <div>
 | 
					 | 
				
			||||||
                    <el-button @click="findDialog.visible = false">{{ $t('common.cancel') }}</el-button>
 | 
					 | 
				
			||||||
                    <el-button @click="confirmFindDialog" type="primary">{{ $t('common.confirm') }}</el-button>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
        </el-dialog>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <el-dialog
 | 
					 | 
				
			||||||
            width="60%"
 | 
					 | 
				
			||||||
            :title="`${state.docEditDialog.isAdd ? $t('common.add') : $t('common.edit')} '${state.activeName}' $t('mongo.doc')`"
 | 
					 | 
				
			||||||
            v-model="docEditDialog.visible"
 | 
					 | 
				
			||||||
            :close-on-click-modal="false"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
            <monaco-editor v-model="docEditDialog.doc" language="json" />
 | 
					 | 
				
			||||||
            <template #footer>
 | 
					 | 
				
			||||||
                <div>
 | 
					 | 
				
			||||||
                    <el-button @click="docEditDialog.visible = false">{{ $t('common.cancel') }}</el-button>
 | 
					 | 
				
			||||||
                    <el-button v-auth="perms.saveData" @click="onSaveDoc" type="primary">{{ $t('common.confirm') }}</el-button>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
            </template>
 | 
					 | 
				
			||||||
        </el-dialog>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div style="text-align: center; margin-top: 10px"></div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts" setup>
 | 
					 | 
				
			||||||
import { mongoApi } from './api';
 | 
					 | 
				
			||||||
import { computed, defineAsyncComponent, onMounted, reactive, ref, toRefs, watch } from 'vue';
 | 
					 | 
				
			||||||
import { ElMessage } from 'element-plus';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { isTrue, notBlank } from '@/common/assert';
 | 
					 | 
				
			||||||
import { TagTreeNode, NodeType, getTagTypeCodeByPath } from '../component/tag';
 | 
					 | 
				
			||||||
import TagTree from '../component/TagTree.vue';
 | 
					 | 
				
			||||||
import { formatByteSize } from '@/common/utils/format';
 | 
					 | 
				
			||||||
import { TagResourceTypeEnum } from '@/common/commonEnum';
 | 
					 | 
				
			||||||
import { sleep } from '@/common/utils/loading';
 | 
					 | 
				
			||||||
import { useAutoOpenResource } from '@/store/autoOpenResource';
 | 
					 | 
				
			||||||
import { storeToRefs } from 'pinia';
 | 
					 | 
				
			||||||
import { useI18n } from 'vue-i18n';
 | 
					 | 
				
			||||||
import { useI18nDeleteSuccessMsg, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
 | 
					 | 
				
			||||||
import ResourceOpPanel from '../component/ResourceOpPanel.vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { t } = useI18n();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const perms = {
 | 
					 | 
				
			||||||
    saveData: 'mongo:data:save',
 | 
					 | 
				
			||||||
    delData: 'mongo:data:del',
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 树节点类型
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
class MongoNodeType {
 | 
					 | 
				
			||||||
    static Mongo = 1;
 | 
					 | 
				
			||||||
    static Dbs = 2;
 | 
					 | 
				
			||||||
    static CollMenu = 3;
 | 
					 | 
				
			||||||
    static Coll = 4;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// tagpath 节点类型
 | 
					 | 
				
			||||||
const NodeTypeTagPath = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
					 | 
				
			||||||
    const res = await mongoApi.mongoList.request({ tagPath: parentNode.key });
 | 
					 | 
				
			||||||
    if (!res.total) {
 | 
					 | 
				
			||||||
        return [];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const mongoInfos = res.list;
 | 
					 | 
				
			||||||
    await sleep(100);
 | 
					 | 
				
			||||||
    return mongoInfos?.map((x: any) => {
 | 
					 | 
				
			||||||
        x.tagPath = parentNode.key;
 | 
					 | 
				
			||||||
        return new TagTreeNode(`${x.code}`, x.name, NodeTypeMongo).withParams(x);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const NodeTypeMongo = new NodeType(MongoNodeType.Mongo).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
					 | 
				
			||||||
    const inst = parentNode.params;
 | 
					 | 
				
			||||||
    // 点击mongo -> 加载mongo数据库列表
 | 
					 | 
				
			||||||
    const res = await mongoApi.databases.request({ id: inst.id });
 | 
					 | 
				
			||||||
    return res.Databases.map((x: any) => {
 | 
					 | 
				
			||||||
        const database = x.Name;
 | 
					 | 
				
			||||||
        return new TagTreeNode(`${inst.id}.${database}`, database, NodeTypeDbs).withParams({
 | 
					 | 
				
			||||||
            id: inst.id,
 | 
					 | 
				
			||||||
            database,
 | 
					 | 
				
			||||||
            size: x.SizeOnDisk,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const NodeTypeDbs = new NodeType(MongoNodeType.Dbs).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
					 | 
				
			||||||
    const params = parentNode.params;
 | 
					 | 
				
			||||||
    // 点击数据库列表 -> 加载数据库下拥有的菜单列表
 | 
					 | 
				
			||||||
    return [new TagTreeNode(`${params.id}.${params.database}.mongo-coll`, 'mongo.coll', NodeTypeCollMenu).withParams(params)];
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const NodeTypeCollMenu = new NodeType(MongoNodeType.CollMenu).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
					 | 
				
			||||||
    const { id, database } = parentNode.params;
 | 
					 | 
				
			||||||
    // 点击数据库集合节点 -> 加载集合列表
 | 
					 | 
				
			||||||
    const colls = await mongoApi.collections.request({ id, database });
 | 
					 | 
				
			||||||
    return colls.map((x: any) => {
 | 
					 | 
				
			||||||
        return new TagTreeNode(`${id}.${database}.${x}`, x, NodeTypeColl).withIsLeaf(true).withParams({
 | 
					 | 
				
			||||||
            id,
 | 
					 | 
				
			||||||
            database,
 | 
					 | 
				
			||||||
            collection: x,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const NodeTypeColl = new NodeType(MongoNodeType.Coll).withNodeClickFunc((nodeData: TagTreeNode) => {
 | 
					 | 
				
			||||||
    const { id, database, collection } = nodeData.params;
 | 
					 | 
				
			||||||
    changeCollection(id, database, collection);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const findParamInputRef: any = ref(null);
 | 
					 | 
				
			||||||
const tagTreeRef: any = ref(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const state = reactive({
 | 
					 | 
				
			||||||
    defaultExpendKey: [] as any,
 | 
					 | 
				
			||||||
    tags: [],
 | 
					 | 
				
			||||||
    mongoList: [] as any,
 | 
					 | 
				
			||||||
    activeName: '', // 当前操作的tab
 | 
					 | 
				
			||||||
    dataTabs: {} as any, // 数据tabs
 | 
					 | 
				
			||||||
    findDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        findParam: {
 | 
					 | 
				
			||||||
            limit: 0,
 | 
					 | 
				
			||||||
            skip: 0,
 | 
					 | 
				
			||||||
            filter: '',
 | 
					 | 
				
			||||||
            sort: '',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    docEditDialog: {
 | 
					 | 
				
			||||||
        isAdd: true,
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        doc: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    insertDocDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        doc: '',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    jsonEditorDialog: {
 | 
					 | 
				
			||||||
        visible: false,
 | 
					 | 
				
			||||||
        doc: '',
 | 
					 | 
				
			||||||
        item: {} as any,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const { findDialog, docEditDialog } = toRefs(state);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const autoOpenResourceStore = useAutoOpenResource();
 | 
					 | 
				
			||||||
const { autoOpenResource } = storeToRefs(autoOpenResourceStore);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const nowColl = computed(() => {
 | 
					 | 
				
			||||||
    return getNowDataTab();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
    () => autoOpenResource.value.mongoCodePath,
 | 
					 | 
				
			||||||
    (codePath: any) => {
 | 
					 | 
				
			||||||
        autoOpenMongo(codePath);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					 | 
				
			||||||
    autoOpenMongo(autoOpenResource.value.mongoCodePath);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const autoOpenMongo = (codePath: string) => {
 | 
					 | 
				
			||||||
    if (!codePath) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const typeAndCodes = getTagTypeCodeByPath(codePath);
 | 
					 | 
				
			||||||
    const tagPath = typeAndCodes[TagResourceTypeEnum.Tag.value].join('/') + '/';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const mongoCode = typeAndCodes[TagResourceTypeEnum.Mongo.value][0];
 | 
					 | 
				
			||||||
    state.defaultExpendKey = [tagPath, mongoCode];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
        // 置空
 | 
					 | 
				
			||||||
        autoOpenResourceStore.setMongoCodePath('');
 | 
					 | 
				
			||||||
        tagTreeRef.value.setCurrentKey(mongoCode);
 | 
					 | 
				
			||||||
    }, 600);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const changeCollection = async (id: any, schema: string, collection: string) => {
 | 
					 | 
				
			||||||
    const label = `${id}:\`${schema}\`.${collection}`;
 | 
					 | 
				
			||||||
    let dataTab = state.dataTabs[label];
 | 
					 | 
				
			||||||
    if (!dataTab) {
 | 
					 | 
				
			||||||
        // 默认查询参数
 | 
					 | 
				
			||||||
        const findParam = {
 | 
					 | 
				
			||||||
            filter: '{}',
 | 
					 | 
				
			||||||
            sort: '{"_id": -1}',
 | 
					 | 
				
			||||||
            skip: 0,
 | 
					 | 
				
			||||||
            limit: 12,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        state.dataTabs[label] = {
 | 
					 | 
				
			||||||
            key: label,
 | 
					 | 
				
			||||||
            label: label,
 | 
					 | 
				
			||||||
            name: label,
 | 
					 | 
				
			||||||
            mongoId: id,
 | 
					 | 
				
			||||||
            database: schema,
 | 
					 | 
				
			||||||
            collection,
 | 
					 | 
				
			||||||
            datas: [],
 | 
					 | 
				
			||||||
            findParamStr: JSON.stringify(findParam),
 | 
					 | 
				
			||||||
            findParam,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    state.activeName = label;
 | 
					 | 
				
			||||||
    findCommand(label);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showFindDialog = (key: string) => {
 | 
					 | 
				
			||||||
    // 获取当前tab的索引位置,将其输入框失去焦点,防止输入以及重复获取焦点
 | 
					 | 
				
			||||||
    const dataTabNames = Object.keys(state.dataTabs);
 | 
					 | 
				
			||||||
    for (let i = 0; i < dataTabNames.length; i++) {
 | 
					 | 
				
			||||||
        if (key == dataTabNames[i]) {
 | 
					 | 
				
			||||||
            findParamInputRef.value[i].blur();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    state.findDialog.findParam = state.dataTabs[key].findParam;
 | 
					 | 
				
			||||||
    state.findDialog.visible = true;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const confirmFindDialog = () => {
 | 
					 | 
				
			||||||
    state.dataTabs[state.activeName].findParam = state.findDialog.findParam;
 | 
					 | 
				
			||||||
    state.dataTabs[state.activeName].findParamStr = JSON.stringify(state.findDialog.findParam);
 | 
					 | 
				
			||||||
    state.findDialog.visible = false;
 | 
					 | 
				
			||||||
    findCommand(state.activeName);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const findCommand = async (key: string) => {
 | 
					 | 
				
			||||||
    const dataTab = getNowDataTab();
 | 
					 | 
				
			||||||
    const findParma = dataTab.findParam;
 | 
					 | 
				
			||||||
    let filter, sort;
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
        filter = findParma.filter ? JSON.parse(findParma.filter) : {};
 | 
					 | 
				
			||||||
        sort = findParma.sort ? JSON.parse(findParma.sort) : {};
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
        ElMessage.error(t('mongo.findParamErrMsg'));
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const datas = await mongoApi.findCommand.request({
 | 
					 | 
				
			||||||
        id: dataTab.mongoId,
 | 
					 | 
				
			||||||
        database: dataTab.database,
 | 
					 | 
				
			||||||
        collection: dataTab.collection,
 | 
					 | 
				
			||||||
        filter,
 | 
					 | 
				
			||||||
        sort,
 | 
					 | 
				
			||||||
        limit: findParma.limit || 12,
 | 
					 | 
				
			||||||
        skip: findParma.skip || 0,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    state.dataTabs[key].datas = wrapDatas(datas);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 获取coll stats
 | 
					 | 
				
			||||||
    state.dataTabs[key].stats = await mongoApi.runCommand.request({
 | 
					 | 
				
			||||||
        id: dataTab.mongoId,
 | 
					 | 
				
			||||||
        database: dataTab.database,
 | 
					 | 
				
			||||||
        command: [
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                collStats: dataTab.collection,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 包装mongo查询回来的对象,即将其都转为json字符串并用value属性值描述,方便显示
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const wrapDatas = (datas: any) => {
 | 
					 | 
				
			||||||
    const wrapDatas = [] as any;
 | 
					 | 
				
			||||||
    if (!datas) {
 | 
					 | 
				
			||||||
        return wrapDatas;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    for (let data of datas) {
 | 
					 | 
				
			||||||
        wrapDatas.push({ value: JSON.stringify(data, null, 4) });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return wrapDatas;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const showEditDocDialog = () => {
 | 
					 | 
				
			||||||
    // tab数据中的第一个文档,因为该集合的文档都类似,故使用第一个文档赋值至需要新增的文档输入框,方便直接修改新增
 | 
					 | 
				
			||||||
    const datasFirstDoc = state.dataTabs[state.activeName].datas[0];
 | 
					 | 
				
			||||||
    let doc = '';
 | 
					 | 
				
			||||||
    if (datasFirstDoc) {
 | 
					 | 
				
			||||||
        // 移除_id字段,因为新增无需该字段
 | 
					 | 
				
			||||||
        const docObj = JSON.parse(datasFirstDoc.value);
 | 
					 | 
				
			||||||
        delete docObj['_id'];
 | 
					 | 
				
			||||||
        doc = JSON.stringify(docObj, null, 4);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    state.docEditDialog.doc = doc;
 | 
					 | 
				
			||||||
    state.docEditDialog.visible = true;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const onEditDoc = async (item: any) => {
 | 
					 | 
				
			||||||
    // 新增文档
 | 
					 | 
				
			||||||
    if (!item) {
 | 
					 | 
				
			||||||
        state.docEditDialog.isAdd = true;
 | 
					 | 
				
			||||||
        showEditDocDialog();
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // 编辑修改文档
 | 
					 | 
				
			||||||
    // state.docEditDialog.item = item;
 | 
					 | 
				
			||||||
    state.docEditDialog.isAdd = false;
 | 
					 | 
				
			||||||
    state.docEditDialog.doc = item.value;
 | 
					 | 
				
			||||||
    state.docEditDialog.visible = true;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const onSaveDoc = async () => {
 | 
					 | 
				
			||||||
    if (state.docEditDialog.isAdd) {
 | 
					 | 
				
			||||||
        let docObj;
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            docObj = JSON.parse(state.docEditDialog.doc);
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
            ElMessage.error(t('mongo.docErrMsg'));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const dataTab = getNowDataTab();
 | 
					 | 
				
			||||||
        const res = await mongoApi.insertCommand.request({
 | 
					 | 
				
			||||||
            id: dataTab.mongoId,
 | 
					 | 
				
			||||||
            database: dataTab.database,
 | 
					 | 
				
			||||||
            collection: dataTab.collection,
 | 
					 | 
				
			||||||
            doc: docObj,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        isTrue(res.InsertedID, 'mongo.insertFail');
 | 
					 | 
				
			||||||
        ElMessage.success(t('mongo.insertSuccess'));
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        const docObj = parseDocJsonString(state.docEditDialog.doc);
 | 
					 | 
				
			||||||
        const id = docObj._id;
 | 
					 | 
				
			||||||
        notBlank(id, t('mongo.idNotExist'));
 | 
					 | 
				
			||||||
        delete docObj['_id'];
 | 
					 | 
				
			||||||
        const dataTab = getNowDataTab();
 | 
					 | 
				
			||||||
        const res = await mongoApi.updateByIdCommand.request({
 | 
					 | 
				
			||||||
            id: dataTab.mongoId,
 | 
					 | 
				
			||||||
            database: dataTab.database,
 | 
					 | 
				
			||||||
            collection: dataTab.collection,
 | 
					 | 
				
			||||||
            docId: id,
 | 
					 | 
				
			||||||
            update: { $set: docObj },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        isTrue(res.ModifiedCount == 1, 'common.modifyFail');
 | 
					 | 
				
			||||||
        useI18nSaveSuccessMsg();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    findCommand(state.activeName);
 | 
					 | 
				
			||||||
    state.docEditDialog.visible = false;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const onDeleteDoc = async (doc: string) => {
 | 
					 | 
				
			||||||
    const docObj = parseDocJsonString(doc);
 | 
					 | 
				
			||||||
    const id = docObj._id;
 | 
					 | 
				
			||||||
    notBlank(id, t('mongo.idNotExist'));
 | 
					 | 
				
			||||||
    const dataTab = getNowDataTab();
 | 
					 | 
				
			||||||
    const res = await mongoApi.deleteByIdCommand.request({
 | 
					 | 
				
			||||||
        id: dataTab.mongoId,
 | 
					 | 
				
			||||||
        database: dataTab.database,
 | 
					 | 
				
			||||||
        collection: dataTab.collection,
 | 
					 | 
				
			||||||
        docId: id,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    isTrue(res.DeletedCount == 1, 'common.deleteFail');
 | 
					 | 
				
			||||||
    useI18nDeleteSuccessMsg();
 | 
					 | 
				
			||||||
    findCommand(state.activeName);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 将json字符串解析为json对象
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
const parseDocJsonString = (doc: string) => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
        return JSON.parse(doc);
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
        ElMessage.error(t('mongo.docParse2jsonFail'));
 | 
					 | 
				
			||||||
        throw e;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const removeDataTab = (targetName: string) => {
 | 
					 | 
				
			||||||
    const tabNames = Object.keys(state.dataTabs);
 | 
					 | 
				
			||||||
    let activeName = state.activeName;
 | 
					 | 
				
			||||||
    tabNames.forEach((name, index) => {
 | 
					 | 
				
			||||||
        if (name === targetName) {
 | 
					 | 
				
			||||||
            const nextTab = tabNames[index + 1] || tabNames[index - 1];
 | 
					 | 
				
			||||||
            if (nextTab) {
 | 
					 | 
				
			||||||
                activeName = nextTab;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    state.activeName = activeName;
 | 
					 | 
				
			||||||
    delete state.dataTabs[targetName];
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const getNowDataTab = () => {
 | 
					 | 
				
			||||||
    return state.dataTabs[state.activeName];
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="scss">
 | 
					 | 
				
			||||||
.mongo-doc-btns {
 | 
					 | 
				
			||||||
    position: absolute;
 | 
					 | 
				
			||||||
    z-index: 2;
 | 
					 | 
				
			||||||
    right: 3px;
 | 
					 | 
				
			||||||
    top: 2px;
 | 
					 | 
				
			||||||
    max-width: 120px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.mongo-data-tab {
 | 
					 | 
				
			||||||
    .mongo-data-tab-data {
 | 
					 | 
				
			||||||
        height: calc(100vh - 230px);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .el-tabs__header {
 | 
					 | 
				
			||||||
        margin: 0 0 5px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .el-tabs__item {
 | 
					 | 
				
			||||||
            padding: 0 5px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
							
								
								
									
										422
									
								
								frontend/src/views/ops/mongo/resource/MongoDataOp.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,422 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <div class="mongo-data-tab card h-full !p-1 w-full">
 | 
				
			||||||
 | 
					        <el-row v-if="nowColl">
 | 
				
			||||||
 | 
					            <el-descriptions class="!w-full" :column="10" size="small" border>
 | 
				
			||||||
 | 
					                <!-- <el-descriptions-item label-align="right" label="tag">xxx</el-descriptions-item> -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <el-descriptions-item label="ns" label-align="right">
 | 
				
			||||||
 | 
					                    {{ nowColl.stats?.ns }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item label="count" label-align="right">
 | 
				
			||||||
 | 
					                    {{ nowColl.stats?.count }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item label="avgObjSize" label-align="right">
 | 
				
			||||||
 | 
					                    {{ formatByteSize(nowColl.stats?.avgObjSize) }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item label="size" label-align="right">
 | 
				
			||||||
 | 
					                    {{ formatByteSize(nowColl.stats?.size) }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item label="totalSize" label-align="right">
 | 
				
			||||||
 | 
					                    {{ formatByteSize(nowColl.stats?.totalSize) }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item label="storageSize" label-align="right">
 | 
				
			||||||
 | 
					                    {{ formatByteSize(nowColl.stats?.storageSize) }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					                <el-descriptions-item label="freeStorageSize" label-align="right">
 | 
				
			||||||
 | 
					                    {{ formatByteSize(nowColl.stats?.freeStorageSize) }}
 | 
				
			||||||
 | 
					                </el-descriptions-item>
 | 
				
			||||||
 | 
					            </el-descriptions>
 | 
				
			||||||
 | 
					        </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-row type="flex">
 | 
				
			||||||
 | 
					            <el-tabs @tab-remove="removeDataTab" class="!w-full ml-1" v-model="state.activeName">
 | 
				
			||||||
 | 
					                <el-tab-pane closable v-for="dt in state.dataTabs" :key="dt.key" :label="dt.label" :name="dt.key">
 | 
				
			||||||
 | 
					                    <el-row>
 | 
				
			||||||
 | 
					                        <el-col :span="2">
 | 
				
			||||||
 | 
					                            <div class="mt-1">
 | 
				
			||||||
 | 
					                                <el-link @click="findCommand(state.activeName)" icon="refresh" underline="never" class=""> </el-link>
 | 
				
			||||||
 | 
					                                <el-divider direction="vertical" border-style="dashed" />
 | 
				
			||||||
 | 
					                                <el-link v-auth="perms.saveData" @click="onEditDoc(null)" type="primary" icon="plus" underline="never"> </el-link>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </el-col>
 | 
				
			||||||
 | 
					                        <el-col :span="22">
 | 
				
			||||||
 | 
					                            <el-input
 | 
				
			||||||
 | 
					                                ref="findParamInputRef"
 | 
				
			||||||
 | 
					                                v-model="dt.findParamStr"
 | 
				
			||||||
 | 
					                                :placeholder="$t('mongo.queryParamPlaceholder')"
 | 
				
			||||||
 | 
					                                @focus="showFindDialog(dt.key)"
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                                <template #prepend>{{ $t('mongo.queryParam') }}</template>
 | 
				
			||||||
 | 
					                            </el-input>
 | 
				
			||||||
 | 
					                        </el-col>
 | 
				
			||||||
 | 
					                    </el-row>
 | 
				
			||||||
 | 
					                    <el-scrollbar class="mongo-data-tab-data">
 | 
				
			||||||
 | 
					                        <el-row>
 | 
				
			||||||
 | 
					                            <el-col :span="6" v-for="item in dt.datas" :key="item">
 | 
				
			||||||
 | 
					                                <el-card :body-style="{ padding: '0px', position: 'relative' }">
 | 
				
			||||||
 | 
					                                    <el-input type="textarea" v-model="item.value" :rows="10" />
 | 
				
			||||||
 | 
					                                    <div style="padding: 3px; float: right" class="mr-1 mongo-doc-btns">
 | 
				
			||||||
 | 
					                                        <div>
 | 
				
			||||||
 | 
					                                            <el-link @click="onEditDoc(item)" underline="never" type="success" icon="MagicStick"></el-link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            <el-divider direction="vertical" border-style="dashed" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                            <el-popconfirm @confirm="onDeleteDoc(item.value)" :title="$t('mongo.deleteDocConfirm')" width="160">
 | 
				
			||||||
 | 
					                                                <template #reference>
 | 
				
			||||||
 | 
					                                                    <el-link v-auth="perms.delData" underline="never" type="danger" icon="DocumentDelete"> </el-link>
 | 
				
			||||||
 | 
					                                                </template>
 | 
				
			||||||
 | 
					                                            </el-popconfirm>
 | 
				
			||||||
 | 
					                                        </div>
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                </el-card>
 | 
				
			||||||
 | 
					                            </el-col>
 | 
				
			||||||
 | 
					                        </el-row>
 | 
				
			||||||
 | 
					                    </el-scrollbar>
 | 
				
			||||||
 | 
					                </el-tab-pane>
 | 
				
			||||||
 | 
					            </el-tabs>
 | 
				
			||||||
 | 
					        </el-row>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-dialog width="600px" title="find params" v-model="findDialog.visible">
 | 
				
			||||||
 | 
					            <el-form label-width="auto">
 | 
				
			||||||
 | 
					                <el-form-item label="filter">
 | 
				
			||||||
 | 
					                    <monaco-editor style="width: 100%" height="150px" ref="monacoEditorRef" v-model="findDialog.findParam.filter" language="json" />
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item label="sort">
 | 
				
			||||||
 | 
					                    <el-input v-model="findDialog.findParam.sort" type="textarea" :rows="3" clearable auto-complete="off"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item label="limit">
 | 
				
			||||||
 | 
					                    <el-input v-model.number="findDialog.findParam.limit" type="number" auto-complete="off"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					                <el-form-item label="skip">
 | 
				
			||||||
 | 
					                    <el-input v-model.number="findDialog.findParam.skip" type="number" auto-complete="off"></el-input>
 | 
				
			||||||
 | 
					                </el-form-item>
 | 
				
			||||||
 | 
					            </el-form>
 | 
				
			||||||
 | 
					            <template #footer>
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <el-button @click="findDialog.visible = false">{{ $t('common.cancel') }}</el-button>
 | 
				
			||||||
 | 
					                    <el-button @click="confirmFindDialog" type="primary">{{ $t('common.confirm') }}</el-button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <el-dialog
 | 
				
			||||||
 | 
					            width="60%"
 | 
				
			||||||
 | 
					            :title="`${state.docEditDialog.isAdd ? $t('common.add') : $t('common.edit')} '${state.activeName}' $t('mongo.doc')`"
 | 
				
			||||||
 | 
					            v-model="docEditDialog.visible"
 | 
				
			||||||
 | 
					            :close-on-click-modal="false"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <monaco-editor v-model="docEditDialog.doc" language="json" />
 | 
				
			||||||
 | 
					            <template #footer>
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <el-button @click="docEditDialog.visible = false">{{ $t('common.cancel') }}</el-button>
 | 
				
			||||||
 | 
					                    <el-button v-auth="perms.saveData" @click="onSaveDoc" type="primary">{{ $t('common.confirm') }}</el-button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </template>
 | 
				
			||||||
 | 
					        </el-dialog>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { computed, defineAsyncComponent, getCurrentInstance, inject, onMounted, reactive, ref, toRefs, watch } from 'vue';
 | 
				
			||||||
 | 
					import { ElMessage } from 'element-plus';
 | 
				
			||||||
 | 
					import { isTrue, notBlank } from '@/common/assert';
 | 
				
			||||||
 | 
					import { formatByteSize } from '@/common/utils/format';
 | 
				
			||||||
 | 
					import { useI18n } from 'vue-i18n';
 | 
				
			||||||
 | 
					import { useI18nDeleteSuccessMsg, useI18nSaveSuccessMsg } from '@/hooks/useI18n';
 | 
				
			||||||
 | 
					import { mongoApi } from '@/views/ops/mongo/api';
 | 
				
			||||||
 | 
					import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
 | 
				
			||||||
 | 
					import { MongoOpComp } from '@/views/ops/mongo/resource';
 | 
				
			||||||
 | 
					import { ResourceOpCtx } from '../../component/tag';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MonacoEditor = defineAsyncComponent(() => import('@/components/monaco/MonacoEditor.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { t } = useI18n();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const perms = {
 | 
				
			||||||
 | 
					    saveData: 'mongo:data:save',
 | 
				
			||||||
 | 
					    delData: 'mongo:data:del',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emits = defineEmits(['init']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const findParamInputRef: any = ref(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const state = reactive({
 | 
				
			||||||
 | 
					    defaultExpendKey: [] as any,
 | 
				
			||||||
 | 
					    tags: [],
 | 
				
			||||||
 | 
					    mongoList: [] as any,
 | 
				
			||||||
 | 
					    activeName: '', // 当前操作的tab
 | 
				
			||||||
 | 
					    dataTabs: {} as any, // 数据tabs
 | 
				
			||||||
 | 
					    findDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        findParam: {
 | 
				
			||||||
 | 
					            limit: 0,
 | 
				
			||||||
 | 
					            skip: 0,
 | 
				
			||||||
 | 
					            filter: '',
 | 
				
			||||||
 | 
					            sort: '',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    docEditDialog: {
 | 
				
			||||||
 | 
					        isAdd: true,
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        doc: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    insertDocDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        doc: '',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    jsonEditorDialog: {
 | 
				
			||||||
 | 
					        visible: false,
 | 
				
			||||||
 | 
					        doc: '',
 | 
				
			||||||
 | 
					        item: {} as any,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { findDialog, docEditDialog } = toRefs(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const nowColl = computed(() => {
 | 
				
			||||||
 | 
					    return getNowDataTab();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					    emits('init', { name: MongoOpComp.name, ref: getCurrentInstance()?.exposed });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const changeCollection = async (id: any, schema: string, collection: string) => {
 | 
				
			||||||
 | 
					    const label = `${id}:\`${schema}\`.${collection}`;
 | 
				
			||||||
 | 
					    let dataTab = state.dataTabs[label];
 | 
				
			||||||
 | 
					    if (!dataTab) {
 | 
				
			||||||
 | 
					        // 默认查询参数
 | 
				
			||||||
 | 
					        const findParam = {
 | 
				
			||||||
 | 
					            filter: '{}',
 | 
				
			||||||
 | 
					            sort: '{"_id": -1}',
 | 
				
			||||||
 | 
					            skip: 0,
 | 
				
			||||||
 | 
					            limit: 12,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        state.dataTabs[label] = {
 | 
				
			||||||
 | 
					            key: label,
 | 
				
			||||||
 | 
					            label: label,
 | 
				
			||||||
 | 
					            name: label,
 | 
				
			||||||
 | 
					            mongoId: id,
 | 
				
			||||||
 | 
					            database: schema,
 | 
				
			||||||
 | 
					            collection,
 | 
				
			||||||
 | 
					            datas: [],
 | 
				
			||||||
 | 
					            findParamStr: JSON.stringify(findParam),
 | 
				
			||||||
 | 
					            findParam,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    state.activeName = label;
 | 
				
			||||||
 | 
					    findCommand(label);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showFindDialog = (key: string) => {
 | 
				
			||||||
 | 
					    // 获取当前tab的索引位置,将其输入框失去焦点,防止输入以及重复获取焦点
 | 
				
			||||||
 | 
					    const dataTabNames = Object.keys(state.dataTabs);
 | 
				
			||||||
 | 
					    for (let i = 0; i < dataTabNames.length; i++) {
 | 
				
			||||||
 | 
					        if (key == dataTabNames[i]) {
 | 
				
			||||||
 | 
					            findParamInputRef.value[i].blur();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    state.findDialog.findParam = state.dataTabs[key].findParam;
 | 
				
			||||||
 | 
					    state.findDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const confirmFindDialog = () => {
 | 
				
			||||||
 | 
					    state.dataTabs[state.activeName].findParam = state.findDialog.findParam;
 | 
				
			||||||
 | 
					    state.dataTabs[state.activeName].findParamStr = JSON.stringify(state.findDialog.findParam);
 | 
				
			||||||
 | 
					    state.findDialog.visible = false;
 | 
				
			||||||
 | 
					    findCommand(state.activeName);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const findCommand = async (key: string) => {
 | 
				
			||||||
 | 
					    const dataTab = getNowDataTab();
 | 
				
			||||||
 | 
					    const findParma = dataTab.findParam;
 | 
				
			||||||
 | 
					    let filter, sort;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        filter = findParma.filter ? JSON.parse(findParma.filter) : {};
 | 
				
			||||||
 | 
					        sort = findParma.sort ? JSON.parse(findParma.sort) : {};
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					        ElMessage.error(t('mongo.findParamErrMsg'));
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const datas = await mongoApi.findCommand.request({
 | 
				
			||||||
 | 
					        id: dataTab.mongoId,
 | 
				
			||||||
 | 
					        database: dataTab.database,
 | 
				
			||||||
 | 
					        collection: dataTab.collection,
 | 
				
			||||||
 | 
					        filter,
 | 
				
			||||||
 | 
					        sort,
 | 
				
			||||||
 | 
					        limit: findParma.limit || 12,
 | 
				
			||||||
 | 
					        skip: findParma.skip || 0,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    state.dataTabs[key].datas = wrapDatas(datas);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 获取coll stats
 | 
				
			||||||
 | 
					    state.dataTabs[key].stats = await mongoApi.runCommand.request({
 | 
				
			||||||
 | 
					        id: dataTab.mongoId,
 | 
				
			||||||
 | 
					        database: dataTab.database,
 | 
				
			||||||
 | 
					        command: [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                collStats: dataTab.collection,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 包装mongo查询回来的对象,即将其都转为json字符串并用value属性值描述,方便显示
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const wrapDatas = (datas: any) => {
 | 
				
			||||||
 | 
					    const wrapDatas = [] as any;
 | 
				
			||||||
 | 
					    if (!datas) {
 | 
				
			||||||
 | 
					        return wrapDatas;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (let data of datas) {
 | 
				
			||||||
 | 
					        wrapDatas.push({ value: JSON.stringify(data, null, 4) });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return wrapDatas;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const showEditDocDialog = () => {
 | 
				
			||||||
 | 
					    // tab数据中的第一个文档,因为该集合的文档都类似,故使用第一个文档赋值至需要新增的文档输入框,方便直接修改新增
 | 
				
			||||||
 | 
					    const datasFirstDoc = state.dataTabs[state.activeName].datas[0];
 | 
				
			||||||
 | 
					    let doc = '';
 | 
				
			||||||
 | 
					    if (datasFirstDoc) {
 | 
				
			||||||
 | 
					        // 移除_id字段,因为新增无需该字段
 | 
				
			||||||
 | 
					        const docObj = JSON.parse(datasFirstDoc.value);
 | 
				
			||||||
 | 
					        delete docObj['_id'];
 | 
				
			||||||
 | 
					        doc = JSON.stringify(docObj, null, 4);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    state.docEditDialog.doc = doc;
 | 
				
			||||||
 | 
					    state.docEditDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onEditDoc = async (item: any) => {
 | 
				
			||||||
 | 
					    // 新增文档
 | 
				
			||||||
 | 
					    if (!item) {
 | 
				
			||||||
 | 
					        state.docEditDialog.isAdd = true;
 | 
				
			||||||
 | 
					        showEditDocDialog();
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // 编辑修改文档
 | 
				
			||||||
 | 
					    // state.docEditDialog.item = item;
 | 
				
			||||||
 | 
					    state.docEditDialog.isAdd = false;
 | 
				
			||||||
 | 
					    state.docEditDialog.doc = item.value;
 | 
				
			||||||
 | 
					    state.docEditDialog.visible = true;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onSaveDoc = async () => {
 | 
				
			||||||
 | 
					    if (state.docEditDialog.isAdd) {
 | 
				
			||||||
 | 
					        let docObj;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            docObj = JSON.parse(state.docEditDialog.doc);
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            ElMessage.error(t('mongo.docErrMsg'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const dataTab = getNowDataTab();
 | 
				
			||||||
 | 
					        const res = await mongoApi.insertCommand.request({
 | 
				
			||||||
 | 
					            id: dataTab.mongoId,
 | 
				
			||||||
 | 
					            database: dataTab.database,
 | 
				
			||||||
 | 
					            collection: dataTab.collection,
 | 
				
			||||||
 | 
					            doc: docObj,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        isTrue(res.InsertedID, 'mongo.insertFail');
 | 
				
			||||||
 | 
					        ElMessage.success(t('mongo.insertSuccess'));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const docObj = parseDocJsonString(state.docEditDialog.doc);
 | 
				
			||||||
 | 
					        const id = docObj._id;
 | 
				
			||||||
 | 
					        notBlank(id, t('mongo.idNotExist'));
 | 
				
			||||||
 | 
					        delete docObj['_id'];
 | 
				
			||||||
 | 
					        const dataTab = getNowDataTab();
 | 
				
			||||||
 | 
					        const res = await mongoApi.updateByIdCommand.request({
 | 
				
			||||||
 | 
					            id: dataTab.mongoId,
 | 
				
			||||||
 | 
					            database: dataTab.database,
 | 
				
			||||||
 | 
					            collection: dataTab.collection,
 | 
				
			||||||
 | 
					            docId: id,
 | 
				
			||||||
 | 
					            update: { $set: docObj },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        isTrue(res.ModifiedCount == 1, 'common.modifyFail');
 | 
				
			||||||
 | 
					        useI18nSaveSuccessMsg();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    findCommand(state.activeName);
 | 
				
			||||||
 | 
					    state.docEditDialog.visible = false;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onDeleteDoc = async (doc: string) => {
 | 
				
			||||||
 | 
					    const docObj = parseDocJsonString(doc);
 | 
				
			||||||
 | 
					    const id = docObj._id;
 | 
				
			||||||
 | 
					    notBlank(id, t('mongo.idNotExist'));
 | 
				
			||||||
 | 
					    const dataTab = getNowDataTab();
 | 
				
			||||||
 | 
					    const res = await mongoApi.deleteByIdCommand.request({
 | 
				
			||||||
 | 
					        id: dataTab.mongoId,
 | 
				
			||||||
 | 
					        database: dataTab.database,
 | 
				
			||||||
 | 
					        collection: dataTab.collection,
 | 
				
			||||||
 | 
					        docId: id,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    isTrue(res.DeletedCount == 1, 'common.deleteFail');
 | 
				
			||||||
 | 
					    useI18nDeleteSuccessMsg();
 | 
				
			||||||
 | 
					    findCommand(state.activeName);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 将json字符串解析为json对象
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					const parseDocJsonString = (doc: string) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        return JSON.parse(doc);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					        ElMessage.error(t('mongo.docParse2jsonFail'));
 | 
				
			||||||
 | 
					        throw e;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const removeDataTab = (targetName: string) => {
 | 
				
			||||||
 | 
					    const tabNames = Object.keys(state.dataTabs);
 | 
				
			||||||
 | 
					    let activeName = state.activeName;
 | 
				
			||||||
 | 
					    tabNames.forEach((name, index) => {
 | 
				
			||||||
 | 
					        if (name === targetName) {
 | 
				
			||||||
 | 
					            const nextTab = tabNames[index + 1] || tabNames[index - 1];
 | 
				
			||||||
 | 
					            if (nextTab) {
 | 
				
			||||||
 | 
					                activeName = nextTab;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    state.activeName = activeName;
 | 
				
			||||||
 | 
					    delete state.dataTabs[targetName];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getNowDataTab = () => {
 | 
				
			||||||
 | 
					    return state.dataTabs[state.activeName];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
					    changeCollection,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					.mongo-doc-btns {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    z-index: 2;
 | 
				
			||||||
 | 
					    right: 3px;
 | 
				
			||||||
 | 
					    top: 2px;
 | 
				
			||||||
 | 
					    max-width: 120px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mongo-data-tab {
 | 
				
			||||||
 | 
					    .mongo-data-tab-data {
 | 
				
			||||||
 | 
					        height: calc(100vh - 230px);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .el-tabs__header {
 | 
				
			||||||
 | 
					        margin: 0 0 5px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .el-tabs__item {
 | 
				
			||||||
 | 
					            padding: 0 5px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										28
									
								
								frontend/src/views/ops/mongo/resource/NodeMongo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <BaseTreeNode v-bind="$attrs">
 | 
				
			||||||
 | 
					        <template #prefix="{ data }">
 | 
				
			||||||
 | 
					            <el-popover :show-after="500" placement="right-start" :title="$t('common.detail')" trigger="hover" :width="250">
 | 
				
			||||||
 | 
					                <template #reference>
 | 
				
			||||||
 | 
					                    <SvgIcon :name="ResourceTypeEnum.Mongo.extra.icon" :color="ResourceTypeEnum.Mongo.extra.iconColor" :size="13" />
 | 
				
			||||||
 | 
					                </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="url">
 | 
				
			||||||
 | 
					                            {{ data.params.uri }}
 | 
				
			||||||
 | 
					                        </el-descriptions-item>
 | 
				
			||||||
 | 
					                    </el-descriptions>
 | 
				
			||||||
 | 
					                </template>
 | 
				
			||||||
 | 
					            </el-popover>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					    </BaseTreeNode>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { ResourceTypeEnum } from '@/common/commonEnum';
 | 
				
			||||||
 | 
					import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss"></style>
 | 
				
			||||||
							
								
								
									
										14
									
								
								frontend/src/views/ops/mongo/resource/NodeMongoDb.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					    <BaseTreeNode v-bind="$attrs">
 | 
				
			||||||
 | 
					        <template #suffix="{ data }">
 | 
				
			||||||
 | 
					            <span>{{ formatByteSize(data.params.size) }}</span>
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
 | 
					    </BaseTreeNode>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					import { formatByteSize } from '@/common/utils/format';
 | 
				
			||||||
 | 
					import BaseTreeNode from '@/views/ops/resource/BaseTreeNode.vue';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss"></style>
 | 
				
			||||||
							
								
								
									
										103
									
								
								frontend/src/views/ops/mongo/resource/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					import { defineAsyncComponent } from 'vue';
 | 
				
			||||||
 | 
					import { NodeType, TagTreeNode, ResourceComponentConfig, ResourceConfig } from '../../component/tag';
 | 
				
			||||||
 | 
					import { ResourceTypeEnum, TagResourceTypeEnum } from '@/common/commonEnum';
 | 
				
			||||||
 | 
					import { sleep } from '@/common/utils/loading';
 | 
				
			||||||
 | 
					import { mongoApi } from '../api';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Icon = {
 | 
				
			||||||
 | 
					    name: ResourceTypeEnum.Mongo.extra.icon,
 | 
				
			||||||
 | 
					    color: ResourceTypeEnum.Mongo.extra.iconColor,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const MongoList = defineAsyncComponent(() => import('../MongoList.vue'));
 | 
				
			||||||
 | 
					const MongoDataOp = defineAsyncComponent(() => import('./MongoDataOp.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeMongo = defineAsyncComponent(() => import('./NodeMongo.vue'));
 | 
				
			||||||
 | 
					const NodeMongoDb = defineAsyncComponent(() => import('./NodeMongoDb.vue'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MongoOpComp: ResourceComponentConfig = {
 | 
				
			||||||
 | 
					    name: 'tag.mongoDataOp',
 | 
				
			||||||
 | 
					    component: MongoDataOp,
 | 
				
			||||||
 | 
					    icon: Icon,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// tagpath 节点类型
 | 
				
			||||||
 | 
					const NodeTypeMongoTag = new NodeType(TagTreeNode.TagPath).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
				
			||||||
 | 
					    parentNode.ctx?.addResourceComponent(MongoOpComp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const res = await mongoApi.mongoList.request({ tagPath: parentNode.params.tagPath });
 | 
				
			||||||
 | 
					    if (!res.total) {
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mongoInfos = res.list;
 | 
				
			||||||
 | 
					    await sleep(100);
 | 
				
			||||||
 | 
					    return mongoInfos?.map((x: any) => {
 | 
				
			||||||
 | 
					        x.tagPath = parentNode.key;
 | 
				
			||||||
 | 
					        return TagTreeNode.new(parentNode, `${x.code}`, x.name, NodeTypeMongo).withParams(x).withNodeComponent(NodeMongo);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeTypeMongo = new NodeType(1).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
				
			||||||
 | 
					    const inst = parentNode.params;
 | 
				
			||||||
 | 
					    // 点击mongo -> 加载mongo数据库列表
 | 
				
			||||||
 | 
					    const res = await mongoApi.databases.request({ id: inst.id });
 | 
				
			||||||
 | 
					    return res.Databases.map((x: any) => {
 | 
				
			||||||
 | 
					        const database = x.Name;
 | 
				
			||||||
 | 
					        return TagTreeNode.new(parentNode, `${inst.id}.${database}`, database, NodeTypeDbs)
 | 
				
			||||||
 | 
					            .withParams({
 | 
				
			||||||
 | 
					                id: inst.id,
 | 
				
			||||||
 | 
					                database,
 | 
				
			||||||
 | 
					                size: x.SizeOnDisk,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .withIcon({ name: 'Coin', color: '#67c23a' })
 | 
				
			||||||
 | 
					            .withNodeComponent(NodeMongoDb);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeTypeDbs = new NodeType(2).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
				
			||||||
 | 
					    const params = parentNode.params;
 | 
				
			||||||
 | 
					    // 点击数据库列表 -> 加载数据库下拥有的菜单列表
 | 
				
			||||||
 | 
					    return [
 | 
				
			||||||
 | 
					        TagTreeNode.new(parentNode, `${params.id}.${params.database}.mongo-coll`, 'mongo.coll', NodeTypeCollMenu)
 | 
				
			||||||
 | 
					            .withIcon({ name: 'Document' })
 | 
				
			||||||
 | 
					            .withParams(params),
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeTypeCollMenu = new NodeType(3).withLoadNodesFunc(async (parentNode: TagTreeNode) => {
 | 
				
			||||||
 | 
					    const { id, database } = parentNode.params;
 | 
				
			||||||
 | 
					    // 点击数据库集合节点 -> 加载集合列表
 | 
				
			||||||
 | 
					    const colls = await mongoApi.collections.request({ id, database });
 | 
				
			||||||
 | 
					    return colls.map((x: any) => {
 | 
				
			||||||
 | 
					        return TagTreeNode.new(parentNode, `${id}.${database}.${x}`, x, NodeTypeColl)
 | 
				
			||||||
 | 
					            .withIsLeaf(true)
 | 
				
			||||||
 | 
					            .withParams({
 | 
				
			||||||
 | 
					                id,
 | 
				
			||||||
 | 
					                database,
 | 
				
			||||||
 | 
					                collection: x,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .withIcon({ name: 'Document' });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const NodeTypeColl = new NodeType(4).withNodeClickFunc(async (nodeData: TagTreeNode) => {
 | 
				
			||||||
 | 
					    const compRef = await nodeData.ctx?.addResourceComponent(MongoOpComp);
 | 
				
			||||||
 | 
					    const { id, database, collection } = nodeData.params;
 | 
				
			||||||
 | 
					    compRef.changeCollection(id, database, collection);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					    order: 4,
 | 
				
			||||||
 | 
					    resourceType: TagResourceTypeEnum.Mongo.value,
 | 
				
			||||||
 | 
					    rootNodeType: NodeTypeMongoTag,
 | 
				
			||||||
 | 
					    manager: {
 | 
				
			||||||
 | 
					        componentConf: {
 | 
				
			||||||
 | 
					            component: MongoList,
 | 
				
			||||||
 | 
					            icon: Icon,
 | 
				
			||||||
 | 
					            name: 'mongo',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        countKey: 'mongo',
 | 
				
			||||||
 | 
					        permCode: 'mongo:manage:base',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					} as ResourceConfig;
 | 
				
			||||||