Files
qianbian/analysis/web/moban.js
T

944 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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();
})();