GitLab Viewer Publish and Deploy Project

GitLab Viewer Publish and Deploy Project!

目前為 2023-05-31 提交的版本,檢視 最新版本

// ==UserScript==
// @name         GitLab Viewer Publish and Deploy Project
// @namespace    http://tampermonkey.net/
// @version      1.06
// @description  GitLab Viewer Publish and Deploy Project!
// @author       Sean
// @match        http://192.168.0.200/fe3project/*
// @match        http://192.168.0.200/frontend_pc/project/*
// @icon         http://192.168.0.200/assets/favicon-7901bd695fb93edb07975966062049829afb56cf11511236e61bcf425070e36e.png
// @require      https://cdn.bootcdn.net/ajax/libs/vue/2.7.14/vue.min.js
// @require      https://unpkg.com/element-ui/lib/index.js
// @require      https://cdn.bootcdn.net/ajax/libs/jszip/3.7.1/jszip.min.js
// @resource     myFontFile https://element.eleme.io/2.11/static/element-icons.535877f.woff
// @grant        GM_getResourceURL
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @resource     ElementCSS https://unpkg.com/element-ui/lib/theme-chalk/index.css
// @grant        GM_xmlhttpRequest
// @license      MIT
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const fontUrl = 'https://element.eleme.io/2.11/static/element-icons.535877f.woff';

    // 添加样式规则,将字体应用到指定元素上
    GM_addStyle(`
        @font-face {
            font-family: element-icons;
            src: url(${fontUrl}) format("woff");
        }
    `);

    GM_addStyle(GM_getResourceText('ElementCSS'));

    let epointCss = ".epoint-tool {position: fixed; bottom: 0%; right:10px; transform: translateY(-50%);}";
    epointCss += ".el-row { padding: 3px 0;} .el-dialog__body .el-tree{min-height: 420px; max-height: 500px;overflow: auto;}";
    epointCss += ".deploy-body {height: 306px;}"
    epointCss += ".deploy-body .el-loading-spinner {margin-top: -50px;} .el-button--primary:focus {outline: 0 !important;}";
    // 添加注入样式
    let extraStyleElement = document.createElement("style");
    extraStyleElement.innerHTML = epointCss;
    document.head.appendChild(extraStyleElement);

    const MyComponent = {
        template: `<div class="epoint-wrap">
            <div class="epoint-tool">
            <el-row><el-button type="primary" icon="el-icon-search" round @click="viewProject">查看</el-button></el-row>
            <el-row>
                <el-tooltip content="一键部署到170服务器" placement="top" effect="light">
                    <el-button type="primary" icon="el-icon-s-unfold" round @click="doDeploy">部署</el-button>
                </el-tooltip>
            </el-row>
            <el-row>
                <el-tooltip content="一键发布到项目案例库" placement="top" effect="light">
                    <el-button type="primary" icon="el-icon-upload" round @click="publish">发布</el-button>
                </el-tooltip>
            </el-row>
            </div>
            <el-dialog
              title="目录结构"
              width="900px"
              :append-to-body="true"
              :visible.sync="dialogVisible"
              :before-close="handleClose">
                <el-tree
                    class="filter-tree"
                    :data="data"
                    :props="defaultProps"
                    node-key="id"
                    default-expand-all
                    @node-click="handleNodeClick"
                    v-loading="loadingTree"
                    ref="tree">
                </el-tree>
            </el-dialog>
            <el-dialog
                title="部署"
                width="420px"
                :visible.sync="depDialogVisible">
                    <div class="deploy-body"
                        v-loading="loading"
                        element-loading-text="正在打包部署至 170 服务器,请耐心等待"
                        element-loading-spinner="el-icon-loading">
                    </div>
            </el-dialog>
            <el-dialog title="部署提示" width="640px" :visible.sync="dialogDeployVisible">
                <el-alert
                    title="此操作将把 GitLab 资源打包部署至 170 服务器,已部署过的项目会进行覆盖部署,是否继续?"
                    type="warning" :closable="false" style="margin-bottom: 20px;">
                </el-alert>
                <el-form :model="form" ref="form" :rules="rules">
                    <el-form-item label="框架类型" :label-width="formLabelWidth" prop="frame">
                        <el-select v-model="form.frame" placeholder="请框架类型">
                            <el-option v-for="item in framework" :key="item.value" :label="item.label" :value="item.value"></el-option>
                        </el-select>
                    </el-form-item>
                </el-form>
                <div slot="footer" class="dialog-footer">
                    <el-button @click="dialogDeployVisible = false">取 消</el-button>
                    <el-button type="primary" @click="doDeploy">确 定</el-button>
                </div>
            </el-dialog>
        </div>`,
        data() {
            return {
                dialogVisible: false, // 查看目录结构弹窗
                data: [], // 目录结构树的数据结构
                defaultProps: {
                    children: 'children',
                    label: 'label'
                },
                loadingTree: false,
                depDialogVisible: false, // 部署中弹窗
                loading: false,
                projectLibUrl: 'http://192.168.201.159:9999/webapp/pages/default/onlinecase.html', // 项目案例库地址
                projectIsDeployed: false, // 项目是否部署过
                projectFtpUrl: '', // ftp路径
                projectEntryUrl: 'http://192.168.219.170/showcase/', // 项目案例库发布表单的入口页面
                supportDeploy: true, // 项目是否支持部署
                dialogDeployVisible: false,
                formLabelWidth: '120px',
                form: {
                    frame: ''
                },
                framework: [
                    {label: '重构模板', value: 1},
                    {label: 'f9x1.0', value: 2},
                    {label: 'f9x2.0', value: 3},
                    {label: 'f950', value: 4},
                    {label: 'f950sp1', value: 5},
                    {label: 'f950sp2', value: 6},
                    {label: 'f950sp3', value: 7},
                    {label: 'f940', value: 8},
                    {label: 'f942', value: 9},
                    {label: 'f941', value: 10},
                    {label: 'f934', value: 11},
                    {label: 'f933', value: 12},
                    {label: 'f932', value: 13},
                    {label: 'f9211', value: 14},
                    {label: '骨架', value: 15},
                    {label: 'Vue', value: 16},
                    {label: 'React', value: 17}
                ],
                rules: {
                    frame: [
                        { required: true, message: '请选择框架类型', trigger: 'change' }
                    ]
                },
                clickNodeEntry: null
            };
        },
        methods: {
            handleClose(done) {
                done();
                /*
                this.$confirm('确认关闭?')
                    .then(_ => {
                        done();
                    })
                    .catch(_ => {});*/
            },
            handleNodeClick(data) {
                console.log(data);
                if(data.type === 'folder') {
                    return false;
                }
                var self = this;
                var entry = data.entry;
                self.clickNodeEntry = entry;
                if(self.projectFtpUrl) {
                    window.open('http://192.168.219.170' + self.projectFtpUrl + data.entry);
                    self.clickNodeEntry = null;
                } else {
                    if(this.supportDeploy === false) {
                        this.$message({
                            type: 'error',
                            message: '暂只支持查看 重构模板 和 F9x2.0 的项目'
                        });
                        self.clickNodeEntry = null;
                        return false;
                    }

                    this.$confirm('资源未部署,部署至 170 服务器后可查看,是否部署?')
                        .then(_ => {

                        this.dialogDeployVisible = true;
                        /*
                        this.depDialogVisible = true;
                        this.loading = true;
                        // 部署
                        this.getDeployInfo({ type: '1' }, (data)=> {
                            this.loading = false;
                            this.depDialogVisible = false;

                            this.data = data.custom.detail;
                            this.projectFtpUrl = data.custom.ftpUrl;

                            this.$alert('部署成功!', '提示', {
                                confirmButtonText: '确定',
                                callback: action => {
                                    window.open('http://192.168.219.170' + self.projectFtpUrl + entry)
                                }
                            });

                        });*/
                    })
                    .catch(_ => {});
                }
            },
            // 部署
            doDeploy () {
                if(!this.isDownLoadPage()) {
                    return;
                }
                if(!this.dialogDeployVisible) {
                    this.dialogDeployVisible = true;
                    return;
                }

                let self = this;

                this.$refs['form'].validate((valid) => {
                    if (valid) {
                        this.dialogDeployVisible = false;
                        this.depDialogVisible = true;
                        this.loading = true;
                        // 部署
                        this.getDeployInfo({ type: '1', frame: this.form.frame }, (data)=> {
                            this.loading = false;
                            this.depDialogVisible = false;

                            if(!data.custom.text){

                                this.data = data.custom.detail;
                                this.projectFtpUrl = data.custom.ftpUrl;
                                this.supportDeploy = data.custom.supportDeploy;
                                // 从部署按钮直接过来的
                                if(!this.clickNodeEntry) {
                                    this.$message({
                                        type: 'success',
                                        message: '部署成功!'
                                    });
                                    // 打开查看弹窗
                                    this.viewProject();
                                } else {// 从点击目录结构过来的,可以调整点击的树节点页面
                                    this.$alert('部署成功!', '提示', {
                                        confirmButtonText: '确定',
                                        callback: action => {
                                            window.open('http://192.168.219.170' + self.projectFtpUrl + self.clickNodeEntry);
                                            self.clickNodeEntry = null;
                                        }
                                    });
                                }

                            } else {
                                this.$message({
                                    type: 'error',
                                    message: data.custom.text
                                });
                            }
                        });
                    } else {
                        console.log('error submit!!');
                        return false;
                    }
                });

                /*
                this.$confirm('此操作将把 GitLab 资源打包部署至 170 服务器,已部署过的项目会进行覆盖部署,是否继续?', '提示', {
                    confirmButtonText: '确定',
                    cancelButtonText: '取消',
                    type: 'warning'
                }).then(() => {
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消部署'
                    });
                });*/
            },
            // 发布项目案例库
            publish () {
                this.$confirm('此操作将把项目发布至 <a href="'+ this.projectLibUrl +'" target="_blank">项目案例库</a>,已发布过的项目案例库会有重复项,是否继续?', '提示', {
                    confirmButtonText: '确定',
                    cancelButtonText: '取消',
                    dangerouslyUseHTMLString: true,
                    type: 'warning'
                }).then(() => {
                    const themes = document.querySelectorAll('.badge-secondary');
                    let keys = [];

                    for(let i = 0, l = themes.length; i < l; i++) {
                        if(themes[0].innerText == '智能设备' && i > 0) {
                            keys.push(themes[i].innerText);
                        } else if (themes[0].innerText !== '智能设备' && i > 1) {
                            keys.push(themes[i].innerText);
                        }
                    }
                    // 存在更多主题的情况
                    const moreKeyEl = document.querySelector('.gl-w-full .text-nowrap');
                    if(moreKeyEl) {
                        const moreKeyElContent = moreKeyEl.getAttribute('data-content');
                        const regex = />([^<]+)</g;
                        const matches = moreKeyElContent.match(regex);
                        const results = matches.filter(function(match) {
                            return match.length > 3;
                        });
                        const moreKeyData = results.map(function(match) {
                            return match.substring(2, match.length - 2);
                        });

                        if(moreKeyData && moreKeyData.length) {
                            keys = keys.concat(moreKeyData);
                        }
                    }

                    // 组织项目案例库所需参数
                    const projectName = document.querySelector('.home-panel-title').innerText;
                    const projectBU = themes[0] ? themes[0].innerText : null;
                    const projectKeys = keys.join(' ');
                    const entryUrl = this.projectEntryUrl;
                    let projectType = themes[1] ? themes[1].innerText : null;

                    if(themes[0]) {
                        if(themes[0].innerText == '智能设备') {
                            projectType = themes[1] ? themes[0].innerText : null;
                        } else {
                            projectType = themes[1] ? themes[1].innerText : null;
                        }
                    }

                    const destUrl = this.projectLibUrl + '?projectName=' + projectName + '&projectBU=' + projectBU + '&projectType=' + projectType + '&projectKeys=' + projectKeys + '&entryUrl=' + entryUrl + '&git=' + window.location.href;

                    this.$message({
                        type: 'success',
                        message: destUrl
                    });

                    window.open(destUrl);

                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消发布'
                    });
                });
            },
            // 查看项目
            viewProject () {
                if(!this.isDownLoadPage()) {
                    return;
                }
                this.dialogVisible = true;
                this.loadingTree = true;
                let self = this;

                // 发送ajax请求,查看是否进行过部署
                this.getDeployInfo((data)=> {
                    // 有部署信息,直接赋值,
                    if(true) {
                        self.projectIsDeployed = true;
                        self.data = data.custom.detail;
                        self.projectFtpUrl = data.custom.ftpUrl;
                        self.supportDeploy = data.custom.supportDeploy;
                        self.loadingTree = false;


                    } else {
                        // 无部署信息,仅查看文件目录
                        getZipResource((data)=> {
                            self.data = data;
                            self.loadingTree = false;
                        });
                    }
                });

            },
            // 项目部署信息
            getDeployInfo(params, callback) {
                const projectId = document.body.getAttribute('data-project-id');
                const downloadBtn = document.querySelector('.gl-button.btn-sm.btn-confirm');

                const sourceUrl = downloadBtn.getAttribute('href');
                const downloadUrl = window.location.origin + sourceUrl;
                const files = document.body.getAttribute('data-find-file').split('/');
                const name = document.body.getAttribute('data-project') + '-' + files[files.length - 1];
                const author = document.querySelector('.current-user .gl-font-weight-bold').innerText.trim();
                const projectName = document.querySelector('.sidebar-context-title').innerText.trim();
                const deployManOA = document.querySelector('.current-user>a').getAttribute('data-user');
                const projectGitUrl = 'http://192.168.0.200' + document.body.getAttribute('data-find-file').split('/-/')[0];

                if(typeof params == 'function') {
                    callback = params;
                    params = null;
                }

                if(projectId && projectId.length && sourceUrl) {
                    fetch('http://192.168.219.170:3008/api/getDeployInfo', {
                        method: 'POST',
                        // 允许跨域请求
                        mode: 'cors',
                        headers: {
                            'Accept': 'application/json',
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({//post请求参数
                            params: {
                                "type": (params && params.type !== undefined) ? params.type : '0',// 0 代表查看, 1代表部署
                                "name": name, // 项目路径英文名
                                "deployMan": author, // 部署人姓名
                                "deployManOA": deployManOA, // 部署人账号
                                "projectName": projectName, // 项目名称
                                "downloadUrl": downloadUrl, // 下载地址
                                "projectId": projectId, // 主键,gitlab上的项目id
                                "projectGitUrl": projectGitUrl,
                                "frame": (params && params.frame) ? params.frame : undefined
                            }
                        })
                    })
                    .then(response => response.text())
                    .then((result) => {
                        var data = JSON.parse(result);
                        callback && callback(data);
                    })
                    .catch(error => {
                        this.depDialogVisible = false;
                        this.dialogVisible = false;
                        this.$message({
                            type: 'error',
                            message: '系统故障,请联系管理员。'
                        });
                        console.error(error);
                    });
                } else {
                    this.$message({
                        type: 'error',
                        message: '本页不支持查看和部署,请至仓库页。'
                    });
                    console.error('部署信息请求参数error');
                }
            },
            // 设置入口
            setProjectEntry(){
                const firstNode = findFirstFileNode(this.data);
                if(firstNode) {
                    this.projectEntryUrl = 'http://192.168.219.170' + this.projectFtpUrl + firstNode.entry;
                }
            },
            isDownLoadPage() {
                const downloadBtn = document.querySelector('.gl-button.btn-sm.btn-confirm');
                const sourceUrl = downloadBtn && downloadBtn.getAttribute('href');

                if(downloadBtn && sourceUrl) {
                    return true;
                } else {
                    this.$message({
                        type: 'error',
                        message: '本页面不支持查看和部署,请移至仓库页。'
                    });
                    return false;
                }
            }
        },
        mounted() {

        }
    };

    const placeholder = document.createElement('div');

    // 创建 Vue 实例并挂载到页面
    const vueInstance = new Vue({
        el: placeholder,
        components: {
            MyComponent
        },
        methods: {
        },
        template: `<my-component></my-component>`
    });

    // 等待页面加载完成
    window.addEventListener('load', function() {
        // 将占位元素追加到 body 元素中
        document.body.appendChild(vueInstance.$el);
    });

    // 将文件条目组织成嵌套结构
    function organizeFileEntries(fileEntries) {
        const root = {
            label: document.querySelector('.home-panel-title').innerText || document.getElementById('project_name_edit').value,
            type: 'folder',
            children: []
        };

        // 创建嵌套结构
        fileEntries.forEach(entry => {
            const pathSegments = entry.name.split('/');
            let currentFolder = root;

            // 遍历路径中的每个部分,创建相应的文件夹节点
            for (let i = 0; i < pathSegments.length - 1; i++) {
                const folderName = pathSegments[i];
                let folder = currentFolder.children.find(child => child.label === folderName);

                if(isExcludeFolder(entry.name)) {
                    continue;
                }

                if (!folder) {
                    folder = {
                        label: folderName,
                        type: 'folder',
                        children: []
                    };
                    currentFolder.children.push(folder);
                }

                currentFolder = folder;
            }

            // 创建文件节点并添加到相应的文件夹中
            const fileName = pathSegments[pathSegments.length - 1];

            if(fileName && fileName.length && isIncludeFile(fileName) && !isExcludeFolder(entry.name)) {
                const fileNode = {
                    label: fileName,
                    type: 'file',
                    entry: entry.name
                };
                currentFolder.children.push(fileNode);
            }
        });

        return [root];
    }
    // 是否排除的文件夹
    function isExcludeFolder(entry) {
        if(entry.indexOf('css') > -1 ||
           entry.indexOf('scss') > -1 ||
           entry.indexOf('js') > -1 ||
           entry.indexOf('images') > -1 ||
           entry.indexOf('fui') > -1 ||
           entry.indexOf('lib') > -1 ||
           entry.indexOf('test') > -1 ||
           entry.indexOf('font') > -1 ||
           entry.indexOf('frame/fui') > -1) {
            return true;
        } else {
            return false;
        }
    }
    // 是否包含的文件
    function isIncludeFile(fileName) {
        if(fileName.indexOf('.html') > -1) {
            return true;
        } else {
            return false;
        }
    }

    function getZipResource(callback) {
        const downloadUrl = window.location.origin + document.querySelector('.gl-button.btn-sm.btn-confirm').getAttribute('href');

        fetch(downloadUrl)
            .then(response => response.arrayBuffer())
            .then(data => {
            // 将 ZIP 文件的二进制数据传递给 JSZip 进行解析
            return JSZip.loadAsync(data);
        })
            .then(zip => {
            // 获取 ZIP 文件中的所有条目(文件和目录)
            const zipEntries = Object.values(zip.files);
            const treeData = organizeFileEntries(zipEntries)

            callback && callback(treeData);

        })
            .catch(error => {
            console.error(error);
        });
    }

    // 树结构第一个节点数据
    function findFirstFileNode(tree) {
        // 遍历树的节点
        for (let i = 0; i < tree.length; i++) {
            const node = tree[i];

            // 如果节点的类型为 file,则返回该节点
            if (node.type === 'file') {
                return node;
            }

            // 如果节点有子节点,则递归调用该函数查找子节点中的第一个 file 节点
            if (node.children && node.children.length > 0) {
                const fileNode = findFirstFileNode(node.children);
                if (fileNode) {
                    return fileNode;
                }
            }
        }

        // 如果没有找到 file 节点,则返回 null
        return null;
    }
})();