月度归档:2024年09月

给清览平台增加允许复制粘贴功能

由于不希望学生可以复制粘贴代码,这样会失去实测的目的,但偶尔自己也需要复制粘贴测试学生提供的代码,于是用篡改猴组件给前端页面注入JS脚本来实现这个功能。由于代码编辑器是高级的monaco[1],所以需要使用monaco的API来设置代码,以保持数据的一致性,用法也是常规的editor->model->get/setValue。唯一需要知道的是如何引用已经存在的editor,凭借经验,大多数会放在window对象下,于是盲猜是window.monaco或者window.editor。最终确定是window.monaco.editor。注意:在篡改猴的JS脚本中,需要使用unsafeWindow来访问正常的window对象。



    // ==UserScript==
    // @name         清览题库|清览测验复制粘贴
    // @namespace    https://app.qingline.net/student/examing*
    // @version      0.2
    // @description  自动屏蔽弹窗;允许复制题目;允许复制代码;允许粘贴代码
    // @author       tpu01yzx
    // @connect      *
    // @match        https://app.qingline.net/student/examing*
    // @match        https://app.qingline.net/student/training?exam_id=*
    // @run-at       document-end
    // @license      MIT
    // @grant        unsafeWindow
    // ==/UserScript==

    (function () {
        console.log('清览题库|清览测验复制粘贴 => start');
        'use strict';

        window.addEventListener('load', function () {
            setTimeout(() => {
                init();
            }, 1000);
        });

        function getQuesText() {
            const $ = (s) => [...document.querySelectorAll(s)];

            const head = $(".stem")[0]
                .textContent
                .trim()
                .replace(/^([0-9]+)/, "")
                .replace(/[。?]/, "")
                .replace(/(\s+?)/g, " ");

            const body = $(".info_content")[0].textContent.trim();
            const test = $(".test_case_content")[0].textContent.trim();
            return head + "\n" + body + "\n" + test;
        }

        function getCodeText() {
            return "//Copied from app.qingline.net \n" + document.getElementsByClassName("inputarea")[0].value;
        }

        function AddButton(p, t, func, tips, cn) {
            // 创建一个新的按钮元素
            const newButton = document.createElement('button');
            newButton.type = 'button';
            //newButton.style.marginRight = "10px";
            newButton.className = 'submit_btn ant-btn '+ cn;

            // 创建一个新的span元素并将其添加到按钮中
            const newSpan = document.createElement('span');
            newSpan.textContent = t;
            newButton.appendChild(newSpan);

            // 添加新按钮作为fixed_con元素的第一个子元素
            p.insertBefore(newButton, p.firstChild);
            //fixedCon.appendChild(newButton);

            newButton.addEventListener('click', function () {
                //const textToCopy = getQuesText();
                //console.log(textToCopy);
                //navigator.clipboard.writeText(textToCopy)
                var msg;
                try{
                    func();
                    msg = tips || "成功";
                }catch(e){
                    msg = tips + "失败";
                }

                //console.log('Text copied to clipboard');
                // 创建一个新的元素来显示复制成功的消息
                const copySuccessMessage = document.createElement('div');
                copySuccessMessage.textContent = msg;
                copySuccessMessage.style.position = 'fixed';
                copySuccessMessage.style.bottom = '10px';
                copySuccessMessage.style.right = '10px';
                copySuccessMessage.style.padding = '10px';
                copySuccessMessage.style.backgroundColor = '#3d64ff';
                copySuccessMessage.style.color = 'white';
                copySuccessMessage.style.borderRadius = '5px';
                document.body.appendChild(copySuccessMessage);

                // 一段时间后隐藏消息
                setTimeout(function () {
                    copySuccessMessage.style.display = 'none';
                }, 1000);

            });
        }

        function init() {
            const cn = 'MyFunctionButton';
            //console.log('清览题库|清览测验禁用复制和全屏 => init');
            // 选择fixed_con元素
            var fixedCon = document.getElementsByClassName("right_bottom_btns");
            if(fixedCon && fixedCon.length > 0) {
                fixedCon = fixedCon[0];
                //console.log("found fixCon.");
                var myButton = document.getElementsByClassName(cn);
                if(myButton == null || myButton.length <= 0){
                    console.log("found no myButton.");
                    AddButton(fixedCon, '复制题目', function() {
                        const textToCopy = getQuesText();
                        //console.log(textToCopy);
                        navigator.clipboard.writeText(textToCopy);
                    }, '复制成功~', cn);

                    AddButton(fixedCon, '投影模式', function() {
                        const applyProjectionStyleToElements = (es) => {
                            // 定义递归函数
                            const applyStyle = (el) => {
                                if (el.style) {
                                    el.style.backgroundColor = "#002b36"; // 深蓝色背景
                                    el.style.color = "#FFFFFF";           // 白色字体
                                    el.style.fontSize = "24px";          // 字体大小
                                    el.style.fontWeight = "bold";        // 加粗
                                }
                                // 遍历子元素并递归调用
                                Array.from(el.children).forEach(child => applyStyle(child));
                            };

                            // 对传入的主元素应用样式
                            if(es && es.length > 0) {
                                var i;
                                for(i = 0; i < es.length; i++) {
                                    var element = es[i];
                                    //console.log(element);
                                    applyStyle(element);
                                }
                            }
                        };

                        const elements = [
                            document.querySelector('.ques_all'),
                            //document.querySelector('.info_content'),
                        ];
                        //console.log(elements);
                        applyProjectionStyleToElements(elements);


                        const _window = unsafeWindow ? unsafeWindow : window;
                        const editor = _window.monaco.editor;
                        // 定义投影模式主题
                        editor.defineTheme('projectionTheme', {
                            base: 'vs-dark', // 基于暗色主题
                            inherit: true,   // 继承默认规则
                            rules: [
                                { token: '', foreground: 'FFFFFF', background: '002b36' }, // 默认颜色
                                { token: 'keyword', foreground: 'FFD700' }, // 关键字 - 金黄色
                                { token: 'number', foreground: '87CEFA' },  // 数字 - 浅蓝色
                                { token: 'string', foreground: '90EE90' },  // 字符串 - 浅绿色
                                { token: 'comment', foreground: 'B0C4DE' }, // 注释 - 浅灰蓝
                            ],
                            colors: {
                                'editor.background': '#002b36', // 编辑器背景色
                                'editor.foreground': '#FFFFFF', // 编辑器前景色
                            }
                        });

                        // 应用主题
                        editor.setTheme('projectionTheme');
                        //const lines = document.querySelector(".monaco-editor");
                        //const fontSize = 32;  // 字体大小
                        //const lineHeight = Math.ceil(fontSize * 1.5); // 动态计算行高

                        //lines.style.fontSize = `${fontSize}px`;
                        //lines.style.lineHeight = `${lineHeight}px`;
                        //const editorDomElement = editor.getDomNode();
                        //editorDomElement.style.fontSize = fontSize + 'px';
                        //editorDomElement.style.lineHeight = lineHeight;

                        // 通知编辑器布局需要更新
                        editor.layout();

                    }, '启用投影模式', cn);

                    AddButton(fixedCon, '切换题目', function() {
                        // 获取左右元素
                        const leftElement = document.querySelector('.training_wrap_left');
                        const rightElement = document.querySelector('.training_wrap_right');
                        // 静态变量,用于记录初始状态
                        if (!leftElement.dataset.originalWidth) {
                            leftElement.dataset.originalWidth = getComputedStyle(leftElement).width;
                            rightElement.dataset.originalWidth = getComputedStyle(rightElement).width;
                        }

                        // 切换状态
                        if (leftElement.style.width === '0px' || leftElement.style.display === 'none') {
                            // 恢复到原始状态
                            leftElement.style.width = leftElement.dataset.originalWidth;
                            leftElement.style.display = 'block';
                            rightElement.style.width = rightElement.dataset.originalWidth;
                        } else {
                            // 隐藏左侧,右侧扩展到全宽
                            leftElement.style.width = '0';
                            leftElement.style.display = 'none';
                            rightElement.style.width = '100%';
                        }

                    }, '切换成功~', cn);

                    AddButton(fixedCon, '复制代码', function() {
                        //const textToCopy = getCodeText();
                        const _window = unsafeWindow ? unsafeWindow : window;
                        const models = _window.monaco.editor.getModels();
                        const spmodel = models[models.length - 1];
                        const textToCopy = spmodel.getValue();
                        console.log(textToCopy);
                        navigator.clipboard.writeText(textToCopy);
                    }, '复制成功~', cn);

                    AddButton(fixedCon, '粘贴代码', async function() {                        
                        const _window = unsafeWindow ? unsafeWindow : window;
                        const models = _window.monaco.editor.getModels();
                        const spmodel = models[models.length - 1];
                        const textToPaste = await navigator.clipboard.readText();
                        console.log("textToPaste:" + textToPaste);
                        spmodel.setValue(textToPaste);
                        //document.getElementsByClassName("inputarea")[0].value = textToPaste;
                    }, '粘贴成功~', cn);
                } else {
                    //console.log("found my button.");
                }
            } else {
                console.log("no fixCon found.");
            }

            // 检查是否存在弹窗
            const modal = document.querySelector('.ant-modal-body');            
            if (modal) {
                const modalRoot = document.querySelector('.ant-modal-root');
                if(modalRoot) {
                    const modalTitle = modalRoot.querySelector(".ant-modal-confirm-title");
                    if(modalTitle) {
                        console.log('modalTitle.textContent:' + modalTitle.textContent);
                        if(modalTitle.textContent == '为了给您最好的使用体验,请您使用谷歌浏览器。') {
                            modalRoot.remove();
                        }
                    }
                }
            }

            setTimeout(function() {
                    init();
            }, 3000);

        }
    })();

参考文献:
[1] https://microsoft.github.io/monaco-editor/
[2] https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.ITextModel.html