feat: 工具链就绪(bleak/pillow/pytest/capstone/androguard) + Web协议提取报告

This commit is contained in:
agent committed 2026-07-24 09:23:28 +00:00
1 parent 30571dc0a5
commit cb7eee312f
11 files changed
+8006

No files matched your search

+943
View File
@@ -0,0 +1,943 @@
(function(){
const canvas = document.getElementById('unifiedCanvas');
const ctx = canvas.getContext('2d');
// 获取当前选中的屏幕尺寸
const screenSelect = document.getElementById('epdwh');
let W = 800, H = 480;
function updateCanvasSize() {
const [newW, newH] = screenSelect.value.split('x').map(Number);
W = newW;
H = newH;
canvas.width = W;
canvas.height = H;
}
// 初始化尺寸
updateCanvasSize();
let currentTemplate = 'schedule';
let globalTitle = "◉ 输入内容·页眉";
let presetLinesData = [];
let hitAreas = [];
let courseData = {
days: ["周一", "周二", "周三", "周四", "周五"],
periods: ["第1节", "第2节", "第3节", "第4节"],
cells: {}
};
function initDefaultCourse() {
courseData.cells = {};
const defaults = {
"0,0": "语文", "0,1": "数学", "0,2": "英语", "0,3": "体育",
"1,0": "数学", "1,1": "语文", "1,2": "物理", "1,3": "历史",
"2,0": "英语", "2,1": "化学", "2,2": "语文", "2,3": "自习",
"3,0": "物理", "3,1": "英语", "3,2": "数学", "3,3": "政治",
"4,0": "历史", "4,1": "体育", "4,2": "化学", "4,3": "班会"
};
for(let k in defaults) courseData.cells[k] = defaults[k];
}
initDefaultCourse();
let courseHitAreas = [];
function applyCourseCustomSettings() {
const dayCount = parseInt(document.getElementById('courseDaysCount').value) || 5;
const periodCount = parseInt(document.getElementById('coursePeriodsCount').value) || 4;
const dayNames = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
courseData.days = dayNames.slice(0, dayCount);
courseData.periods = [];
for(let i=1; i<=periodCount; i++) {
courseData.periods.push(`第${i}节`);
}
courseData.cells = {};
render();
}
let qrText = "https://mspppclock.tech/";
let qrSize = 150;
let qrCodeImage = null;
let qrCaptionLines = [];
let qrCaptionStyle = { fontSize: 14, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "center" };
function syncCaptionFromTextarea() {
const raw = document.getElementById('qrCaptionText').value;
const lines = raw.split(/\r?\n/).filter(l => l.trim() !== "" || l === "");
qrCaptionLines = lines.length ? lines : ["(点击添加文字)"];
}
function updateCaptionLinesToUI() {
document.getElementById('qrCaptionText').value = qrCaptionLines.join("\n");
}
async function generateQRCodeImage(text, size=90, colorDark="#000000", colorLight="#FFFFFF") {
return new Promise((resolve) => {
const tempDiv = document.createElement('div');
tempDiv.style.position = 'absolute';
tempDiv.style.left = '-9999px';
document.body.appendChild(tempDiv);
try {
const qrcode = new QRCode(tempDiv, {
text: text,
width: size,
height: size,
colorDark: colorDark,
colorLight: colorLight,
correctLevel: QRCode.CorrectLevel.M
});
setTimeout(() => {
const canvasQR = tempDiv.querySelector('canvas');
if (canvasQR) {
const imgData = canvasQR.toDataURL();
resolve(imgData);
} else resolve(null);
document.body.removeChild(tempDiv);
}, 60);
} catch(e) { resolve(null); document.body.removeChild(tempDiv); }
});
}
async function refreshQRImage() {
qrSize = parseInt(document.getElementById('qrSizeInput').value) || 84;
const img = await generateQRCodeImage(qrText, qrSize, "#000", "#fff");
qrCodeImage = img;
if(currentTemplate === 'qrcode') render();
}
let customRawText = "小雨在放学路上捡到一枚【闪闪发亮的硬币】。\n她买了两颗【橘子味的糖果】,一颗塞进嘴里,\n甜得她眯起了眼睛;另一颗放在路边的长椅上。\n一只【唱歌的小鸟】飞过来,歪着脑袋看了看\n,叼起糖果飞走了。小雨有点失落,\n但还是继续往家走。第二天早上,她推开窗户,\n发现窗台上放着一朵【金黄色的小野花】,花瓣上还挂着露珠。小雨捧着花笑了:\n“原来快乐会【越分越多】呀。”\n从那天起,她每天都往长椅上放一颗糖。";
let customStyle = {
fontSize: 17, fontFamily: "宋体", baseColor: "#000000", invertMode: "none", alignment: "center"
};
const chineseFonts = ["宋体","微软雅黑","黑体","楷体","思源黑体","苹方"];
const invertModes = [
{ value: "none", label: "无" }, { value: "red-black", label: "红字黑底" }, { value: "red-white", label: "红字白底" },
{ value: "black-red", label: "黑字红底" }, { value: "black-white", label: "黑字白底" },
{ value: "white-red", label: "白字红底" }, { value: "white-black", label: "白字黑底" }
];
function applyInvertStyle(baseColor, invertMode) {
if (invertMode === "none") return { bgColor: null, textColor: baseColor };
if (invertMode === "red-black") return { bgColor: "#000000", textColor: "#FF0000" };
if (invertMode === "red-white") return { bgColor: "#FFFFFF", textColor: "#FF0000" };
if (invertMode === "black-red") return { bgColor: "#FF0000", textColor: "#000000" };
if (invertMode === "black-white") return { bgColor: "#FFFFFF", textColor: "#000000" };
if (invertMode === "white-red") return { bgColor: "#FF0000", textColor: "#FFFFFF" };
if (invertMode === "white-black") return { bgColor: "#000000", textColor: "#FFFFFF" };
return { bgColor: null, textColor: baseColor };
}
const templatePresets = {
schedule: [ { text: "✅ 今日核心议程", fontSize: 24, fontFamily: "微软雅黑", baseColor: "#FF0000", invertMode: "none", alignment: "center" },
{ text: "【09:30】 敏捷站会 · 【需求对齐】", fontSize: 18,fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "left" },
{ text: "11:00 前端架构评审会议", fontSize: 18, fontFamily: "黑体", baseColor: "#000000", invertMode: "black-red", alignment: "left" },
{ text: "14:30 跨部门协作同步会", fontSize: 18, fontFamily: "微软雅黑", baseColor: "#FF0000", invertMode: "red-white", alignment: "left" },
{ text: "16:45 代码审查 + 项目复盘", fontSize: 18, fontFamily: "楷体", baseColor: "#000000", invertMode: "none", alignment: "left" }
],
businesscard: [ { text: "✦ 名 片 精 选 ✦", fontSize: 24, fontFamily: "微软雅黑", baseColor: "#FF0000", invertMode: "none", alignment: "center" },
{ text: "欧阳 静澜", fontSize: 26, fontFamily: "黑体", baseColor: "#000000", invertMode: "black-white", alignment: "center" },
{ text: "创意设计总监 / 合伙人", fontSize: 18, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "center" },
{ text: "jinglan@art.cn", fontSize: 17, fontFamily: "宋体", baseColor: "#FF0000", invertMode: "red-black", alignment: "center" },
{ text: "三色哲学 · 红黑白", fontSize: 16, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "center" }
],
memo: [ { text: "📋 紧 急 备 忘 录", fontSize: 28, fontFamily: "微软雅黑", baseColor: "#FF0000", invertMode: "none", alignment: "center" },
{ text: "• 季度总结报告 (周五截止)", fontSize: 24, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "black-red", alignment: "left" },
{ text: "• 设计评审会议 14:00", fontSize: 24, fontFamily: "宋体", baseColor: "#000000", invertMode: "none", alignment: "left" },
{ text: "• 前端组件库升级计划", fontSize: 24, fontFamily: "黑体", baseColor: "#FF0000", invertMode: "red-white", alignment: "left" },
{ text: "• 团队建设活动策划", fontSize: 23, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "left" }
]
};
function loadPresetData(templateId) {
if(templatePresets[templateId]) presetLinesData = JSON.parse(JSON.stringify(templatePresets[templateId]));
else presetLinesData = [{ text: "示例行", fontSize: 16, fontFamily: "微软雅黑", baseColor: "#000000", invertMode: "none", alignment: "left" }];
}
function renderCourseTable() {
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, W, H);
const isSmallScreen = (W === 296 && H === 128);
const isLargeScreen = (W === 800 && H === 480);
if (!isSmallScreen) {
// 显示页眉
const headerFontSize = isLargeScreen ? 36 : 18;
ctx.strokeStyle = "#000000";
ctx.lineWidth = 2;
ctx.strokeRect(4, 4, W-8, H-8);
ctx.fillStyle = "#FF0000";
ctx.fillRect(8, 8, 26, 4);
ctx.fillStyle = "#000000";
ctx.fillRect(W-54, 12, 42, 4);
let title = globalTitle.trim() || "输入学校班级学期等信息";
// 页眉标题
ctx.font = `bold ${headerFontSize}px '微软雅黑'`;
ctx.fillStyle = "#FF0000";
ctx.fillText(title, 20, headerFontSize + 12);
ctx.beginPath();
ctx.strokeStyle = "#FF0000";
ctx.moveTo(12, headerFontSize + 22);
ctx.lineTo(W-12, headerFontSize + 22);
ctx.stroke();
var startY = headerFontSize + 30;
} else {
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 2;
ctx.strokeRect(4, 4, W-8, H-8);
var startY = 8;
}
const dayCount = courseData.days.length;
const periodCount = courseData.periods.length;
const startX = 12;
const cellW = (W - 24) / (dayCount + 1);
const cellH = (H - startY - 8) / (periodCount + 1);
const headerTextFontSize = isLargeScreen ? 30 : 15;
const cellTextFontSize = isLargeScreen ? 28 : (isSmallScreen ? 11 : 14);
// 绘制节次表头
ctx.font = `bold ${headerTextFontSize}px '微软雅黑'`;
ctx.fillStyle = "#000000";
ctx.fillRect(startX, startY, cellW, cellH);
ctx.fillStyle = "#FF0000";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("节次", startX + cellW/2, startY + cellH/2);
// 绘制星期表头
for(let d=0; d<dayCount; d++) {
ctx.fillStyle = d%2===0 ? "#000000" : "#FF0000";
ctx.fillRect(startX + (d+1)*cellW, startY, cellW, cellH);
ctx.fillStyle = "#FFFFFF";
ctx.font = `bold ${headerTextFontSize}px '微软雅黑'`;
ctx.fillText(courseData.days[d], startX + (d+1)*cellW + cellW/2, startY + cellH/2);
}
courseHitAreas = [];
// 绘制课程内容
for(let p=0; p<periodCount; p++) {
const yPos = startY + (p+1)*cellH;
// 节次列
ctx.fillStyle = "#000000";
ctx.fillRect(startX, yPos, cellW, cellH);
ctx.fillStyle = "#FF0000";
ctx.font = `bold ${cellTextFontSize}px '微软雅黑'`;
ctx.fillText(courseData.periods[p], startX + cellW/2, yPos + cellH/2);
for(let d=0; d<dayCount; d++) {
const x = startX + (d+1)*cellW;
const y = yPos;
const cellKey = `${d},${p}`;
let courseName = courseData.cells[cellKey] || "点击";
ctx.fillStyle = "#F0F0F0";
ctx.fillRect(x, y, cellW, cellH);
// 获取单元格样式(如果没有则使用默认值)
const cellStyle = courseData.cellStyles?.[cellKey] || {
fontSize: cellTextFontSize,
fontFamily: "微软雅黑",
color: "#000000"
};
// 限制字体大小不超过单元格高度的60%
const finalFontSize = Math.min(cellStyle.fontSize, cellH * 0.6);
ctx.font = `${finalFontSize}px "${cellStyle.fontFamily}"`;
ctx.fillStyle = cellStyle.color;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// 文字截断处理
let maxChars = isLargeScreen ? 6 : (isSmallScreen ? 2 : 4);
let displayName = courseName.length > maxChars ? courseName.slice(0, maxChars-1) + "…" : courseName;
// 如果文字仍然太宽,进一步缩小字体
let tempFontSize = finalFontSize;
let textWidth = ctx.measureText(displayName).width;
while (textWidth > cellW - 8 && tempFontSize > 10) {
tempFontSize--;
ctx.font = `${tempFontSize}px "${cellStyle.fontFamily}"`;
textWidth = ctx.measureText(displayName).width;
}
ctx.fillText(displayName, x + cellW/2, y + cellH/2);
courseHitAreas.push({ row: p, col: d, x, y, w: cellW, h: cellH, cellKey, currentName: courseName });
}
}
if (!isSmallScreen) {
// 底部装饰线
ctx.fillStyle = "#000000";
ctx.fillRect(16, H-11, 42, 2);
ctx.fillStyle = "#FF0000";
ctx.fillRect(66, H-13, 26, 2);
}
ctx.textAlign = "left";
ctx.textBaseline = "alphabetic";
ctx.beginPath();
ctx.strokeStyle = "#ff0000";
ctx.lineWidth = 1;
ctx.setLineDash([]);
for(let d = 1; d <= dayCount; d++){
ctx.beginPath();
ctx.moveTo(startX + d * cellW, startY);
ctx.lineTo(startX + d * cellW, startY + (periodCount + 1) * cellH);
ctx.stroke();
}
for(let p = 1; p <= periodCount; p++){
ctx.beginPath();
ctx.moveTo(startX, startY + p * cellH);
ctx.lineTo(startX + (dayCount + 1) * cellW, startY + p * cellH);
ctx.stroke();
}
}
function editCourseCell(row, col, currentName) {
const cellKey = `${col},${row}`;
const existingStyle = courseData.cellStyles?.[cellKey] || {
fontSize: 16,
fontFamily: "微软雅黑",
color: "#000000"
};
const modal = document.createElement('div');
modal.className = 'modal-mask';
modal.innerHTML = `
<div class="modal-card" style="width: 320px;">
<h3>编辑课程</h3>
<div class="modal-field"><label>课程名称</label><input id="courseNameInput" value="${escapeHtml(currentName)}" style="width:100%;"></div>
<div class="modal-field"><label>字体大小</label><input id="fontSizeInput" type="number" value="${existingStyle.fontSize}" min="10" max="48" step="1" style="width:100%;"></div>
<div class="modal-field"><label>字体</label><select id="fontFamilyInput" style="width:100%;">${chineseFonts.map(f => `<option value="${f}" ${existingStyle.fontFamily === f ? 'selected' : ''}>${f}</option>`).join('')}</select></div>
<div class="modal-field"><label>文字颜色</label><select id="colorInput" style="width:100%;"><option value="#000000" ${existingStyle.color === '#000000' ? 'selected' : ''}>黑色</option><option value="#FF0000" ${existingStyle.color === '#FF0000' ? 'selected' : ''}>红色</option></select></div>
<div class="modal-buttons"><button class="cancel" id="cancelBtn">取消</button><button id="confirmBtn">确认</button></div>
</div>
`;
document.body.appendChild(modal);
modal.querySelector('#confirmBtn').onclick = () => {
const newName = modal.querySelector('#courseNameInput').value.trim() || "自习";
const newFontSize = parseInt(modal.querySelector('#fontSizeInput').value) || 16;
const newFontFamily = modal.querySelector('#fontFamilyInput').value;
const newColor = modal.querySelector('#colorInput').value;
courseData.cells[cellKey] = newName;
if (!courseData.cellStyles) courseData.cellStyles = {};
courseData.cellStyles[cellKey] = {
fontSize: Math.min(48, Math.max(10, newFontSize)),
fontFamily: newFontFamily,
color: newColor
};
modal.remove();
render();
};
modal.querySelector('.cancel').onclick = () => modal.remove();
modal.onclick = e => e.target === modal && modal.remove();
}
function renderPresetTemplate() {
ctx.fillStyle = "#ffffffff";
ctx.fillRect(0, 0, W, H);
const isSmallScreen = (W === 296 && H === 128);
const isLargeScreen = (W === 800 && H === 480);
if (!isSmallScreen) {
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 2;
ctx.strokeRect(4, 4, W-8, H-8);
ctx.fillStyle = "#FF0000";
ctx.fillRect(8, 8, 26, 4);
ctx.fillStyle = "#000000";
ctx.fillRect(W-54, 12, 42, 4);
let titleText = globalTitle.trim() || "输入内容·页眉";
let headerFontSize = isLargeScreen ? 52 : 26;
ctx.font = `bold ${headerFontSize}px '微软雅黑'`;
let titleWidth = ctx.measureText(titleText.slice(0,24)).width;
let bgX = (W - (titleWidth+40))/2;
ctx.fillStyle = "#FF0000";
ctx.fillRect(bgX, 7, titleWidth+40, headerFontSize + 10);
ctx.fillStyle = "#FFF";
ctx.fillText(titleText.slice(0,24), (W-titleWidth)/2, headerFontSize + 5);
ctx.beginPath();
ctx.strokeStyle = "#FF0000";
ctx.moveTo(12, headerFontSize + 20);
ctx.lineTo(W-12, headerFontSize + 20);
ctx.stroke();
var startY = headerFontSize + 50;
} else {
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 2;
ctx.strokeRect(4, 4, W-8, H-8);
var startY = 10;
}
const lineHeightBase = 26;
let currentY = startY;
hitAreas = [];
for(let i=0; i<presetLinesData.length; i++) {
const line = presetLinesData[i];
const segments = parseColoredText(line.text);
let { bgColor, textColor } = applyInvertStyle(line.baseColor, line.invertMode);
ctx.font = `${line.fontSize}px "${line.fontFamily}"`;
let totalWidth = 0;
segments.forEach(seg => {
ctx.font = `${line.fontSize}px "${line.fontFamily}"`;
totalWidth += ctx.measureText(seg.text).width;
});
let startX = line.alignment === 'center' ? (W - totalWidth) / 2 :
(line.alignment === 'right' ? W - totalWidth - 18 : 22);
let currentX = startX;
segments.forEach(seg => {
ctx.font = `${line.fontSize}px "${line.fontFamily}"`;
let textWidth = ctx.measureText(seg.text).width;
if(bgColor) {
ctx.fillStyle = bgColor;
ctx.fillRect(currentX, currentY-line.fontSize, textWidth+2, line.fontSize+4);
}
if(seg.isRed) {
ctx.fillStyle = "#FF0000";
} else {
ctx.fillStyle = textColor;
}
ctx.fillText(seg.text, currentX, currentY);
currentX += textWidth;
});
hitAreas.push({ index:i, yTop: currentY-20, yBottom: currentY+10 });
currentY += line.fontSize + 8;
if(currentY > H-20) break;
}
ctx.fillStyle = "#000";
ctx.fillRect(16, H-11, 42, 2);
ctx.fillStyle = "#FF0000";
ctx.fillRect(66, H-13, 26, 2);
}
function parseColoredText(text) {
const segments = [];
let lastIndex = 0;
const regex = /【(.*?)】/g;
let match;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
segments.push({
text: text.substring(lastIndex, match.index),
isRed: false
});
}
segments.push({
text: match[1],
isRed: true
});
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
segments.push({
text: text.substring(lastIndex),
isRed: false
});
}
return segments.length ? segments : [{ text: text, isRed: false }];
}
async function renderQRCodeMode() {
ctx.fillStyle = "#ffffffff";
ctx.fillRect(0, 0, W, H);
const isSmallScreen = (W === 296 && H === 128);
const isLargeScreen = (W === 800 && H === 480);
if (!isSmallScreen) {
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 2;
ctx.strokeRect(4, 4, W-8, H-8);
ctx.fillStyle = "#FF0000";
ctx.fillRect(8, 8, 26, 4);
ctx.fillStyle = "#000";
ctx.fillRect(W-54, 12, 42, 4);
const headerFontSize = isLargeScreen ? 36 : 18;
ctx.font = `bold ${headerFontSize}px '微软雅黑'`;
ctx.fillStyle = "#FF0000";
ctx.fillText(globalTitle.slice(0, 20), 33, headerFontSize + 10);
ctx.beginPath();
ctx.moveTo(12, headerFontSize + 20);
ctx.lineTo(W-12, headerFontSize + 20);
ctx.stroke();
var qrY = headerFontSize + 24;
var captionStartY = headerFontSize + qrSize + 30;
} else {
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 2;
ctx.strokeRect(4, 4, W-8, H-8);
var qrY = 8;
var captionStartY = qrSize + 12;
}
if(qrCodeImage) {
const img = new Image();
img.src = qrCodeImage;
await new Promise(r => { img.onload = r; img.onerror = r; });
let drawSize = qrSize;
if (isSmallScreen && drawSize > 90) {
drawSize = 80;
}
ctx.drawImage(img, (W-drawSize)/2, qrY, drawSize, drawSize);
}
let y = captionStartY;
const captionFontSize = isLargeScreen ? 32 : (isSmallScreen ? 12 : 18);
for(let line of qrCaptionLines) {
if(!line.trim()) { y += captionFontSize + 4; continue; }
ctx.font = `${captionFontSize}px '微软雅黑'`;
let tw = ctx.measureText(line).width;
ctx.fillStyle = "#f00";
ctx.fillText(line, (W-tw)/2, y+(H/20));
y += captionFontSize + 6;
}
if (!isSmallScreen) {
ctx.fillStyle = "#000";
ctx.fillRect(16, H-11, 42, 2);
ctx.fillStyle = "#FF0000";
ctx.fillRect(66, H-13, 26, 2);
}
}
function renderCustomMode() {
ctx.fillStyle = "#ffffffff";
ctx.fillRect(0, 0, W, H);
const isSmallScreen = (W === 296 && H === 128);
const isLargeScreen = (W === 800 && H === 480);
if (!isSmallScreen) {
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 2;
ctx.strokeRect(4, 4, W-8, H-8);
const headerFontSize = isLargeScreen ? 40 : 20;
const headerText = globalTitle.slice(0, 24);
ctx.font = `bold ${headerFontSize}px '微软雅黑'`;
const textWidth = ctx.measureText(headerText).width;
const boxX = 14;
const boxY = 8;
const boxWidth = Math.max(textWidth + 30, 150);
const boxHeight = headerFontSize + 10;
ctx.fillStyle = "#000";
ctx.fillRect(boxX, boxY, boxWidth, boxHeight);
ctx.fillStyle = "#FF0000";
ctx.fillText(headerText, boxX + 15, boxY + headerFontSize + 3);
ctx.fillStyle = "#FF0000";
ctx.fillRect(8, 8, 26, 4);
ctx.fillStyle = "#000";
ctx.fillRect(W-54, 12, 42, 4);
ctx.beginPath();
ctx.moveTo(12, headerFontSize + 30);
ctx.lineTo(W-12, headerFontSize + 30);
ctx.stroke();
var startY = headerFontSize + 48;
} else {
ctx.strokeStyle = "#FF0000";
ctx.lineWidth = 2;
ctx.strokeRect(4, 4, W-8, H-8);
var startY = 8;
}
const { fontSize, fontFamily, baseColor, invertMode, alignment } = customStyle;
const { bgColor, textColor } = applyInvertStyle(baseColor, invertMode);
ctx.font = `${fontSize}px "${fontFamily}"`;
let rawLines = customRawText.split('\n');
let allLines = [];
for(let line of rawLines) {
let segments = parseColoredText(line);
let currentLineSegments = [];
let currentLineWidth = 0;
for(let seg of segments) {
ctx.font = `${fontSize}px "${fontFamily}"`;
let segWidth = ctx.measureText(seg.text).width;
if(currentLineWidth + segWidth > W - 44 && currentLineSegments.length > 0) {
allLines.push([...currentLineSegments]);
currentLineSegments = [];
currentLineWidth = 0;
}
currentLineSegments.push(seg);
currentLineWidth += segWidth;
}
if(currentLineSegments.length > 0) {
allLines.push(currentLineSegments);
}
}
let y = startY;
for(let lineSegments of allLines) {
if(!lineSegments.length) { y += fontSize + 6; continue; }
let totalWidth = 0;
for(let seg of lineSegments) {
ctx.font = `${fontSize}px "${fontFamily}"`;
totalWidth += ctx.measureText(seg.text).width;
}
let startX = alignment === 'center' ? (W - totalWidth) / 2 :
(alignment === 'right' ? W - totalWidth - 18 : 22);
let currentX = startX;
for(let seg of lineSegments) {
ctx.font = `${fontSize}px "${fontFamily}"`;
let textWidth = ctx.measureText(seg.text).width;
const textHeight = fontSize;
if(bgColor) {
ctx.fillStyle = bgColor;
ctx.fillRect(currentX-2, y + fontSize/2 - textHeight+1, textWidth+4, textHeight + 4);
}
if(seg.isRed) {
ctx.fillStyle = "#FF0000";
} else {
ctx.fillStyle = textColor;
}
ctx.fillText(seg.text, currentX, y + fontSize/2);
currentX += textWidth;
}
y += fontSize + 6;
if(y > H-30) break;
}
if (!isSmallScreen) {
ctx.fillStyle = "#000";
ctx.fillRect(16, H-11, 42, 2);
ctx.fillStyle = "#FF0000";
ctx.fillRect(66, H-13, 26, 2);
}
}
function render() {
if(currentTemplate === 'course') renderCourseTable();
else if(currentTemplate === 'qrcode') renderQRCodeMode();
else if(currentTemplate === 'customText') renderCustomMode();
else renderPresetTemplate();
const canvas = document.getElementById('unifiedCanvas');
applyImageAdjustmentsmb(canvas)
}
function applyImageAdjustmentsmb(canvas) {
const brightness = 0;
const contrast = 100;
const saturation = 100;
const diffusion = 0;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
if (brightness !== 0) {
const brightnessFactor = brightness / 100;
for (let i = 0; i < data.length; i += 4) {
data[i] = clamp(data[i] + 255 * brightnessFactor, 0, 255);
data[i + 1] = clamp(data[i + 1] + 255 * brightnessFactor, 0, 255);
data[i + 2] = clamp(data[i + 2] + 255 * brightnessFactor, 0, 255);
}
}
if (contrast !== 0) {
const contrastFactor = (contrast + 100) / 100;
const contrastAdjust = (1 - contrastFactor) * 128;
for (let i = 0; i < data.length; i += 4) {
data[i] = clamp(data[i] * contrastFactor + contrastAdjust, 0, 255);
data[i + 1] = clamp(data[i + 1] * contrastFactor + contrastAdjust, 0, 255);
data[i + 2] = clamp(data[i + 2] * contrastFactor + contrastAdjust, 0, 255);
}
}
if (saturation !== 100) {
const saturationFactor = saturation / 100;
for (let i = 0; i < data.length; i += 4) {
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
data[i] = clamp(gray + (data[i] - gray) * saturationFactor, 0, 255);
data[i + 1] = clamp(gray + (data[i + 1] - gray) * saturationFactor, 0, 255);
data[i + 2] = clamp(gray + (data[i + 2] - gray) * saturationFactor, 0, 255);
}
}
ctx.putImageData(imageData, 0, 0);
return diffusion;
}
function handleCanvasClick(e) {
if(currentTemplate === 'course') {
const r = canvas.getBoundingClientRect();
const x = (e.clientX-r.left)*(W/r.width);
const y = (e.clientY-r.top)*(H/r.height);
for(let a of courseHitAreas) {
if(x>=a.x&&x<=a.x+a.w&&y>=a.y&&y<=a.y+a.h) {
editCourseCell(a.row,a.col,a.currentName); return;
}
}
return;
}
if(currentTemplate === 'qrcode') {
const r = canvas.getBoundingClientRect();
const y = (e.clientY-r.top)*(H/r.height);
if(y>130) {
const m = document.createElement('div');
m.className='modal-mask';
m.innerHTML=`<div class="modal-card"><h3>编辑文字</h3><textarea rows=4 style="width:100%;background:#000;color:#fff;border:1px solid red;border-radius:16px;padding:10px;">${qrCaptionLines.join('\n')}</textarea><div class="modal-buttons"><button class=cancel>取消</button><button class=ok>保存</button></div></div>`;
document.body.appendChild(m);
m.querySelector('.ok').onclick=()=>{
qrCaptionLines = m.querySelector('textarea').value.split('\n');
updateCaptionLinesToUI();
render(); m.remove();
};
m.querySelector('.cancel').onclick=()=>m.remove();
}
return;
}
if(currentTemplate !== 'customText') {
const r = canvas.getBoundingClientRect();
const y = (e.clientY-r.top)*(H/r.height);
for(let a of hitAreas) {
if(y>=a.yTop&&y<=a.yBottom) {
editPresetModal(a.index); return;
}
}
addPresetNewRow();
}
}
function addPresetNewRow() {
const modal = document.createElement('div');
modal.className = 'modal-mask';
modal.innerHTML = `
<div class="modal-card">
<h3>➕ 添加新行</h3>
<div class="modal-field"><label>文字内容</label><input id="modalText" value="新行内容"></div>
<div class="modal-field"><label>大小</label><input id="modalFontSize" type="number" value="14" min="10" max="52"></div>
<div class="modal-field"><label>字体</label><select id="modalFontFamily">${chineseFonts.map(f => `<option value="${f}">${f}</option>`).join('')}</select></div>
<div class="modal-field"><label>基色</label><select id="modalBaseColor"><option value="#000000">黑色</option><option value="#FF0000">红色</option></select></div>
<div class="modal-field"><label>反色</label><select id="modalInvert">${invertModes.map(m => `<option value="${m.value}">${m.label}</option>`).join('')}</select></div>
<div class="modal-field"><label>对齐</label><select id="modalAlign"><option value="left">左</option><option value="center">中</option><option value="right">右</option></select></div>
<div class="modal-buttons"><button class="cancel" id="cancelBtn">取消</button><button id="confirmBtn">确认</button></div>
</div>
`;
document.body.appendChild(modal);
const confirm = () => {
const newRow = {
text: modal.querySelector('#modalText').value,
fontSize: Math.min(52, Math.max(10, parseInt(modal.querySelector('#modalFontSize').value) || 14)),
fontFamily: modal.querySelector('#modalFontFamily').value,
baseColor: modal.querySelector('#modalBaseColor').value,
invertMode: modal.querySelector('#modalInvert').value,
alignment: modal.querySelector('#modalAlign').value
};
presetLinesData.push(newRow);
renderPresetTemplate();
modal.remove();
};
modal.querySelector('#confirmBtn').onclick = confirm;
modal.querySelector('#cancelBtn').onclick = () => modal.remove();
modal.onclick = (e) => { if(e.target === modal) modal.remove(); };
}
function editPresetModal(idx) {
const line = presetLinesData[idx];
const modal = document.createElement('div');
modal.className = 'modal-mask';
modal.innerHTML = `
<div class="modal-card">
<h3>编辑第 ${idx+1} 行</h3>
<div class="modal-field"><label>文字内容</label><input id="modalText" value="${escapeHtml(line.text)}"></div>
<div class="modal-field"><label>大小</label><input id="modalFontSize" type="number" value="${line.fontSize}" min="10" max="52"></div>
<div class="modal-field"><label>字体</label><select id="modalFontFamily">${chineseFonts.map(f => `<option value="${f}" ${line.fontFamily===f?'selected':''}>${f}</option>`).join('')}</select></div>
<div class="modal-field"><label>基色</label><select id="modalBaseColor"><option value="#000000" ${line.baseColor==='#000000'?'selected':''}>黑色</option><option value="#FF0000" ${line.baseColor==='#FF0000'?'selected':''}>红色</option></select></div>
<div class="modal-field"><label>反色</label><select id="modalInvert">${invertModes.map(m => `<option value="${m.value}" ${line.invertMode===m.value?'selected':''}>${m.label}</option>`).join('')}</select></div>
<div class="modal-field"><label>对齐</label><select id="modalAlign"><option value="left" ${line.alignment==='left'?'selected':''}>左</option><option value="center" ${line.alignment==='center'?'selected':''}>中</option><option value="right" ${line.alignment==='right'?'selected':''}>右</option></select></div>
<div class="modal-buttons"><button class="cancel" id="cancelBtn">取消</button><button id="confirmBtn">确认</button></div>
</div>
`;
document.body.appendChild(modal);
const confirm = () => {
presetLinesData[idx] = {
text: modal.querySelector('#modalText').value,
fontSize: Math.min(52, Math.max(10, parseInt(modal.querySelector('#modalFontSize').value) || 14)),
fontFamily: modal.querySelector('#modalFontFamily').value,
baseColor: modal.querySelector('#modalBaseColor').value,
invertMode: modal.querySelector('#modalInvert').value,
alignment: modal.querySelector('#modalAlign').value
};
renderPresetTemplate();
modal.remove();
};
modal.querySelector('#confirmBtn').onclick = confirm;
modal.querySelector('#cancelBtn').onclick = () => modal.remove();
modal.onclick = (e) => { if(e.target === modal) modal.remove(); };
}
function switchTemplate(tpl) {
currentTemplate = tpl;
document.getElementById('customBlock').style.display = tpl==='customText'?'block':'none';
document.getElementById('qrcodeBlock').style.display = tpl==='qrcode'?'block':'none';
document.getElementById('courseBlock').style.display = tpl==='course'?'block':'none';
document.getElementById('presetHintBlock').style.display = (tpl!=='customText'&&tpl!=='qrcode')?'block':'none';
if(tpl === 'course') { loadPresetData('schedule'); render(); }
else if(tpl === 'qrcode') { syncCaptionFromTextarea(); refreshQRImage(); }
else if(tpl === 'customText') { render(); }
else { loadPresetData(tpl); render(); }
}
function resetCurrent() {
if(currentTemplate === 'customText') {
customRawText = "小雨在放学路上捡到一枚【闪闪发亮的硬币】。\n她买了两颗【橘子味的糖果】,一颗塞进嘴里,\n甜得她眯起了眼睛;另一颗放在路边的长椅上。\n一只【唱歌的小鸟】飞过来,歪着脑袋看了看\n,叼起糖果飞走了。小雨有点失落,\n但还是继续往家走。第二天早上,她推开窗户,\n发现窗台上放着一朵【金黄色的小野花】,花瓣上还挂着露珠。小雨捧着花笑了:\n“原来快乐会【越分越多】呀。”\n从那天起,她每天都往长椅上放一颗糖。";
document.getElementById('freeTextArea').value = customRawText;
} else if(currentTemplate === 'qrcode') {
qrText = "https://mspppclock.tech/";
document.getElementById('qrContentInput').value = qrText;
qrCaptionLines = ["扫码访问官网"];
updateCaptionLinesToUI();
refreshQRImage();
} else if(currentTemplate === 'course') {
initDefaultCourse();
} else {
loadPresetData(currentTemplate);
}
render();
}
function escapeHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function init() {
const fsel = document.getElementById('customFontFamily');
chineseFonts.forEach(f=>{ let o=document.createElement('option');o.value=f;o.textContent=f;fsel.appendChild(o); });
const isel = document.getElementById('customInvertMode');
invertModes.forEach(m=>{ let o=document.createElement('option');o.value=m.value;o.textContent=m.label;isel.appendChild(o); });
document.getElementById('freeTextArea').value = customRawText;
document.getElementById('freeTextArea').oninput = e=>{ customRawText=e.target.value; render(); };
document.querySelectorAll('#customFontSize,#customFontFamily,#customBaseColor,#customInvertMode,#customAlign').forEach(i=>{
i.onchange=()=>{
customStyle.fontSize = +document.getElementById('customFontSize').value;
customStyle.fontFamily = document.getElementById('customFontFamily').value;
customStyle.baseColor = document.getElementById('customBaseColor').value;
customStyle.invertMode = document.getElementById('customInvertMode').value;
customStyle.alignment = document.getElementById('customAlign').value;
render();
};
});
document.getElementById('globalTitleInput').oninput = e=>{ globalTitle=e.target.value; render(); };
document.getElementById('forceRefreshBtn').onclick = render;
document.getElementById('resetCurrentBtn').onclick = resetCurrent;
document.getElementById('generateQRBtn').onclick = ()=>{ qrText=document.getElementById('qrContentInput').value; refreshQRImage(); };
document.getElementById('qrCaptionText').oninput = ()=>{ syncCaptionFromTextarea(); render(); };
document.getElementById('applyCourseSettingBtn').onclick = applyCourseCustomSettings;
canvas.onclick = handleCanvasClick;
document.querySelectorAll('.temp-btn').forEach(b=>{
b.onclick=()=>{
document.querySelectorAll('.temp-btn').forEach(x=>x.classList.remove('active'));
b.classList.add('active');
switchTemplate(b.dataset.template);
};
});
document.getElementById('epdwh').addEventListener('change', function() {
const [newW, newH] = this.value.split('x').map(Number);
canvas.width = newW;
canvas.height = newH;
render();
});
// 监听屏幕尺寸变化
document.getElementById('epdwh').addEventListener('change', function() {
updateCanvasSize();
if (currentTemplate === 'qrcode') {
refreshQRImage(); // 二维码模式需要重新生成
} else {
render();
}
});
switchTemplate('schedule');
}
init();
})();