禅道日志工具

禅道的一些便捷工具

// ==UserScript==
// @name         禅道日志工具
// @namespace
// @version      1.2.1
// @description  禅道的一些便捷工具
// @author       chenwuai
// @license      MIT
// @match        http://192.168.0.16:88/zentao/*
// @grant        none
// @namespace
// @namespace 
// ==/UserScript==
(function() {
    'use strict';

    if(typeof $ === 'undefined') return

    // 样式
    const style = `
        #dropMenu {
            width: 600px;
        }
        #dropMenu #tabContent {
            max-width: 100%;
        }
        #currentItem .text{
            max-width: 100% !important;
        }
    `;
    document.head.insertAdjacentHTML("beforeend", `<style>${style}</style>`);


    const pathName = document.location.pathname;
    let projectName = '';
    let loginUserId = ''

    // 初始化函数,根据页面路径调用不同的初始化方法
    function init() {
        if (/zentao\/execution-task\S+/.test(pathName)) {
            executionTaskInit();
        } else if (/my-work-task\S+/.test(pathName)) {
            myWorkTaskInit();
        } else if (/task-create\S+/.test(pathName)) {
            taskCreateInit();
        } else if (/story-create\S+/.test(pathName)) {
            storyCreateInit();
        }
    }

    // 任务页面初始化
    function executionTaskInit() {
        const projectBtn = document.querySelector('#heading #currentItem');
        projectName = projectBtn?.title || ''; // 使用可选链操作符

        const moreBtn = document.querySelector('#more');
        if (moreBtn) {
            addCustomButton(moreBtn, 'copyTaskLog', '复制日志', formatterExecutionTaskLog);
        }
    }

    // 我的任务总览页面初始化
    function myWorkTaskInit() {
        const moreBtn = document.querySelector('#mainMenu');
        if (moreBtn) {
            addCustomButton(moreBtn, 'copyTaskLog', '复制日志', formatterMyWorkTaskLog);
        }
    }

    // 新增任务页面初始化
    function taskCreateInit(){
        $('#dataform tr.mailtoBox').hide()
        $('#dataform tr.datePlanBox').hide()

        $('select#type').val('devel').trigger("chosen:updated")
        $('select#assignedTo').val(loginUserId).trigger("chosen:updated")
    }

    // 新增需求页面初始化
    function storyCreateInit(){
        $('#dataform tr.sourceBox').hide()
        $('#dataform tr.verifyBox').hide()
        $('#dataform tr.mailtoBox').hide()
        $('#dataform tr.keywordsBox').hide()
        // $('#dataform #planIdBox').parent().hide()
        $('#dataform #moduleIdBox').parent().css({'display':'block','min-width':'500px'})
        $('#dataform #moduleIdBox').parent().prev().css({'display':'block','min-width':'500px'})
        $('#dataform #parent_chosen').parent().parent().hide()

        $('select#assignedTo').val(loginUserId).trigger("chosen:updated")
    }

    // 任务页面-格式化日志
    function formatterExecutionTaskLog(event){

        let checkedIDs = getCheckedTaskLogIds()
        if(checkedIDs.length === 0){
            alert('请选中日志')
            return
        }

        const logs = getCheckedTaskLog() // 获取选中任务对象
        if(!logs) return;

        const textList = [projectName]
        logs.forEach((i,index)=>{
            const text = getTaskLogText(i)
            textList.push(`${text}`)
        });
        copyTextToClipboard(textList.join('\r\n'), event.target);
    }

    // 获取选中的任务日志
    function getCheckedTaskLog() {
        try {
            const logCells = $('#taskList .dtable-body .dtable-cell.is-checked');
            if (!logCells || logCells.length === 0) return null;
            const logs = {};
            const needKeys = ['id', 'name', 'status', 'progress'];

            logCells.each(function(){
                let row = $(this).data('row');
                if (!logs[row]) {
                    logs[row] = { id: row };
                }
                if (needKeys.includes($(this).data('col'))) {
                    logs[row][$(this).data('col')] = $(this).text();
                }
            })


            return Object.values(logs).sort((a, b) => (a.progress || 0) - (b.progress || 0) || a.id - b.id);
        } catch (error) {
            console.error("Error in getCheckedTaskLog:", error);
            return null;
        }
    }

    // 获取任务日志选中的id数组
    function getCheckedTaskLogIds(){
        var taskForm = $('#executionTaskForm')[0]
        if (taskForm && taskForm.elements && taskForm.elements['taskIDList[]']){
            var checkedIdList = []
            if(typeof taskForm.elements['taskIDList[]'].length !== 'undefined'){
                $(taskForm.elements['taskIDList[]']).each(function(){
                    checkedIdList.push(+$(this).val())
                })
            }else{
                checkedIdList.push(+$(taskForm.elements['taskIDList[]']).val())
            }
            return checkedIdList
        }else{
            return []
        }
    }

    // 获取任务日志文本
    function getTaskLogText(data) {
        if (!data) {
            console.error('Invalid task log data');
            return '';
        }

        let id = data.id?.trim() || '';
        let name = data.name?.trim() || '';
        let status = data.status?.trim() || '';
        const progress = data.progress;

        return `【${id}】${name} - ${status === '进行中' ? `${status} ${progress}%` : status}`;
    }

    // 我的任务页面-格式化日志
    function formatterMyWorkTaskLog(event){

        const logs = getCheckedMyWorkTaskLog()
        if(!logs){
            alert('请选中日志')
            return
        }

        const textList = []
        Object.keys(logs).forEach(key=>{
            const list = logs[key].sort((a, b) => (a.progress || 0) - (b.progress || 0) || a.id - b.id)
            const strList = list.reduce((a,c)=>{
                const str = `【${c.id}】${c.name} - ${c.statusName} ${c.progress > 0 ? c.progress + '%' : ''}`
                a.push(str)
                return a
            },[key])
            textList.push(strList.join('\r\n'))
        })

        copyTextToClipboard(textList.join('\r\n'), event.target);
    }
    // 获取选中的任务日志
    function getCheckedMyWorkTaskLog() {
        try {
            const logCells = $('#myTaskList tr.checked');
            if (!logCells || logCells.length === 0) return null;
            const logs = {};

            logCells.each(function(){
                const log = getMyWorkTaskLogInfo(this)
                if(log){
                    const projectName = log.project.replace(/[\r\n]/g, '').replace(/\s+/g, ' ').trim()
                    if(typeof logs[projectName] === 'undefined'){
                        logs[projectName] = []
                    }
                    logs[projectName].push(log)
                }
            })
            return logs
        } catch (error) {
            console.error("Error in getCheckedTaskLog:", error);
            return null;
        }
    }
    // 获取我的任务日志信息
    function getMyWorkTaskLogInfo(dom) {
        if (!dom) {
            console.error('Invalid task log data');
            return '';
        }
        const id = getChildDomText(dom,'.c-id');
        const name = getChildDomText(dom,'.c-name');
        const statusName = getChildDomText(dom,'.c-status');
        const project = getChildDomText(dom,'.c-project');
        const dataset = dom.dataset

        const status = dataset.status
        let progress = 0
        if(status === 'doing'){
            const consumed = +dataset.consumed
            const total = +dataset.consumed + +dataset.left
            progress = (Math.floor(consumed / total * 100))
        }
        return { project, id, name, status, statusName, progress}
    }


    // tools
    // 复制文本到剪贴板,并提供用户反馈
    async function copyTextToClipboard(text, target) {
        try {
            if (navigator.clipboard && navigator.clipboard.writeText) { // 检查 navigator.clipboard 和 writeText 是否存在
                await navigator.clipboard.writeText(text);
                $(target).data('originalText', target.innerHTML);
                target.innerHTML = '✌已复制✌';
                setTimeout(() => target.innerHTML = $(target).data('originalText') || '复制', 1500);
                return; // 成功复制后直接返回
            }
        } catch (err) {
            console.warn('使用 navigator.clipboard 复制失败,尝试使用 document.execCommand:', err);
            // 不做任何处理,继续执行下面的兼容方案
        }

        // 兼容方案:使用 document.execCommand('copy')
        const el = $('<textarea>');
        el.val(text);
        el.attr('readonly', '');
        el.css({
            position: 'absolute',
            left: '-9999px'
        });
        $('body').append(el);

        // 保存当前选区
        const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;

        el.select();
        try {
            const successful = document.execCommand('copy');
            if(successful){
                $(target).data('originalText', target.innerHTML);
                target.innerHTML = '✌已复制✌';
                setTimeout(() => target.innerHTML = $(target).data('originalText') || '复制', 1500);
            } else {
                console.error('使用 document.execCommand 复制失败');
                alert('复制失败,请手动复制');
            }
        } catch (err) {
            console.error('使用 document.execCommand 复制时发生错误:', err);
            alert('复制失败,请手动复制');
        }

        el.remove(); // 移除textarea

        // 恢复选区
        if (selected) {
            document.getSelection().removeAllRanges();
            document.getSelection().addRange(selected);
        }
    }
    
    // 添加 button 子元素
    function addCustomButton(parent,id,text,cb){
        if(!parent || !id || !text || !cb){
            console.error('addCustomButton Error',parent,id,text,cb)
            return
        }

        const btn = $('<button>', { // 使用 jQuery 创建元素更简洁
            id: id,
            class: 'btn btn-link',
            style: 'color: #0DBB7D;',
            text: text,
            click: cb,
        });

        $(parent).append(btn);
    }

    function getChildDomText(dom,child){
      const childDom = $(dom).find(child)
      return childDom?.text().trim() || ''
    }

    function getLoginUser(){
        loginUserId = localStorage.getItem('zentao_user')
        if(loginUserId) return
        fetch(`/zentao/my-profile.html?onlybody=yes`, {
            credentials: 'include'
          })
            .then(res => res.text())
            .then(html => {
              const match = html.match(/<th>\s*用户名\s*<\/th>\s*<td>(.*?)<\/td>/);
              if (match && match[1]) {
                loginUserId = match[1].trim();
                localStorage.setItem('zentao_user', loginUserId)
              } else {
                console.warn('⚠️ 未匹配到用户名字段');
              }
            })
            .catch(err => {
              console.error('获取用户信息失败:', err);
            });
    }

    $(document).ready(()=>{
        getLoginUser()
        console.log('当前用户账号:', loginUserId);
        init()
    });
})();