GitLab Viewer Publish and Deploy Project

GitLab Viewer Publish and Deploy Project!

当前为 2023-05-25 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name GitLab Viewer Publish and Deploy Project
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.04
  5. // @description GitLab Viewer Publish and Deploy Project!
  6. // @author Sean
  7. // @match http://192.168.0.200/fe3project/*
  8. // @icon http://192.168.0.200/assets/favicon-7901bd695fb93edb07975966062049829afb56cf11511236e61bcf425070e36e.png
  9. // @require https://cdn.bootcdn.net/ajax/libs/vue/2.7.14/vue.min.js
  10. // @require https://unpkg.com/element-ui/lib/index.js
  11. // @require https://cdn.bootcdn.net/ajax/libs/jszip/3.7.1/jszip.min.js
  12. // @resource myFontFile https://element.eleme.io/2.11/static/element-icons.535877f.woff
  13. // @grant GM_getResourceURL
  14. // @grant GM_addStyle
  15. // @grant GM_getResourceText
  16. // @resource ElementCSS https://unpkg.com/element-ui/lib/theme-chalk/index.css
  17. // @grant GM_xmlhttpRequest
  18. // @license MIT
  19. // @run-at document-end
  20. // ==/UserScript==
  21.  
  22. (function() {
  23. 'use strict';
  24.  
  25. const fontUrl = 'https://element.eleme.io/2.11/static/element-icons.535877f.woff';
  26.  
  27. // 添加样式规则,将字体应用到指定元素上
  28. GM_addStyle(`
  29. @font-face {
  30. font-family: element-icons;
  31. src: url(${fontUrl}) format("woff");
  32. }
  33. `);
  34.  
  35. GM_addStyle(GM_getResourceText('ElementCSS'));
  36.  
  37. let epointCss = ".epoint-tool {position: fixed; bottom: 0%; right:10px; transform: translateY(-50%);}";
  38. epointCss += ".el-row { padding: 3px 0;} .el-dialog__body .el-tree{min-height: 420px; max-height: 500px;overflow: auto;}";
  39. epointCss += ".deploy-body {height: 306px;}"
  40. epointCss += ".deploy-body .el-loading-spinner {margin-top: -50px;}";
  41. // 添加注入样式
  42. let extraStyleElement = document.createElement("style");
  43. extraStyleElement.innerHTML = epointCss;
  44. document.head.appendChild(extraStyleElement);
  45.  
  46. const MyComponent = {
  47. template: `<div class="epoint-wrap">
  48. <div class="epoint-tool">
  49. <el-row><el-button type="primary" icon="el-icon-search" round @click="viewProject">查看</el-button></el-row>
  50. <el-row>
  51. <el-tooltip content="一键部署到170服务器" placement="top" effect="light">
  52. <el-button type="primary" icon="el-icon-s-unfold" round @click="doDeploy">部署</el-button>
  53. </el-tooltip>
  54. </el-row>
  55. <el-row>
  56. <el-tooltip content="一键发布到项目案例库" placement="top" effect="light">
  57. <el-button type="primary" icon="el-icon-upload" round @click="publish">发布</el-button>
  58. </el-tooltip>
  59. </el-row>
  60. </div>
  61. <el-dialog
  62. title="目录结构"
  63. width="900px"
  64. :append-to-body="true"
  65. :visible.sync="dialogVisible"
  66. :before-close="handleClose">
  67. <el-tree
  68. class="filter-tree"
  69. :data="data"
  70. :props="defaultProps"
  71. node-key="id"
  72. default-expand-all
  73. @node-click="handleNodeClick"
  74. v-loading="loadingTree"
  75. ref="tree">
  76. </el-tree>
  77. </el-dialog>
  78. <el-dialog
  79. title="部署"
  80. width="420px"
  81. :visible.sync="depDialogVisible">
  82. <div class="deploy-body"
  83. v-loading="loading"
  84. element-loading-text="正在打包部署至 170 服务器,请耐心等待"
  85. element-loading-spinner="el-icon-loading">
  86. </div>
  87. </el-dialog>
  88. </div>`,
  89. data() {
  90. return {
  91. dialogVisible: false, // 查看目录结构弹窗
  92. data: [], // 目录结构树的数据结构
  93. defaultProps: {
  94. children: 'children',
  95. label: 'label'
  96. },
  97. loadingTree: false,
  98. depDialogVisible: false, // 部署中弹窗
  99. loading: false,
  100. projectLibUrl: 'http://192.168.201.159:9999/webapp/pages/default/onlinecase.html', // 项目案例库地址
  101. projectIsDeployed: false, // 项目是否部署过
  102. projectFtpUrl: '', // ftp路径
  103. projectEntryUrl: 'http://192.168.219.170/showcase/', // 项目案例库发布表单的入口页面
  104. supportDeploy: true, // 项目是否支持部署
  105. };
  106. },
  107. methods: {
  108. handleClose(done) {
  109. done();
  110. /*
  111. this.$confirm('确认关闭?')
  112. .then(_ => {
  113. done();
  114. })
  115. .catch(_ => {});*/
  116. },
  117. handleNodeClick(data) {
  118. console.log(data);
  119. if(data.type === 'folder') {
  120. return false;
  121. }
  122. var self = this;
  123. var entry = data.entry;
  124. if(self.projectFtpUrl) {
  125. window.open('http://192.168.219.170' + self.projectFtpUrl + data.entry)
  126. } else {
  127. if(this.supportDeploy === false) {
  128. this.$message({
  129. type: 'error',
  130. message: '暂只支持查看 重构模板 和 F9x2.0 的项目'
  131. });
  132.  
  133. return false;
  134. }
  135. this.$confirm('资源未部署,部署至 170 服务器后可查看,是否部署?')
  136. .then(_ => {
  137. this.depDialogVisible = true;
  138. this.loading = true;
  139. // 部署
  140. this.getDeployInfo({ type: '1' }, (data)=> {
  141. this.loading = false;
  142. this.depDialogVisible = false;
  143.  
  144. this.data = data.custom.detail;
  145. this.projectFtpUrl = data.custom.ftpUrl;
  146.  
  147. this.$alert('部署成功!', '提示', {
  148. confirmButtonText: '确定',
  149. callback: action => {
  150. window.open('http://192.168.219.170' + self.projectFtpUrl + entry)
  151. }
  152. });
  153.  
  154. });
  155. })
  156. .catch(_ => {});
  157. }
  158. },
  159. // 部署
  160. doDeploy () {
  161. this.$confirm('此操作将把 GitLab 资源打包部署至 170 服务器,已部署过的项目会进行覆盖部署,是否继续?', '提示', {
  162. confirmButtonText: '确定',
  163. cancelButtonText: '取消',
  164. type: 'warning'
  165. }).then(() => {
  166. this.depDialogVisible = true;
  167. this.loading = true;
  168. // 部署
  169. this.getDeployInfo({ type: '1' }, (data)=> {
  170. this.loading = false;
  171. this.depDialogVisible = false;
  172.  
  173. if(!data.custom.text){
  174. this.$message({
  175. type: 'success',
  176. message: '部署成功!'
  177. });
  178.  
  179. this.data = data.custom.detail;
  180. this.projectFtpUrl = data.custom.ftpUrl;
  181. this.supportDeploy = data.custom.supportDeploy;
  182. // 打开查看弹窗
  183. this.viewProject();
  184.  
  185. } else {
  186. this.$message({
  187. type: 'error',
  188. message: data.custom.text
  189. });
  190. }
  191. });
  192. }).catch(() => {
  193. this.$message({
  194. type: 'info',
  195. message: '已取消部署'
  196. });
  197. });
  198. },
  199. // 发布项目案例库
  200. publish () {
  201. this.$confirm('此操作将把项目发布至 <a href="'+ this.projectLibUrl +'" target="_blank">项目案例库</a>,已发布过的项目案例库会有重复项,是否继续?', '提示', {
  202. confirmButtonText: '确定',
  203. cancelButtonText: '取消',
  204. dangerouslyUseHTMLString: true,
  205. type: 'warning'
  206. }).then(() => {
  207. const themes = document.querySelectorAll('.badge-secondary');
  208. let keys = [];
  209.  
  210. for(let i = 0, l = themes.length; i < l; i++) {
  211. if(themes[0].innerText == '智能设备' && i > 0) {
  212. keys.push(themes[i].innerText);
  213. } else if (themes[0].innerText !== '智能设备' && i > 1) {
  214. keys.push(themes[i].innerText);
  215. }
  216. }
  217. // 存在更多主题的情况
  218. const moreKeyEl = document.querySelector('.gl-w-full .text-nowrap');
  219. if(moreKeyEl) {
  220. const moreKeyElContent = moreKeyEl.getAttribute('data-content');
  221. const regex = />([^<]+)</g;
  222. const matches = moreKeyElContent.match(regex);
  223. const results = matches.filter(function(match) {
  224. return match.length > 3;
  225. });
  226. const moreKeyData = results.map(function(match) {
  227. return match.substring(2, match.length - 2);
  228. });
  229.  
  230. if(moreKeyData && moreKeyData.length) {
  231. keys = keys.concat(moreKeyData);
  232. }
  233. }
  234.  
  235. // 组织项目案例库所需参数
  236. const projectName = document.querySelector('.home-panel-title').innerText;
  237. const projectBU = themes[0] ? themes[0].innerText : null;
  238. const projectKeys = keys.join(' ');
  239. const entryUrl = this.projectEntryUrl;
  240. let projectType = themes[1] ? themes[1].innerText : null;
  241.  
  242. if(themes[0]) {
  243. if(themes[0].innerText == '智能设备') {
  244. projectType = themes[1] ? themes[0].innerText : null;
  245. } else {
  246. projectType = themes[1] ? themes[1].innerText : null;
  247. }
  248. }
  249.  
  250. const destUrl = this.projectLibUrl + '?projectName=' + projectName + '&projectBU=' + projectBU + '&projectType=' + projectType + '&projectKeys=' + projectKeys + '&entryUrl=' + entryUrl + '&git=' + window.location.href;
  251.  
  252. this.$message({
  253. type: 'success',
  254. message: destUrl
  255. });
  256.  
  257. window.open(destUrl);
  258.  
  259. }).catch(() => {
  260. this.$message({
  261. type: 'info',
  262. message: '已取消发布'
  263. });
  264. });
  265. },
  266. // 查看项目
  267. viewProject () {
  268. this.dialogVisible = true;
  269. this.loadingTree = true;
  270. let self = this;
  271.  
  272. // 发送ajax请求,查看是否进行过部署
  273. this.getDeployInfo((data)=> {
  274. // 有部署信息,直接赋值,
  275. if(true) {
  276. self.projectIsDeployed = true;
  277. self.data = data.custom.detail;
  278. self.projectFtpUrl = data.custom.ftpUrl;
  279. self.supportDeploy = data.custom.supportDeploy;
  280. self.loadingTree = false;
  281.  
  282.  
  283. } else {
  284. // 无部署信息,仅查看文件目录
  285. getZipResource((data)=> {
  286. self.data = data;
  287. self.loadingTree = false;
  288. });
  289. }
  290. });
  291.  
  292. },
  293. // 项目部署信息
  294. getDeployInfo(params, callback) {
  295. const projectId = document.body.getAttribute('data-project-id');
  296. const downloadUrl = window.location.origin + document.querySelector('.gl-button.btn-sm.btn-confirm').getAttribute('href');
  297. const files = document.body.getAttribute('data-find-file').split('/');
  298. const name = document.body.getAttribute('data-project') + '-' + files[files.length - 1];
  299. const author = document.querySelector('.current-user .gl-font-weight-bold').innerText.trim();
  300. const projectName = document.querySelectorAll('.js-breadcrumbs-list li')[1].innerText.trim();
  301.  
  302. if(typeof params == 'function') {
  303. callback = params;
  304. params = null;
  305. }
  306.  
  307. if(projectId && projectId.length && downloadUrl) {
  308. fetch('http://192.168.219.170:3008/api/getDeployInfo', {
  309. method: 'POST',
  310. // 允许跨域请求
  311. mode: 'cors',
  312. headers: {
  313. 'Accept': 'application/json',
  314. 'Content-Type': 'application/json'
  315. },
  316. body: JSON.stringify({//post请求参数
  317. projectId: projectId,
  318. downloadUrl: downloadUrl,
  319. type: (params && params.type !== undefined) ? params.type : '0',// 0 代表查看, 1代表部署
  320. name: name,
  321. author: author, // 增加上传人,用来生成根目录下的readme.html
  322. projectName: projectName, // 项目名称
  323. })
  324. })
  325. .then(response => response.text())
  326. .then((result) => {
  327. var data = JSON.parse(result);
  328. callback && callback(data);
  329. })
  330. .catch(error => console.error(error));
  331. } else {
  332. console.error('部署信息请求参数error');
  333. }
  334. },
  335. // 设置入口
  336. setProjectEntry(){
  337. const firstNode = findFirstFileNode(this.data);
  338. if(firstNode) {
  339. this.projectEntryUrl = 'http://192.168.219.170' + this.projectFtpUrl + firstNode.entry;
  340. }
  341. }
  342. },
  343. mounted() {
  344.  
  345. }
  346. };
  347.  
  348. const placeholder = document.createElement('div');
  349.  
  350. // 创建 Vue 实例并挂载到页面
  351. const vueInstance = new Vue({
  352. el: placeholder,
  353. components: {
  354. MyComponent
  355. },
  356. methods: {
  357. },
  358. template: `<my-component></my-component>`
  359. });
  360.  
  361. // 等待页面加载完成
  362. window.addEventListener('load', function() {
  363. // 将占位元素追加到 body 元素中
  364. document.body.appendChild(vueInstance.$el);
  365. });
  366.  
  367. // 将文件条目组织成嵌套结构
  368. function organizeFileEntries(fileEntries) {
  369. const root = {
  370. label: document.querySelector('.home-panel-title').innerText || document.getElementById('project_name_edit').value,
  371. type: 'folder',
  372. children: []
  373. };
  374.  
  375. // 创建嵌套结构
  376. fileEntries.forEach(entry => {
  377. const pathSegments = entry.name.split('/');
  378. let currentFolder = root;
  379.  
  380. // 遍历路径中的每个部分,创建相应的文件夹节点
  381. for (let i = 0; i < pathSegments.length - 1; i++) {
  382. const folderName = pathSegments[i];
  383. let folder = currentFolder.children.find(child => child.label === folderName);
  384.  
  385. if(isExcludeFolder(entry.name)) {
  386. continue;
  387. }
  388.  
  389. if (!folder) {
  390. folder = {
  391. label: folderName,
  392. type: 'folder',
  393. children: []
  394. };
  395. currentFolder.children.push(folder);
  396. }
  397.  
  398. currentFolder = folder;
  399. }
  400.  
  401. // 创建文件节点并添加到相应的文件夹中
  402. const fileName = pathSegments[pathSegments.length - 1];
  403.  
  404. if(fileName && fileName.length && isIncludeFile(fileName) && !isExcludeFolder(entry.name)) {
  405. const fileNode = {
  406. label: fileName,
  407. type: 'file',
  408. entry: entry.name
  409. };
  410. currentFolder.children.push(fileNode);
  411. }
  412. });
  413.  
  414. return [root];
  415. }
  416. // 是否排除的文件夹
  417. function isExcludeFolder(entry) {
  418. if(entry.indexOf('css') > -1 ||
  419. entry.indexOf('scss') > -1 ||
  420. entry.indexOf('js') > -1 ||
  421. entry.indexOf('images') > -1 ||
  422. entry.indexOf('fui') > -1 ||
  423. entry.indexOf('lib') > -1 ||
  424. entry.indexOf('test') > -1 ||
  425. entry.indexOf('font') > -1 ||
  426. entry.indexOf('frame/fui') > -1) {
  427. return true;
  428. } else {
  429. return false;
  430. }
  431. }
  432. // 是否包含的文件
  433. function isIncludeFile(fileName) {
  434. if(fileName.indexOf('.html') > -1) {
  435. return true;
  436. } else {
  437. return false;
  438. }
  439. }
  440.  
  441. function getZipResource(callback) {
  442. const downloadUrl = window.location.origin + document.querySelector('.gl-button.btn-sm.btn-confirm').getAttribute('href');
  443.  
  444. fetch(downloadUrl)
  445. .then(response => response.arrayBuffer())
  446. .then(data => {
  447. // 将 ZIP 文件的二进制数据传递给 JSZip 进行解析
  448. return JSZip.loadAsync(data);
  449. })
  450. .then(zip => {
  451. // 获取 ZIP 文件中的所有条目(文件和目录)
  452. const zipEntries = Object.values(zip.files);
  453. const treeData = organizeFileEntries(zipEntries)
  454.  
  455. callback && callback(treeData);
  456.  
  457. })
  458. .catch(error => {
  459. console.error(error);
  460. });
  461. }
  462.  
  463. // 树结构第一个节点数据
  464. function findFirstFileNode(tree) {
  465. // 遍历树的节点
  466. for (let i = 0; i < tree.length; i++) {
  467. const node = tree[i];
  468.  
  469. // 如果节点的类型为 file,则返回该节点
  470. if (node.type === 'file') {
  471. return node;
  472. }
  473.  
  474. // 如果节点有子节点,则递归调用该函数查找子节点中的第一个 file 节点
  475. if (node.children && node.children.length > 0) {
  476. const fileNode = findFirstFileNode(node.children);
  477. if (fileNode) {
  478. return fileNode;
  479. }
  480. }
  481. }
  482.  
  483. // 如果没有找到 file 节点,则返回 null
  484. return null;
  485. }
  486. })();