// ==UserScript== // @name 小雅答答答 // @license MIT // @version 1.32 // @description 小雅平台学习助手 📖,智能整理归纳学习资料 📚,辅助完成练习 💪,并提供便捷的查阅和修改功能 📝! // @author Yi // @match https://*.ai-augmented.com/* // @icon https://www.ai-augmented.com/static/logo3.1dbbea8f.png // @grant none // @run-at document-end // @require https://update.greasyfork.icu/scripts/517782/1485790/docx.js // @require https://update.greasyfork.icu/scripts/517783/1485791/FileSaverminjs.js // @require https://update.greasyfork.icu/scripts/515732/1477483/av-min.js // @homepageURL https://zygame1314.site // @namespace https://greasyfork.org/users/1268039 // @downloadURL https://update.greasyfork.icu/scripts/517280/%E5%B0%8F%E9%9B%85%E7%AD%94%E7%AD%94%E7%AD%94.user.js // @updateURL https://update.greasyfork.icu/scripts/517280/%E5%B0%8F%E9%9B%85%E7%AD%94%E7%AD%94%E7%AD%94.meta.js // ==/UserScript== (function () { 'use strict'; let isActivated = false; const activationTime = localStorage.getItem('activationTime'); if (activationTime) { const currentTime = Date.now(); const elapsed = currentTime - parseInt(activationTime, 10); if (elapsed < 14400000) { isActivated = true; } else { localStorage.removeItem('isActivated'); localStorage.removeItem('activationTime'); } } const { Document, Packer, Paragraph, HeadingLevel, AlignmentType, ImageRun, TextRun } = window.docx; let autoFetchEnabled = localStorage.getItem('autoFetchEnabled') === 'true'; let autoFillEnabled = localStorage.getItem('autoFillEnabled') === 'true'; let isProcessing = false; let debounceTimer = null; function addButtons() { const container = document.createElement('div'); container.style.cssText = ` position: fixed; top: 150px; left: 150px; z-index: 9999; `; const mainBall = document.createElement('div'); mainBall.style.cssText = ` width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(145deg, #6366F1, #4F46E5); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); cursor: move; display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; transition: transform 0.3s ease; user-select: none; `; mainBall.innerHTML = '✨'; const buttons = [ { icon: '🕷️', text: '获取答案/激活脚本', onClick: () => getAndStoreAnswers(), color: '#EF4444' }, { icon: '✍️', text: '填写答案', onClick: () => fillAnswers(), color: '#3B82F6' }, { icon: '🖋️', text: '查看/编辑答案', onClick: () => showAnswerEditor(), color: '#F97316' }, { icon: '📄', text: '导出作业', onClick: () => exportHomework(), color: '#8B5CF6' }, { icon: '🧭', text: '使用指南', onClick: () => showTutorial(), color: '#6366F1' }, { icon: autoFetchEnabled ? '🔄' : '⭕', text: '自动获取', onClick: () => { autoFetchEnabled = !autoFetchEnabled; localStorage.setItem('autoFetchEnabled', autoFetchEnabled); buttons[5].ball.innerHTML = autoFetchEnabled ? '🔄' : '⭕'; buttons[5].ball.style.background = autoFetchEnabled ? '#22c55e' : '#94a3b8'; }, color: autoFetchEnabled ? '#22c55e' : '#94a3b8' }, { icon: autoFillEnabled ? '🔄' : '⭕', text: '自动填写', onClick: () => { autoFillEnabled = !autoFillEnabled; localStorage.setItem('autoFillEnabled', autoFillEnabled); buttons[6].ball.innerHTML = autoFillEnabled ? '🔄' : '⭕'; buttons[6].ball.style.background = autoFillEnabled ? '#22c55e' : '#94a3b8'; }, color: autoFillEnabled ? '#22c55e' : '#94a3b8' }, ...(localStorage.getItem('activationCode') === 'ILOVEXIAOYA' ? [ { icon: '⚡', text: '创建补交', onClick: () => enableMakeup(), color: 'linear-gradient(145deg, #22c55e, #16a34a)', special: true }, { icon: '🔥', text: '上传补交', onClick: () => submitHomework(), color: 'linear-gradient(145deg, #22c55e, #16a34a)', special: true } ] : []) ]; let isExpanded = false; const functionBalls = []; buttons.forEach((btn, index) => { const ball = document.createElement('div'); ball.style.cssText = ` position: absolute; width: 50px; height: 50px; border-radius: 50%; background: ${btn.special ? btn.color : btn.color}; display: flex; align-items: center; justify-content: center; color: white; font-size: 20px; cursor: pointer; transition: all 0.3s ease; opacity: 0; transform: scale(0); transform-origin: center center; box-shadow: ${btn.special ? '0 2px 12px rgba(34, 197, 94, 0.5)' : '0 2px 8px rgba(0,0,0,0.2)'}; ${btn.special ? 'animation: pulse 2s infinite;' : ''} `; ball.innerHTML = btn.icon; btn.ball = ball; const style = document.createElement('style'); style.textContent = ` @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); } 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); } } `; document.head.appendChild(style); ball.addEventListener('mouseenter', () => { const currentTransform = ball.style.transform; if (currentTransform.includes('translate')) { ball.style.transform = currentTransform.replace('scale(1)', 'scale(1.1)'); } const tooltip = document.createElement('div'); const ballRect = ball.getBoundingClientRect(); const viewportWidth = window.innerWidth; const showOnLeft = ballRect.right > viewportWidth - 100; const left = showOnLeft ? (ballRect.left - 100) : (ballRect.right + 10); tooltip.style.cssText = ` position: fixed; left: ${left}px; top: ${ballRect.top + ballRect.height / 2}px; transform: translateY(-50%); padding: 6px 12px; background: rgba(0,0,0,0.8); color: white; border-radius: 4px; font-size: 14px; white-space: nowrap; pointer-events: none; z-index: 100000; `; tooltip.textContent = btn.text; document.body.appendChild(tooltip); ball.tooltip = tooltip; }); ball.addEventListener('mouseleave', () => { const currentTransform = ball.style.transform; if (currentTransform.includes('translate')) { ball.style.transform = currentTransform.replace('scale(1.1)', 'scale(1)'); } if (ball.tooltip) { document.body.removeChild(ball.tooltip); ball.tooltip = null; } }); ball.addEventListener('click', (e) => { e.stopPropagation(); btn.onClick(); }); functionBalls.push(ball); container.appendChild(ball); }); function toggleButtons() { isExpanded = !isExpanded; const radius = 120; functionBalls.forEach((ball, index) => { if (isExpanded) { const angle = (2 * Math.PI / functionBalls.length) * index; const x = Math.cos(angle) * radius; const y = Math.sin(angle) * radius; ball.style.transform = `translate(${x}px, ${y}px) scale(1)`; ball.style.opacity = '1'; } else { ball.style.transform = 'translate(0, 0) scale(0)'; ball.style.opacity = '0'; } }); mainBall.style.transform = isExpanded ? 'rotate(180deg)' : 'rotate(0)'; } mainBall.addEventListener('click', (e) => { if (!hasDragged) { e.stopPropagation(); toggleButtons(); } }); let isDragging = false; let currentX; let currentY; let initialX; let initialY; let dragStartX; let dragStartY; const DRAG_THRESHOLD = 5; let hasDragged = false; mainBall.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); mainBall.addEventListener('touchstart', dragStart, { passive: false }); document.addEventListener('touchmove', drag, { passive: false }); document.addEventListener('touchend', dragEnd); let touchStartTime = 0; let touchStartX = 0; let touchStartY = 0; function dragStart(e) { if (e.type === 'mousedown') { initialX = e.clientX - container.offsetLeft; initialY = e.clientY - container.offsetTop; dragStartX = e.clientX; dragStartY = e.clientY; } else if (e.type === 'touchstart') { e.preventDefault(); const touch = e.touches[0]; initialX = touch.clientX - container.offsetLeft; initialY = touch.clientY - container.offsetTop; dragStartX = touch.clientX; dragStartY = touch.clientY; touchStartTime = Date.now(); touchStartX = touch.clientX; touchStartY = touch.clientY; } if (e.target === mainBall) { isDragging = true; hasDragged = false; } } function drag(e) { if (isDragging) { e.preventDefault(); let clientX, clientY; if (e.type === 'mousemove') { clientX = e.clientX; clientY = e.clientY; } else if (e.type === 'touchmove') { const touch = e.touches[0]; clientX = touch.clientX; clientY = touch.clientY; } currentX = clientX - initialX; currentY = clientY - initialY; const dragDistance = Math.sqrt( Math.pow(clientX - dragStartX, 2) + Math.pow(clientY - dragStartY, 2) ); if (dragDistance > DRAG_THRESHOLD) { hasDragged = true; } currentX = Math.min(Math.max(0, currentX), window.innerWidth - container.offsetWidth); currentY = Math.min(Math.max(0, currentY), window.innerHeight - container.offsetHeight); container.style.left = currentX + 'px'; container.style.top = currentY + 'px'; } } function dragEnd(e) { if (e.type === 'touchend' && !hasDragged) { const touchEndTime = Date.now(); const touchDuration = touchEndTime - touchStartTime; if (touchDuration < 200) { e.stopPropagation(); toggleButtons(); } } initialX = currentX; initialY = currentY; isDragging = false; } container.appendChild(mainBall); document.body.appendChild(container); } function createProgressBar() { const style = document.createElement('style'); style.textContent = ` .answer-progress { position: fixed; top: 0; left: 0; width: 100%; height: 6px; background: rgba(0, 0, 0, 0.05); z-index: 10000; opacity: 0; transition: opacity 0.4s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); pointer-events: none; } .answer-progress-bar { height: 100%; background: linear-gradient(90deg, #60a5fa, #818cf8); width: 0%; transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1); border-radius: 0 3px 3px 0; box-shadow: 0 0 8px rgba(96, 165, 250, 0.5); } .answer-progress-text { position: fixed; top: 12px; right: 20px; transform: translateY(-10px); background: #4f46e5; color: white; padding: 6px 12px; border-radius: 6px; font-size: 13px; opacity: 0; transition: all 0.4s ease; box-shadow: 0 2px 6px rgba(79, 70, 229, 0.3); font-weight: bold; pointer-events: none; } `; document.head.appendChild(style); const progressContainer = document.createElement('div'); progressContainer.className = 'answer-progress'; const progressBar = document.createElement('div'); progressBar.className = 'answer-progress-bar'; const progressText = document.createElement('div'); progressText.className = 'answer-progress-text'; progressContainer.appendChild(progressBar); document.body.appendChild(progressContainer); document.body.appendChild(progressText); return { show: () => { progressContainer.style.opacity = '1'; progressText.style.opacity = '1'; progressText.style.transform = 'translateY(0)'; }, hide: () => { progressContainer.style.opacity = '0'; progressText.style.opacity = '0'; progressText.style.transform = 'translateY(-10px)'; setTimeout(() => { progressContainer.remove(); progressText.remove(); }, 300); }, update: (current, total, action = '正在填写') => { const percent = (current / total) * 100; progressBar.style.width = percent + '%'; progressText.textContent = `${action}: ${current}/${total} 题`; } }; } addButtons(); class NotificationAnimator { static animations = { fadeSlide: { enter: { initial: { opacity: '0', transform: 'translateY(-20px)' }, final: { opacity: '1', transform: 'translateY(0)' } }, exit: { initial: { opacity: '1', transform: 'translateY(0)' }, final: { opacity: '0', transform: 'translateY(-20px)' } } }, scale: { enter: { initial: { opacity: '0', transform: 'scale(0.8)' }, final: { opacity: '1', transform: 'scale(1)' } }, exit: { initial: { opacity: '1', transform: 'scale(1)' }, final: { opacity: '0', transform: 'scale(0.8)' } } }, slideRight: { enter: { initial: { opacity: '0', transform: 'translateX(-100%)' }, final: { opacity: '1', transform: 'translateX(0)' } }, exit: { initial: { opacity: '1', transform: 'translateX(0)' }, final: { opacity: '0', transform: 'translateX(100%)' } } } }; static applyAnimation(element, animationType, isEnter) { const animation = this.animations[animationType]; if (!animation) return; const { initial, final } = isEnter ? animation.enter : animation.exit; Object.assign(element.style, { transition: 'all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55)', ...initial }); requestAnimationFrame(() => { Object.assign(element.style, final); }); } } function getNotificationContainer() { let container = document.getElementById('notification-container'); if (!container) { container = document.createElement('div'); container.id = 'notification-container'; container.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 10000; max-height: calc(100vh - 40px); overflow-y: auto; pointer-events: none; display: flex; flex-direction: column; align-items: center; opacity: 0; transition: opacity 0.3s ease; `; document.body.appendChild(container); container.offsetHeight; container.style.opacity = '1'; } return container; } function showNotification(message, options = {}) { const { type = 'info', duration = 3000, keywords = [], animation = 'fadeSlide' } = options; const highlightColors = { success: '#ffba08', error: '#14b8a6', warning: '#8b5cf6', info: '#f472b6' }; const highlightColor = highlightColors[type] || highlightColors.info; const highlightStyle = ` color: ${highlightColor}; font-weight: bold; border-bottom: 2px solid ${highlightColor}50; transition: all 0.3s ease; border-radius: 3px; `; const highlightedMessage = keywords.reduce((msg, keyword) => { if (keyword && keyword.trim()) { const regex = new RegExp(keyword.trim(), 'g'); return msg.replace(regex, `${keyword}`); } return msg; }, message); const notification = document.createElement('div'); notification.style.position = 'relative'; notification.style.marginBottom = '10px'; notification.style.padding = '15px 20px'; notification.style.borderRadius = '12px'; notification.style.color = '#333'; notification.style.fontSize = '16px'; notification.style.fontWeight = 'bold'; notification.style.boxShadow = '0 8px 16px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.06)'; notification.style.pointerEvents = 'auto'; notification.style.opacity = '0'; notification.style.transform = 'translateY(-20px)'; notification.style.transition = 'all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55)'; notification.style.display = 'flex'; notification.style.alignItems = 'center'; notification.style.backdropFilter = 'blur(8px)'; const typeStyles = { success: { background: 'linear-gradient(145deg, rgba(104, 214, 156, 0.95), rgba(89, 186, 134, 0.95))', icon: '🎉' }, error: { background: 'linear-gradient(145deg, rgba(248, 113, 113, 0.95), rgba(220, 38, 38, 0.95))', icon: '❌' }, warning: { background: 'linear-gradient(145deg, rgba(251, 191, 36, 0.95), rgba(245, 158, 11, 0.95))', icon: '⚠️' }, info: { background: 'linear-gradient(145deg, rgba(96, 165, 250, 0.95), rgba(59, 130, 246, 0.95))', icon: 'ℹ️' } }; const currentType = typeStyles[type] || typeStyles.info; notification.style.background = currentType.background; notification.style.color = type === 'info' || type === 'success' ? '#fff' : '#000'; const progressBar = document.createElement('div'); progressBar.style.position = 'absolute'; progressBar.style.bottom = '0'; progressBar.style.left = '0'; progressBar.style.height = '4px'; progressBar.style.width = '100%'; progressBar.style.background = 'rgba(255, 255, 255, 0.3)'; progressBar.style.borderRadius = '0 0 12px 12px'; progressBar.style.transition = `width ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`; const icon = document.createElement('span'); icon.style.marginRight = '12px'; icon.style.fontSize = '20px'; icon.textContent = currentType.icon; icon.style.filter = 'saturate(1.2)'; const messageContainer = document.createElement('div'); messageContainer.innerHTML = highlightedMessage; messageContainer.style.flex = '1'; messageContainer.style.fontWeight = 'bold'; const closeBtn = document.createElement('span'); closeBtn.textContent = '×'; closeBtn.style.marginLeft = '12px'; closeBtn.style.fontSize = '24px'; closeBtn.style.cursor = 'pointer'; closeBtn.style.opacity = '0.8'; closeBtn.style.transition = 'opacity 0.2s'; closeBtn.addEventListener('mouseover', () => { closeBtn.style.opacity = '1'; }); closeBtn.addEventListener('mouseout', () => { closeBtn.style.opacity = '0.8'; }); notification.appendChild(icon); notification.appendChild(messageContainer); notification.appendChild(closeBtn); notification.appendChild(progressBar); const container = getNotificationContainer(); container.appendChild(notification); requestAnimationFrame(() => { notification.style.opacity = '1'; notification.style.transform = 'translateY(0)'; requestAnimationFrame(() => { progressBar.style.width = '0'; }); }); requestAnimationFrame(() => { requestAnimationFrame(() => { NotificationAnimator.applyAnimation(notification, animation, true); }); }); function hideNotification(notification) { NotificationAnimator.applyAnimation(notification, animation, false); setTimeout(() => { container.removeChild(notification); if (container.children.length === 0) { document.body.removeChild(container); } }, 300); } closeBtn.addEventListener('click', (e) => { e.stopPropagation(); hideNotification(notification); }); notification.addEventListener('click', () => { hideNotification(notification); }); if (duration > 0) { setTimeout(() => { if (container.contains(notification)) { hideNotification(notification); } }, duration); } } function showConfirmNotification(message, options = {}) { const { animation = 'scale' } = options; return new Promise((resolve) => { const container = getNotificationContainer(); const notification = document.createElement('div'); notification.style.cssText = ` position: relative; margin-bottom: 10px; padding: 15px 20px; border-radius: 12px; color: #333; font-size: 16px; font-weight: bold; box-shadow: 0 8px 16px rgba(0,0,0,0.08); pointer-events: auto; opacity: 0; transform: translateY(-20px); transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); display: flex; flex-direction: column; gap: 10px; background: linear-gradient(145deg, rgba(251, 191, 36, 0.95), rgba(245, 158, 11, 0.95)); backdrop-filter: blur(8px); `; const messageDiv = document.createElement('div'); messageDiv.textContent = message; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; gap: 10px; justify-content: flex-end; `; const confirmBtn = document.createElement('button'); confirmBtn.textContent = '确认'; confirmBtn.style.cssText = ` padding: 6px 12px; border: none; border-radius: 6px; background: #4f46e5; color: white; cursor: pointer; font-weight: bold; transition: all 0.2s ease; `; const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.style.cssText = ` padding: 6px 12px; border: none; border-radius: 6px; background: #ef4444; color: white; cursor: pointer; font-weight: bold; transition: all 0.2s ease; `; [confirmBtn, cancelBtn].forEach(btn => { btn.onmouseover = () => btn.style.filter = 'brightness(110%)'; btn.onmouseout = () => btn.style.filter = 'brightness(100%)'; }); buttonContainer.appendChild(confirmBtn); buttonContainer.appendChild(cancelBtn); notification.appendChild(messageDiv); notification.appendChild(buttonContainer); container.appendChild(notification); requestAnimationFrame(() => { notification.style.opacity = '1'; notification.style.transform = 'translateY(0)'; }); requestAnimationFrame(() => { requestAnimationFrame(() => { NotificationAnimator.applyAnimation(notification, animation, true); }); }); const hideNotification = (result) => { NotificationAnimator.applyAnimation(notification, animation, false); setTimeout(() => { container.removeChild(notification); resolve(result); }, 300); }; confirmBtn.onclick = () => hideNotification(true); cancelBtn.onclick = () => hideNotification(false); }); } function getDeviceFingerprint() { const screenInfo = `${window.screen.width}x${window.screen.height}x${window.screen.colorDepth}`; const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const language = navigator.language; const platform = navigator.platform; return `${screenInfo}-${timeZone}-${language}-${platform}`; } function generateActivationCode(timestamp) { const deviceInfo = getDeviceFingerprint(); const raw = deviceInfo + timestamp; let hash = 0; for (let i = 0; i < raw.length; i++) { const char = raw.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(36).toUpperCase().slice(0, 8); } function verifyActivationCode(userCode) { const currentTimestamp = Math.floor(Date.now() / (1000 * 3600 * 4)); const currentCode = generateActivationCode(currentTimestamp); const previousCode = generateActivationCode(currentTimestamp - 1); return userCode === currentCode || userCode === previousCode; } function promptActivationCode() { const modalOverlay = document.createElement('div'); modalOverlay.style.position = 'fixed'; modalOverlay.style.top = '0'; modalOverlay.style.left = '0'; modalOverlay.style.width = '100%'; modalOverlay.style.height = '100%'; modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.75)'; modalOverlay.style.zIndex = '9999'; modalOverlay.style.display = 'flex'; modalOverlay.style.alignItems = 'center'; modalOverlay.style.justifyContent = 'center'; modalOverlay.style.opacity = '0'; modalOverlay.style.transition = 'opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1)'; modalOverlay.style.backdropFilter = 'blur(8px)'; const modalContainer = document.createElement('div'); modalContainer.style.backgroundColor = '#ffffff'; modalContainer.style.padding = '40px'; modalContainer.style.borderRadius = '20px'; modalContainer.style.boxShadow = '0 20px 50px rgba(0,0,0,0.15), 0 0 20px rgba(0,0,0,0.1)'; modalContainer.style.width = '420px'; modalContainer.style.maxWidth = '90%'; modalContainer.style.textAlign = 'center'; modalContainer.style.position = 'relative'; modalContainer.style.transform = 'scale(0.8) translateY(20px)'; modalContainer.style.transition = 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)'; modalContainer.style.border = '1px solid rgba(255, 255, 255, 0.1)'; const modalHeader = document.createElement('div'); modalHeader.style.marginBottom = '30px'; modalHeader.style.position = 'relative'; const icon = document.createElement('div'); icon.innerHTML = ``; icon.style.marginBottom = '15px'; icon.style.color = '#4CAF50'; const closeButton = document.createElement('button'); closeButton.innerHTML = ``; closeButton.style.position = 'absolute'; closeButton.style.top = '16px'; closeButton.style.right = '16px'; closeButton.style.padding = '8px'; closeButton.style.border = 'none'; closeButton.style.background = 'transparent'; closeButton.style.cursor = 'pointer'; closeButton.style.color = '#9e9e9e'; closeButton.style.transition = 'all 0.2s ease'; closeButton.style.display = 'flex'; closeButton.style.alignItems = 'center'; closeButton.style.justifyContent = 'center'; closeButton.style.width = '32px'; closeButton.style.height = '32px'; closeButton.style.borderRadius = '8px'; closeButton.addEventListener('mouseenter', () => { closeButton.style.background = '#f5f5f5'; closeButton.style.color = '#666'; closeButton.style.transform = 'rotate(90deg)'; }); closeButton.addEventListener('mouseleave', () => { closeButton.style.background = 'transparent'; closeButton.style.color = '#9e9e9e'; closeButton.style.transform = 'rotate(0deg)'; }); closeButton.addEventListener('mousedown', () => { closeButton.style.transform = 'rotate(90deg) scale(0.9)'; }); closeButton.addEventListener('mouseup', () => { closeButton.style.transform = 'rotate(90deg) scale(1)'; }); const title = document.createElement('h2'); title.textContent = '输入激活码'; title.style.fontSize = '24px'; title.style.fontWeight = '600'; title.style.color = '#333'; title.style.margin = '0 0 8px 0'; const subtitle = document.createElement('p'); subtitle.textContent = '请输入激活码以继续使用完整功能'; subtitle.style.color = '#666'; subtitle.style.fontSize = '14px'; subtitle.style.margin = '0'; const infoMessage = document.createElement('p'); infoMessage.innerHTML = '激活码免费获取,主要用于防滥用,请移步我的主页'; infoMessage.style.color = '#666'; infoMessage.style.fontSize = '14px'; infoMessage.style.margin = '10px 0 0 0'; const tipMessage = document.createElement('p'); tipMessage.innerHTML = '据说只要表达对小雅的爱,就可以用上测试功能哦~'; tipMessage.style.color = '#666'; tipMessage.style.fontSize = '14px'; tipMessage.style.margin = '10px 0 0 0'; const inputContainer = document.createElement('div'); inputContainer.style.position = 'relative'; inputContainer.style.marginTop = '25px'; const input = document.createElement('input'); input.type = 'text'; input.placeholder = '请输入激活码'; input.style.width = '100%'; input.style.padding = '15px 20px'; input.style.border = '2px solid #e0e0e0'; input.style.borderRadius = '12px'; input.style.fontSize = '16px'; input.style.backgroundColor = '#f8f9fa'; input.style.transition = 'all 0.3s ease'; input.style.boxSizing = 'border-box'; input.style.outline = 'none'; input.addEventListener('focus', () => { input.style.border = '2px solid #4CAF50'; input.style.backgroundColor = '#ffffff'; input.style.boxShadow = '0 0 0 4px rgba(76, 175, 80, 0.1)'; }); input.addEventListener('blur', () => { input.style.border = '2px solid #e0e0e0'; input.style.backgroundColor = '#f8f9fa'; input.style.boxShadow = 'none'; }); const confirmButton = document.createElement('button'); confirmButton.textContent = '激活'; confirmButton.style.width = '100%'; confirmButton.style.padding = '15px'; confirmButton.style.marginTop = '20px'; confirmButton.style.border = 'none'; confirmButton.style.borderRadius = '12px'; confirmButton.style.backgroundColor = '#4CAF50'; confirmButton.style.color = '#fff'; confirmButton.style.fontSize = '16px'; confirmButton.style.fontWeight = '600'; confirmButton.style.cursor = 'pointer'; confirmButton.style.transition = 'all 0.3s ease'; confirmButton.style.transform = 'translateY(0)'; confirmButton.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.2)'; let isLoading = false; const setLoadingState = (loading) => { isLoading = loading; if (loading) { confirmButton.innerHTML = '验证中...'; confirmButton.style.backgroundColor = '#45a049'; confirmButton.disabled = true; } else { confirmButton.textContent = '激活'; confirmButton.style.backgroundColor = '#4CAF50'; confirmButton.disabled = false; } }; const style = document.createElement('style'); style.textContent = ` .loading { display: inline-block; width: 20px; height: 20px; border: 3px solid rgba(255,255,255,.3); border-radius: 50%; border-top-color: #fff; animation: spin 1s ease-in-out infinite; margin-right: 8px; vertical-align: middle; } @keyframes spin { to { transform: rotate(360deg); } } `; document.head.appendChild(style); modalHeader.appendChild(icon); modalHeader.appendChild(title); modalHeader.appendChild(subtitle); modalHeader.appendChild(infoMessage); modalHeader.appendChild(tipMessage); modalContainer.appendChild(modalHeader); modalContainer.appendChild(closeButton); inputContainer.appendChild(input); modalContainer.appendChild(inputContainer); modalContainer.appendChild(confirmButton); modalOverlay.appendChild(modalContainer); document.body.appendChild(modalOverlay); requestAnimationFrame(() => { modalOverlay.style.opacity = '1'; modalContainer.style.transform = 'scale(1) translateY(0)'; }); function closeModal() { modalOverlay.style.opacity = '0'; modalContainer.style.transform = 'scale(0.8) translateY(20px)'; setTimeout(() => { document.body.removeChild(modalOverlay); document.head.removeChild(style); }, 400); } closeButton.addEventListener('click', () => { closeModal(); showNotification('请输入激活码。', { type: 'warning', keywords: ['激活码'], animation: 'scale' }); }); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { closeModal(); showNotification('请输入激活码。', { type: 'warning', keywords: ['激活码'], animation: 'scale' }); } }); confirmButton.addEventListener('click', () => { const userCode = input.value.trim(); if (userCode) { setLoadingState(true); setTimeout(() => { if (userCode === 'ILOVEXIAOYA') { isActivated = true; localStorage.setItem('isActivated', 'true'); localStorage.setItem('activationTime', Date.now().toString()); localStorage.setItem('activationCode', userCode); showNotification('🌟 授权成功!欢迎回来!页面将于2s后刷新以激活隐藏功能!', { type: 'success', keywords: ['超级权限', '授权成功'], duration: 2000, animation: 'scale' }); closeModal(); setTimeout(() => { location.reload(); }, 2000); getAndStoreAnswers(); } else if (verifyActivationCode(userCode)) { isActivated = true; localStorage.setItem('isActivated', 'true'); localStorage.setItem('activationTime', Date.now().toString()); showNotification('激活成功!', { type: 'success', keywords: ['激活', '成功'], animation: 'scale' }); closeModal(); getAndStoreAnswers(); } else { setLoadingState(false); input.style.border = '2px solid #ff4444'; input.style.backgroundColor = '#fff8f8'; showNotification('激活码不正确或已过期,请重试。', { type: 'error', keywords: ['激活码', '不正确', '过期'], animation: 'scale' }); input.focus(); } }, 500); } else { input.style.border = '2px solid #ff4444'; input.style.backgroundColor = '#fff8f8'; showNotification('请输入激活码。', { type: 'warning', keywords: ['激活码'], animation: 'fadeSlide' }); input.focus(); } }); input.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !isLoading) { confirmButton.click(); } }); } let taskNoticesCache = { groupId: null, data: null, timestamp: null, CACHE_DURATION: 5 * 60 * 1000 }; async function getTaskNotices(groupId) { const now = Date.now(); if ( taskNoticesCache.groupId === groupId && taskNoticesCache.data && (now - taskNoticesCache.timestamp) < taskNoticesCache.CACHE_DURATION ) { return taskNoticesCache.data; } try { const response = await fetch( `${window.location.origin}/api/jx-stat/group/task/queryTaskNotices?group_id=${groupId}&role=1`, { headers: { 'authorization': `Bearer ${getToken()}`, 'content-type': 'application/json; charset=utf-8' } } ); const data = await response.json(); if (!data.success) { throw new Error('获取作业信息失败'); } taskNoticesCache = { groupId, data: data.data, timestamp: now, CACHE_DURATION: taskNoticesCache.CACHE_DURATION }; return data.data; } catch (error) { console.error('获取任务信息失败:', error); return null; } } async function checkAssignmentStatus(groupId, nodeId) { try { const data = await getTaskNotices(groupId); if (!data) return null; const tasks = data.student_tasks || []; const task = tasks.find(t => t.node_id === nodeId); if (task) { const endTime = new Date(task.end_time); const now = new Date(); const isExpired = now > endTime; const isCompleted = task.finish === 2; return { isExpired, isCompleted, canSubmitAfterExpired: task.is_allow_after_submitted, endTime, status: isCompleted ? '已完成' : (isExpired ? '已截止' : '进行中') }; } throw new Error('未找到作业信息'); } catch (error) { console.error('检查作业状态失败:', error); return null; } } async function createRecord(paperId, groupId, token, forceCreate = false) { try { const currentUrl = window.location.href; const nodeId = getNodeIDFromUrl(currentUrl); if (!forceCreate) { const status = await checkAssignmentStatus(groupId, nodeId); if (status) { if (status.isCompleted) { showNotification(`该作业已完成,将不会创建答题记录,仅可查看答案。`, { type: 'warning', keywords: ['已完成'], animation: 'scale' }); return null; } if (status.isExpired) { if (!status.canSubmitAfterExpired) { showNotification(`作业已于 ${status.endTime.toLocaleString()} 截止,且不允许补交,仅可查看答案。`, { type: 'warning', keywords: ['截止', '不允许补交'], animation: 'fadeSlide' }); return null; } showNotification(`作业已于 ${status.endTime.toLocaleString()} 截止,但允许补交。`, { type: 'info', keywords: ['截止', '允许补交'], animation: 'slideRight' }); } } } const response = await fetch(`${window.location.origin}/api/jx-iresource/survey/createRecord`, { method: 'POST', headers: { 'accept': '*/*', 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=UTF-8' }, body: JSON.stringify({ paper_id: paperId, group_id: groupId }), credentials: 'include' }); const data = await response.json(); if (!data.success) { throw new Error(data.message || '创建记录失败'); } return data.data.id; } catch (error) { console.error('创建记录请求失败:', error); throw error; } } async function getAndStoreAnswers() { if (!isActivated) { promptActivationCode(); return; } const taskElement = document.querySelector('#xy_app_content > div.ta-frame > div.ta_panel.ta_panel_group.ta_group > section > section > main > div > div.group-resource-header.flex_panel.hor > div.flex_align_center > div.entry_task_btn'); if (!taskElement) { showNotification('请确保在作业页面操作!', { type: 'warning', keywords: ['作业页面'], animation: 'scale' }); return; } const token = getToken(); if (!token) { showNotification('无法获取token,请确保已登录。', { type: 'error', keywords: ['token', '登录'], animation: 'fadeSlide' }); return; } const currentUrl = window.location.href; const node_id = getNodeIDFromUrl(currentUrl); const group_id = getGroupIDFromUrl(currentUrl); if (!node_id || !group_id) { showNotification('无法获取必要参数,请确保在正确的页面。', { type: 'error', keywords: ['参数'], animation: 'slideRight' }); return; } try { const resourceData = await fetch( `${window.location.origin}/api/jx-iresource/resource/queryResource?node_id=${node_id}`, { headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=utf-8' }, credentials: 'include' } ).then(res => res.json()); if (!resourceData.success) { throw new Error('获取试卷资源失败'); } const paperId = resourceData.data.resource.id; const recordId = await createRecord(paperId, group_id, token); localStorage.setItem('recordId', recordId || ''); localStorage.setItem('groupId', group_id); localStorage.setItem('paperId', paperId); localStorage.setItem('assignmentTitle', resourceData.data.resource.title || '作业答案'); localStorage.setItem('answerData', JSON.stringify(resourceData.data.resource.questions)); showNotification('答案数据获取成功!', { type: 'success', keywords: ['答案', '获取'], animation: 'slideRight' }); return true; } catch (error) { console.error('获取数据失败:', error); showNotification('获取数据失败,请查看控制台。', { type: 'error', keywords: ['获取', '失败'], animation: 'scale' }); return false; } } async function fillAnswers() { const answerData = JSON.parse(localStorage.getItem('answerData')); const recordId = localStorage.getItem('recordId'); const groupId = localStorage.getItem('groupId'); const paperId = localStorage.getItem('paperId'); if (!answerData || !recordId || !groupId || !paperId) { showNotification('缺少必要数据,请先获取答案或检查作业状态。', { type: 'error', keywords: ['数据', '获取', '检查'], animation: 'scale' }); return; } const token = getToken(); if (!token) { showNotification('无法获取token。', { type: 'error', keywords: ['token'], animation: 'slideRight' }); return; } const progress = createProgressBar(); progress.show(); try { let completedCount = 0; const totalQuestions = answerData.length; const batchSize = 10; for (let i = 0; i < answerData.length; i += batchSize) { const batch = answerData.slice(i, i + batchSize); await Promise.all(batch.map(async question => { await submitAnswer(question, recordId, groupId, paperId, token); completedCount++; progress.update(completedCount, totalQuestions); })); } progress.hide(); showNotification('答案填写完成!页面将于0.5s后刷新。', { type: 'success', keywords: ['答案', '填写', '刷新'], animation: 'slideRight' }); setTimeout(() => { location.reload(); }, 500); } catch (error) { progress.hide(); console.error('填写答案失败:', error); showNotification('填写答案失败,请查看控制台。', { type: 'error', keywords: ['填写', '失败'], animation: 'scale' }); } } async function submitAnswer(question, recordId, groupId, paperId, token) { let answer; let extAnswer = ''; switch (question.type) { case 1: { answer = [question.answer_items.find(item => item.answer_checked === 2)?.id]; break; } case 2: { answer = question.answer_items.filter(item => item.answer_checked === 2).map(item => item.id); break; } case 4: { const fillObject = {}; question.answer_items.forEach(item => { fillObject[item.id] = item.answer; }); answer = [fillObject]; break; } case 5: { answer = [question.answer_items.find(item => item.answer_checked === 2)?.id]; break; } case 6: { answer = [question.answer_items[0].answer]; break; } case 9: { if (question.subQuestions && question.subQuestions.length > 0) { for (const subQuestion of question.subQuestions) { await submitAnswer(subQuestion, recordId, groupId, paperId, token); } } return; } case 12: { answer = question.answer_items .sort((a, b) => parseInt(a.answer) - parseInt(b.answer)) .map(item => item.id); break; } case 13: { const matchObject = {}; question.answer_items .filter(item => !item.is_target_opt && item.answer) .forEach(item => { matchObject[item.id] = item.answer; }); if (Object.keys(matchObject).length > 0) { answer = [matchObject]; } else { return; } break; } default: return; } const requestBody = { record_id: recordId, question_id: question.id, answer: answer, ext_answer: extAnswer, group_id: groupId, paper_id: paperId, is_try: 0 }; return fetch(`${window.location.origin}/api/jx-iresource/survey/answer`, { method: 'POST', headers: { 'accept': '*/*', 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=UTF-8' }, body: JSON.stringify(requestBody) }); } async function enableMakeup() { try { const taskElement = document.querySelector('#xy_app_content > div.ta-frame > div.ta_panel.ta_panel_group.ta_group > section > section > main > div > div.group-resource-header.flex_panel.hor > div.flex_align_center > div.entry_task_btn'); if (!taskElement) { showNotification('请确保在作业页面操作!', { type: 'warning', keywords: ['作业页面'], animation: 'scale' }); return; } const currentUrl = window.location.href; const node_id = getNodeIDFromUrl(currentUrl); const group_id = getGroupIDFromUrl(currentUrl); if (!node_id || !group_id) { showNotification('无法获取当前课程信息,请确保在正确的页面', { type: 'error', keywords: ['课程信息'], animation: 'fadeSlide' }); return; } const token = getToken(); if (!token) { showNotification('获取token失败,请重新登录', { type: 'error', keywords: ['token', '登录'], animation: 'slideRight' }); return; } const resourceData = await fetch( `${window.location.origin}/api/jx-iresource/resource/queryResource?node_id=${node_id}`, { headers: { 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=utf-8' } } ).then(res => res.json()); if (!resourceData.success) { throw new Error('获取试卷资源失败'); } const paperId = resourceData.data.resource.id; const status = await checkAssignmentStatus(group_id, node_id); if (status) { if (status.isCompleted) { const confirmed = await showConfirmNotification( `该作业已完成,创建补交记录将导致状态变为"正在答题"。是否继续?`, { type: 'warning', keywords: ['已完成', '补交记录'], animation: 'scale' } ); if (!confirmed) return; } if (status.isExpired && !status.canSubmitAfterExpired) { showNotification(`作业已于 ${status.endTime.toLocaleString()} 截止,且教师设置不允许补交。`, { type: 'error', keywords: ['截止', '不允许'], animation: 'slideRight' }); return; } } const recordId = await createRecord(paperId, group_id, token, true); if (recordId) { localStorage.setItem('recordId', recordId); localStorage.setItem('groupId', group_id); localStorage.setItem('paperId', paperId); localStorage.setItem('assignmentTitle', resourceData.data.resource.title || '作业答案'); localStorage.setItem('answerData', JSON.stringify(resourceData.data.resource.questions)); showNotification('已创建新的答题记录,可以开始补交了。', { type: 'success', keywords: ['补交', '记录'], animation: 'slideRight' }); } } catch (error) { console.error('创建失败:', error); showNotification(`创建记录失败,请刷新页面重试。`, { type: 'error', keywords: ['创建', '失败'], animation: 'scale' }); } } async function submitHomework() { const recordId = localStorage.getItem('recordId'); const groupId = localStorage.getItem('groupId'); if (!recordId || !groupId) { showNotification('未找到记录,请先创建记录并填写答案。', { type: 'error', keywords: ['记录', '创建记录'], animation: 'scale' }); return; } const confirmed = await showConfirmNotification( '确定要提交补交作业吗?提交后将覆盖先前数据。', { type: 'warning', keywords: ['提交', '补交作业', '覆盖'], animation: 'scale' } ); if (!confirmed) return; try { const token = getToken(); if (!token) { showNotification('无法获取令牌,请确保已登录。', { type: 'error', keywords: ['令牌', '登录'], animation: 'scale' }); return; } const response = await fetch(`${window.location.origin}/api/jx-iresource/survey/submit`, { method: 'POST', headers: { 'accept': '*/*', 'authorization': `Bearer ${token}`, 'content-type': 'application/json; charset=UTF-8' }, body: JSON.stringify({ record_id: recordId, group_id: groupId }), credentials: 'include' }); const result = await response.json(); if (result.success) { showNotification('作业提交成功!1s后刷新。', { type: 'success', keywords: ['作业', '提交', '成功'], animation: 'scale' }); setTimeout(() => { location.reload(); }, 1000); } else { throw new Error(result.message || '提交失败'); } } catch (error) { console.error('提交作业失败:', error); showNotification(`提交失败,补交记录已失效,请创建新记录。`, { type: 'error', keywords: ['提交', '失败', '记录'], animation: 'fadeSlide' }); } } function getToken() { const cookies = document.cookie.split('; '); for (let cookie of cookies) { const [name, value] = cookie.split('='); if (name.includes('prd-access-token')) { return value; } } return null; } function parseRichText(content) { try { let jsonContent = JSON.parse(content); let text = ''; jsonContent.blocks.forEach((block) => { text += block.text + '\n'; }); return text.trim(); } catch (e) { return content; } } function parseRichTextForDisplay(content) { try { let jsonContent = JSON.parse(content); let result = ''; jsonContent.blocks.forEach((block) => { if (block.type === 'atomic' && block.data && block.data.type === 'IMAGE') { let imageSrc = block.data.src; let fileIdMatch = imageSrc.match(/\/cloud\/file_access\/(\d+)/); if (fileIdMatch && fileIdMatch[1]) { let fileId = fileIdMatch[1]; let randomParam = Date.now(); let imageUrl = `${window.location.origin}/api/jx-oresource/cloud/file_access/${fileId}?random=${randomParam}`; result += `
欢迎使用 小雅答答答 答题助手! 这里有一些可能帮得上你的信息~
发送邮件至 zygame1314@gmail.com 或访问 我的个人主页
别太依赖脚本哦,多动脑才是真本事!😉
版权 © zygame1314 保留所有权利。
`; tutorialContent.style.fontSize = '16px'; tutorialContent.style.lineHeight = '1.6'; modalContent.style.scrollbarWidth = 'thin'; modalContent.style.scrollbarColor = '#4e4376 #f1f1f1'; const scrollbarStyles = ` .tutorial-modal::-webkit-scrollbar { width: 8px; } .tutorial-modal::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .tutorial-modal::-webkit-scrollbar-thumb { background: #4e4376; border-radius: 4px; } `; style.textContent += scrollbarStyles; modalContent.classList.add('tutorial-modal'); modalContent.appendChild(closeButton); modalContent.appendChild(tutorialContent); modalOverlay.appendChild(modalContent); document.body.appendChild(modalOverlay); setTimeout(() => { modalOverlay.style.opacity = '1'; }, 10); } function getQuestionType(typeCode) { const typeMap = { 1: "单选题", 2: "多选题", 4: "填空题", 5: "判断题", 6: "简答题", 9: "数组题", 12: "排序题", 13: "匹配题" }; return typeMap[typeCode] || "未知题型"; } function createAIButton(answerInput, question) { let aiButton = document.createElement('button'); aiButton.innerHTML = '🤖AI辅助'; aiButton.className = 'ai-assist-btn'; aiButton.title = '使用 AI 生成答案建议'; const style = document.createElement('style'); style.textContent = ` .ai-assist-btn { position: absolute; bottom: -5px; padding: 8px 16px; background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 6px; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(79, 70, 229, 0.1); } .ai-assist-btn:hover { transform: translateY(-1px); background: linear-gradient(135deg, #4338ca 0%, #4f46e5 100%); box-shadow: 0 4px 8px rgba(79, 70, 229, 0.2); } .ai-assist-btn:active { transform: translateY(1px); } .ai-assist-btn.loading { background: #6b7280; cursor: not-allowed; opacity: 0.8; } .ai-assist-btn .icon { font-size: 16px; } .ai-assist-btn.loading .icon { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `; document.head.appendChild(style); let isLoading = false; aiButton.onclick = async () => { if (isLoading) return; try { isLoading = true; aiButton.className = 'ai-assist-btn loading'; aiButton.innerHTML = '🧠生成中...'; const questionType = getQuestionType(question.type); const response = await fetch(`${window.location.origin}/api/jx-oresource/assistant/chat`, { method: "POST", headers: { "content-type": "application/json", "authorization": `Bearer ${getToken()}` }, body: JSON.stringify({ ask_key: "create_answer_analysis", ask_object: { type: questionType, title: question.title, content: parseRichTextToPlainText(question.title), answer: answerInput.value, multilingual_description: ` 请按照以下要求生成【${questionType}】的答案: 1. 使用简体中文 2. 答案要清晰准确,符合题目要求 3. 适当使用专业术语 4. 分点论述,层次分明 5. 避免废话和重复内容 6. 请直接输出纯文本,不要使用特殊格式 7. 如需分点,使用数字加顿号格式 8. 根据【${questionType}】的特点组织答案结构 ` }, token: localStorage.getItem('XY_GLOBAL_CONFIG') ? JSON.parse(localStorage.getItem('XY_GLOBAL_CONFIG')).xy_ai_token : null }) }); const data = await response.json(); if (data.success) { answerInput.value = data.data; answerInput.focus(); answerInput.dispatchEvent(new Event('input', { bubbles: true })); showNotification(`AI已生成${questionType}答案建议,请检查修改`, { type: 'success', keywords: ['AI', questionType, '建议'], animation: 'scale' }); } else { throw new Error(data.message || '请求失败'); } } catch (error) { console.error('AI请求失败:', error); showNotification('AI生成失败: ' + error.message, { type: 'error', keywords: ['AI', '失败'], animation: 'scale' }); } finally { isLoading = false; aiButton.className = 'ai-assist-btn'; aiButton.innerHTML = '🤖AI辅助'; } }; return aiButton; } function showAnswerEditor() { let storedData = localStorage.getItem('answerData'); if (!storedData) { showNotification('未找到存储的答案数据,请先点击"获取答案"按钮。', { type: 'error', keywords: ['存储', '答案', '获取'], animation: 'fadeSlide' }); return; } let answerData = JSON.parse(storedData); let overlay = document.createElement('div'); let modalContainer = document.createElement('div'); let resizeHandle = document.createElement('div'); let dragHandle = document.createElement('div'); let closeButton = document.createElement('button'); let modalContentWrapper = document.createElement('div'); let title = document.createElement('h2'); let saveButton = document.createElement('button'); overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'transparent'; overlay.style.pointerEvents = 'none'; overlay.style.zIndex = '9999'; overlay.style.opacity = '0'; overlay.style.transition = 'opacity 0.3s ease-in-out'; modalContainer.id = 'modal-container'; modalContainer.style.cssText = ` position: fixed; top: 50%; left: 50%; z-index: 10000; width: 90%; max-width: 800px; height: 85vh; min-width: 400px; background-color: #ffffff; border-radius: 20px; padding: 48px 32px 32px 32px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); opacity: 0; transition: opacity 0.3s ease; display: flex; flex-direction: column; `; resizeHandle.style.cssText = ` position: absolute; right: 2px; bottom: 2px; width: 20px; height: 20px; cursor: nw-resize; border-radius: 0 0 18px 0; background: linear-gradient(135deg, transparent 25%, #e2e8f0 25%, #e2e8f0 37%, #6366f1 37%, #6366f1 50%, transparent 50%, transparent 62%, #6366f1 62%, #6366f1 75%, transparent 75% ); opacity: 0.6; `; resizeHandle.addEventListener('mouseenter', () => { resizeHandle.style.opacity = '1'; resizeHandle.style.transform = 'scale(1.1)'; }); resizeHandle.addEventListener('mouseleave', () => { resizeHandle.style.opacity = '0.6'; resizeHandle.style.transform = 'scale(1)'; }); let isResizing = false; let originalWidth, originalHeight, originalX, originalY; resizeHandle.addEventListener('mousedown', (e) => { isResizing = true; const rect = modalContainer.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalX = e.clientX; originalY = e.clientY; modalContainer.style.transform = 'none'; modalContainer.style.top = rect.top + 'px'; modalContainer.style.left = rect.left + 'px'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isResizing) return; const newWidth = originalWidth + (e.clientX - originalX); const newHeight = originalHeight + (e.clientY - originalY); const minWidth = 400; const minHeight = 300; if (newWidth >= minWidth) { modalContainer.style.width = newWidth + 'px'; } if (newHeight >= minHeight) { modalContainer.style.height = newHeight + 'px'; } }); document.addEventListener('mouseup', () => { isResizing = false; }); dragHandle.style.cssText = ` position: absolute; top: 0; left: 0; right: 0; height: 48px; cursor: move; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; background: linear-gradient(to right, rgba(243, 244, 246, 0.95), rgba(243, 244, 246, 0.5)); border-radius: 20px 20px 0 0; user-select: none; transition: all 0.3s ease; `; dragHandle.innerHTML = `该题型暂不支持查看答案
`; questionContainer.appendChild(questionTitle); questionContainer.appendChild(notSupportedMessage); } if (question.description && question.description !== '{}') { let toggleDescriptionContainer = document.createElement('div'); toggleDescriptionContainer.style.cssText = ` margin: 24px 0; position: relative; `; let toggleDescriptionButton = document.createElement('button'); toggleDescriptionButton.style.cssText = ` display: flex; align-items: center; justify-content: center; font-size: 16px; color: #2563eb; background: #fff; border: 1px solid #e5e7eb; border-bottom: none; border-radius: 8px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; cursor: pointer; padding: 12px 16px; width: 100%; transition: background-color 0.3s, box-shadow 0.3s; position: relative; `; toggleDescriptionButton.innerHTML = ` 查看解析 `; let descriptionContainer = document.createElement('div'); descriptionContainer.style.cssText = ` max-height: 0; overflow: hidden; transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1); background-color: #f9fafb; border: 1px solid #e5e7eb; border-top: none; border-radius: 8px; border-top-left-radius: 0; border-top-right-radius: 0; margin-top: 0; `; let descriptionContent = document.createElement('div'); descriptionContent.style.cssText = ` padding: 24px; color: #111827; font-size: 16px; line-height: 1.8; `; descriptionContent.innerHTML = parseRichTextForDisplay(question.description); descriptionContainer.appendChild(descriptionContent); let isDescriptionVisible = false; toggleDescriptionButton.onclick = () => { isDescriptionVisible = !isDescriptionVisible; let svgIcon = toggleDescriptionButton.querySelector('svg'); let textLabel = toggleDescriptionButton.querySelector('span > span'); if (isDescriptionVisible) { descriptionContainer.style.maxHeight = descriptionContent.scrollHeight + 'px'; svgIcon.style.transform = 'rotate(180deg)'; textLabel.textContent = '收起解析'; toggleDescriptionButton.style.backgroundColor = '#ebf5ff'; toggleDescriptionButton.style.boxShadow = 'inset 0 3px 6px rgba(0,0,0,0.1)'; } else { descriptionContainer.style.maxHeight = '0'; svgIcon.style.transform = 'rotate(0deg)'; textLabel.textContent = '查看解析'; toggleDescriptionButton.style.backgroundColor = '#fff'; toggleDescriptionButton.style.boxShadow = 'none'; } }; toggleDescriptionContainer.appendChild(toggleDescriptionButton); toggleDescriptionContainer.appendChild(descriptionContainer); questionContainer.appendChild(toggleDescriptionContainer); } content.appendChild(questionContainer); questionContainers.push(questionContainer); }); modalContainer.appendChild(resizeHandle); modalContainer.appendChild(dragHandle); modalContainer.appendChild(closeButton); modalContainer.appendChild(title); modalContainer.appendChild(saveButton); modalContainer.appendChild(modalContentWrapper); modalContentWrapper.appendChild(tocContainer); modalContentWrapper.appendChild(content); document.body.appendChild(overlay); document.body.appendChild(modalContainer); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(); } }); function updateCurrentQuestionHighlight() { const contentRect = content.getBoundingClientRect(); const viewportTop = contentRect.top; const viewportHeight = contentRect.height; const viewportCenter = viewportTop + (viewportHeight / 2); let currentQuestionIndex = -1; let minDistance = Infinity; questionContainers.forEach((qc, index) => { const qcRect = qc.getBoundingClientRect(); const qcCenter = qcRect.top + (qcRect.height / 2); const distance = Math.abs(qcCenter - viewportCenter); if (distance < minDistance) { minDistance = distance; currentQuestionIndex = index; } }); if (currentQuestionIndex !== -1) { tocLinks.forEach((tocLink, idx) => { if (idx === currentQuestionIndex) { tocLink.isActive = true; tocLink.style.backgroundColor = '#6366f1'; tocLink.style.color = '#ffffff'; tocLink.style.transform = 'scale(1.05)'; tocLink.style.boxShadow = '0 4px 6px -1px rgba(99, 102, 241, 0.1)'; } else { tocLink.isActive = false; tocLink.style.backgroundColor = '#f3f4f6'; tocLink.style.color = '#1f2937'; tocLink.style.transform = 'scale(1)'; tocLink.style.boxShadow = 'none'; } }); } } content.addEventListener('scroll', () => { requestAnimationFrame(updateCurrentQuestionHighlight); }); setTimeout(updateCurrentQuestionHighlight, 100); requestAnimationFrame(() => { overlay.style.opacity = '1'; modalContainer.style.transform = 'translate(-50%, -50%) scale(1)'; modalContainer.style.opacity = '1'; updateCurrentQuestionHighlight(); }); } async function exportHomework() { console.log('exportHomework function called'); let storedData = localStorage.getItem('answerData'); if (!storedData) { console.error('未找到存储的答案数据,请先获取并存储答案。'); showNotification('未找到存储的答案数据,请先点击"获取答案"按钮。', { type: 'error', keywords: ['存储', '答案', '获取'], animation: 'fadeSlide' }); return; } const answerData = JSON.parse(storedData); let assignmentTitle = localStorage.getItem('assignmentTitle') || '作业答案'; const progress = createProgressBar(); progress.show(); let completedCount = 0; const totalQuestions = answerData.length; try { const docContent = []; showNotification('开始导出作业...', { type: 'info', keywords: ['导出', '开始'], animation: 'scale' }); docContent.push( new Paragraph({ text: assignmentTitle, heading: HeadingLevel.TITLE, alignment: AlignmentType.CENTER, spacing: { after: 400 }, }), new Paragraph({ text: `导出时间:${new Date().toLocaleString()}`, alignment: AlignmentType.CENTER, spacing: { after: 400 }, }) ); for (let index = 0; index < answerData.length; index++) { const question = answerData[index]; const questionNumber = `${index + 1}、`; const titleRuns = await parseRichTextContent(question.title); const titleParagraph = new Paragraph({ children: [ new TextRun({ text: questionNumber, bold: true, }), ...titleRuns, ], }); docContent.push(titleParagraph); switch (question.type) { case 1: case 2: { const options = question.answer_items.map((item, idx) => { const optionLetter = String.fromCharCode(65 + idx); return { letter: optionLetter, content: item.value, }; }); for (const option of options) { const optionRuns = await parseRichTextContent(option.content); const optionParagraph = new Paragraph({ children: [ new TextRun({ text: `${option.letter}. `, bold: true, }), ...optionRuns, ], }); docContent.push(optionParagraph); } const correctOptions = question.answer_items .map((item, idx) => item.answer_checked === 2 ? String.fromCharCode(65 + idx) : null) .filter(item => item !== null) .join(''); docContent.push( new Paragraph({ text: `答案:${correctOptions}`, spacing: { before: 100, after: 100 }, }) ); if (question.description && question.description !== '{}') { const descriptionRuns = await parseRichTextContent(question.description); const descriptionParagraph = new Paragraph({ children: [ new TextRun({ text: '解析:', bold: true, }), ...descriptionRuns, ], spacing: { before: 100, after: 100 }, }); docContent.push(descriptionParagraph); } break; } case 5: { const isCorrect = question.answer_items.some(item => item.answer_checked === 2 && (item.value === '正确' || item.value.toLowerCase() === 'true')); const answerText = isCorrect ? '对' : '错'; docContent.push( new Paragraph({ text: `答案:${answerText}`, spacing: { before: 100, after: 100 }, }) ); if (question.description && question.description !== '{}') { const descriptionRuns = await parseRichTextContent(question.description); const descriptionParagraph = new Paragraph({ children: [ new TextRun({ text: '解析:', bold: true, }), ...descriptionRuns, ], spacing: { before: 100, after: 100 }, }); docContent.push(descriptionParagraph); } break; } case 4: { const blankCount = question.answer_items.length; let blanks = ''; for (let i = 0; i < blankCount; i++) { blanks += '(____)'; } docContent.push( new Paragraph({ text: blanks, spacing: { before: 100, after: 100 }, }) ); const answers = question.answer_items.map(item => parseRichTextToPlainText(item.answer)).join('|'); docContent.push( new Paragraph({ text: `答案:${answers}`, spacing: { before: 100, after: 100 }, }) ); if (question.description && question.description !== '{}') { const descriptionRuns = await parseRichTextContent(question.description); const descriptionParagraph = new Paragraph({ children: [ new TextRun({ text: '解析:', bold: true, }), ...descriptionRuns, ], spacing: { before: 100, after: 100 }, }); docContent.push(descriptionParagraph); } break; } case 6: { const answers = question.answer_items.map(item => parseRichTextToPlainText(item.answer)).join(';'); docContent.push( new Paragraph({ text: `答案:${answers}`, spacing: { before: 100, after: 100 }, }) ); if (question.description && question.description !== '{}') { const descriptionRuns = await parseRichTextContent(question.description); const descriptionParagraph = new Paragraph({ children: [ new TextRun({ text: '解析:', bold: true, }), ...descriptionRuns, ], spacing: { before: 100, after: 100 }, }); docContent.push(descriptionParagraph); } break; } case 9: { if (question.subQuestions && question.subQuestions.length > 0) { for (let subIndex = 0; subIndex < question.subQuestions.length; subIndex++) { const subQuestion = question.subQuestions[subIndex]; const subQuestionNumber = `${index + 1}.${subIndex + 1}、`; const subTitleRuns = await parseRichTextContent(subQuestion.title); docContent.push( new Paragraph({ children: [ new TextRun({ text: subQuestionNumber, bold: true, }), ...subTitleRuns ], spacing: { before: 200 } }) ); switch (subQuestion.type) { case 1: case 2: { for (const [idx, item] of question.answer_items.entries()) { const optionLetter = String.fromCharCode(65 + idx); const optionRuns = await parseRichTextContent(item.value); const optionParagraph = new Paragraph({ children: [ new TextRun({ text: `${optionLetter}. `, bold: true, }), ...optionRuns, ], }); docContent.push(optionParagraph); } const correctOptions = question.answer_items .map((item, idx) => item.answer_checked === 2 ? String.fromCharCode(65 + idx) : null) .filter(item => item !== null) .join(''); docContent.push( new Paragraph({ text: `答案:${correctOptions}`, spacing: { before: 100, after: 100 }, }) ); break; } case 4: { const blankCount = subQuestion.answer_items.length; let blanks = ''; for (let i = 0; i < blankCount; i++) { blanks += '(____)'; } docContent.push( new Paragraph({ text: blanks, spacing: { before: 100, after: 100 } }) ); const answers = subQuestion.answer_items .map(item => parseRichTextToPlainText(item.answer)) .join('|'); docContent.push( new Paragraph({ text: `答案:${answers}`, spacing: { before: 100, after: 100 } }) ); break; } case 5: { const isCorrect = subQuestion.answer_items .some(item => item.answer_checked === 2 && (item.value === '正确' || item.value.toLowerCase() === 'true')); const answerText = isCorrect ? '对' : '错'; docContent.push( new Paragraph({ text: `答案:${answerText}`, spacing: { before: 100, after: 100 } }) ); break; } case 6: { const answers = subQuestion.answer_items .map(item => parseRichTextToPlainText(item.answer)) .join(';'); docContent.push( new Paragraph({ text: `答案:${answers}`, spacing: { before: 100, after: 100 } }) ); break; } } if (subQuestion.description && subQuestion.description !== '{}') { const descriptionRuns = await parseRichTextContent(subQuestion.description); docContent.push( new Paragraph({ children: [ new TextRun({ text: '解析:', bold: true }), ...descriptionRuns ], spacing: { before: 100, after: 100 } }) ); } docContent.push( new Paragraph({ text: '', spacing: { after: 200 } }) ); } } break; } case 12: { const options = question.answer_items.map((item, idx) => { const optionLetter = String.fromCharCode(65 + idx); return { letter: optionLetter, content: item.value, originalIndex: idx, }; }); for (const option of options) { const optionRuns = await parseRichTextContent(option.content); const optionParagraph = new Paragraph({ children: [ new TextRun({ text: `${option.letter}. `, bold: true, }), ...optionRuns, ], }); docContent.push(optionParagraph); } const sortedItems = question.answer_items.slice().sort((a, b) => parseInt(a.answer) - parseInt(b.answer)); const answerLetters = sortedItems.map(item => { const originalIndex = question.answer_items.indexOf(item); return String.fromCharCode(65 + originalIndex); }).join(''); docContent.push( new Paragraph({ text: `答案:${answerLetters}`, spacing: { before: 100, after: 100 }, }) ); if (question.description && question.description !== '{}') { const descriptionRuns = await parseRichTextContent(question.description); const descriptionParagraph = new Paragraph({ children: [ new TextRun({ text: '解析:', bold: true, }), ...descriptionRuns, ], spacing: { before: 100, after: 100 }, }); docContent.push(descriptionParagraph); } break; } case 13: { const leftItems = question.answer_items.filter(item => !item.is_target_opt); const rightItems = question.answer_items.filter(item => item.is_target_opt); docContent.push(new Paragraph({ text: "左侧选项:" })); leftItems.forEach((leftItem, index) => { const leftContent = parseRichTextToPlainText(leftItem.value); docContent.push(new Paragraph({ text: `左${index + 1}:${leftContent}`, })); }); docContent.push(new Paragraph({ text: "右侧选项:" })); rightItems.forEach((rightItem, index) => { const rightContent = parseRichTextToPlainText(rightItem.value); docContent.push(new Paragraph({ text: `右${index + 1}:${rightContent}`, })); }); const answerText = '答案:' + leftItems.map((leftItem, leftIndex) => { const leftOptionNumber = `左${leftIndex + 1}`; const matchedRightIds = leftItem.answer ? leftItem.answer.toString().split(',') : []; const matchedRightNumbers = matchedRightIds.map((id) => { const rightIndex = rightItems.findIndex(item => item.id === id); return rightIndex >= 0 ? `右${rightIndex + 1}` : ''; }).join('、'); return `${leftOptionNumber} - ${matchedRightNumbers}`; }).join('|'); docContent.push( new Paragraph({ text: answerText, spacing: { before: 100, after: 100 }, }) ); if (question.description && question.description !== '{}') { const descriptionRuns = await parseRichTextContent(question.description); const descriptionParagraph = new Paragraph({ children: [ new TextRun({ text: '解析:', bold: true, }), ...descriptionRuns, ], spacing: { before: 100, after: 100 }, }); docContent.push(descriptionParagraph); } break; } default: { docContent.push(new Paragraph({ text: "该题型暂不支持查看答案。", spacing: { before: 100, after: 100 }, })); if (question.description && question.description !== '{}') { const descriptionRuns = await parseRichTextContent(question.description); const descriptionParagraph = new Paragraph({ children: [ new TextRun({ text: '解析:', bold: true, }), ...descriptionRuns, ], spacing: { before: 100, after: 100 }, }); docContent.push(descriptionParagraph); } break; } } completedCount++; progress.update(completedCount, totalQuestions, '正在导出'); docContent.push(new Paragraph({ text: "", spacing: { after: 200 } })); } const doc = new Document({ creator: "小雅答答答", description: `导出的作业答案 - ${assignmentTitle}`, title: assignmentTitle, numbering: { config: [ { reference: "default", levels: [ { level: 0, format: "decimal", text: "%1.", alignment: AlignmentType.START, }, ], }, ], }, styles: { default: { document: { run: { font: "Microsoft YaHei", size: 24, }, }, }, paragraphStyles: [ { id: "Normal", name: "Normal", run: { font: "Microsoft YaHei", size: 24, }, paragraph: { spacing: { line: 360, before: 0, after: 0 }, }, }, { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, run: { font: "Microsoft YaHei", size: 32, bold: true, }, paragraph: { spacing: { before: 240, after: 120 }, }, }, ], }, sections: [ { properties: { page: { margin: { top: 720, right: 720, bottom: 720, left: 720 }, size: { width: 11906, height: 16838, }, }, }, children: docContent, }, ], compatibility: { doNotExpandShiftReturn: true, useWord2013TrackBottomHyphenation: true, compatibilityMode: 15, useFELayout: true, }, settings: { compatibility: { useFELayout: true, useNormalStyleForList: true, doNotUseIndentAsNumberingTabStop: true, balanceSingleByteDoubleByteWidth: true } } }); await Packer.toBlob(doc).then((blob) => { let safeTitle = assignmentTitle.replace(/[\\/:*?"<>|]/g, '_'); window.saveAs(blob, `${safeTitle}.docx`); progress.hide(); showNotification('作业导出成功,如需导入其他题库,请手动编辑保存一次以确保被准确识别。', { type: 'success', keywords: ['导出', '成功', '题库'], animation: 'fadeSlide' }); }).catch((error) => { progress.hide(); console.error('导出失败:', error); showNotification('导出失败,请查看控制台日志。', { type: 'error', keywords: ['导出', '失败', '日志'], animation: 'scale' }); }); } catch (error) { progress.hide(); console.error('导出作业时出错:', error); showNotification('导出失败,请查看控制台日志。', { type: 'error', keywords: ['导出', '失败', '日志'], animation: 'scale' }); } } async function parseRichTextContent(content) { let result = []; try { let jsonContent = JSON.parse(content); for (const block of jsonContent.blocks) { if (block.type === 'atomic' && block.data && block.data.type === 'IMAGE') { let imageSrc = block.data.src; let fileIdMatch = imageSrc.match(/\/cloud\/file_access\/(\d+)/); if (fileIdMatch && fileIdMatch[1]) { let fileId = fileIdMatch[1]; let randomParam = Date.now(); let imageUrl = `${window.location.origin}/api/jx-oresource/cloud/file_access/${fileId}?random=${randomParam}`; const imageData = await fetchImageData(imageUrl); if (imageData) { const imageSize = await getImageSize(imageData); if (imageSize) { let { width, height } = imageSize; const maxWidth = 500; if (width > maxWidth) { const ratio = maxWidth / width; width = maxWidth; height = height * ratio; } result.push( new ImageRun({ data: imageData, transformation: { width: width, height: height, }, }) ); } else { result.push(new TextRun('[无法加载图片]')); } } else { result.push(new TextRun('[无法加载图片]')); } } else { result.push(new TextRun('[无法解析图片链接]')); } } else if (block.text) { result.push(new TextRun({ text: block.text, font: "Microsoft YaHei", eastAsia: "Microsoft YaHei" })); } } } catch (e) { const sanitizedContent = content.replace(/[\x00-\x1F\x7F\u200B-\u200D\uFEFF]/g, ''); if (sanitizedContent) { result.push(new TextRun({ text: sanitizedContent, font: "Microsoft YaHei" })); } } return result; } function parseRichTextToPlainText(content) { try { let jsonContent = JSON.parse(content); let result = ''; jsonContent.blocks.forEach((block) => { result += block.text + '\n'; }); return result.trim(); } catch (e) { return content; } } async function getImageSize(imageData) { return new Promise((resolve, reject) => { const blob = new Blob([imageData]); const url = URL.createObjectURL(blob); const img = new Image(); img.onload = function () { const width = img.width; const height = img.height; URL.revokeObjectURL(url); resolve({ width, height }); }; img.onerror = function () { URL.revokeObjectURL(url); reject(new Error('Cannot load image')); }; img.src = url; }); } async function fetchImageData(url) { try { const response = await fetch(url, { method: 'GET' }); if (response.ok) { const blob = await response.blob(); return await blob.arrayBuffer(); } else { console.error('获取图片失败:', response.statusText); return null; } } catch (error) { console.error('fetchImageData Error:', error); return null; } } function checkAndExecuteAuto() { if (isProcessing) { return; } if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(async () => { const taskElement = document.querySelector('#xy_app_content > div.ta-frame > div.ta_panel.ta_panel_group.ta_group > section > section > main > div > div.group-resource-header.flex_panel.hor > div.flex_align_center > div.entry_task_btn'); if (taskElement && autoFetchEnabled) { if (!isActivated) { showNotification('请先激活后再使用自动功能', { type: 'warning', keywords: ['激活', '自动功能'], animation: 'slideRight' }); debounceTimer = null; return; } try { isProcessing = true; showNotification('正在自动获取答案...', { type: 'info', keywords: ['自动', '获取', '答案'], animation: 'fadeSlide' }); await getAndStoreAnswers(); if (autoFillEnabled) { await new Promise(resolve => setTimeout(resolve, 1000)); await fillAnswers(); } } catch (error) { console.error('自动执行出错:', error); } finally { isProcessing = false; debounceTimer = null; } } else { debounceTimer = null; } }, 500); } function detectPageChange() { let lastUrl = location.href; const observer = new MutationObserver(() => { if (location.href !== lastUrl) { lastUrl = location.href; isProcessing = false; if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = null; } setTimeout(() => { checkAndExecuteAuto(); }, 1000); } }); observer.observe(document, { subtree: true, childList: true }); checkAndExecuteAuto(); } detectPageChange(); })();