用聊天的方式生成初步代码、仍然用聊天的方式修改和更新代码,调整或增减相应的功能。
记得我有一次上物理化学实验课,要求用Fortran,Pascal 或Turbo C 2.0任意一种语言编写一个程序完成一个简单的任务。我老早就在笔记本上写好、希望早点下课去图书馆继续看编程类的书或者继续stereohomology
之类的散心思考,因为问题简单,用的C语言是纯自学的、而且故意用一个指针代替数组实现了某种功能。然而,电脑上输入的时候,嵌套循环中误把一个循环变量i
打成了j
,结果反复调试总是不出正确结果,几乎崩溃。我们的实验辅导老师Dr. Yan
,曾经完成过用C
语言编写某个专家系统的子任务,直到临下课终于帮我找出了这个bug的所在!——超级低级的一个错误,如果交给大语言模型,还不是分分钟秒杀。现在想起来,趁手工具对于生产效率的提升真的是不可思议啊。
所以,用大模型编程写代码,不在VS code里面借助插件,不用github Copilot cursor之类的时候,我都是这么用的:把问题和代码统统交给大模型。我的经验,这种用法DeepSeek R1满血版直接偷工减料,根本不出活,还会直接导致代码退化降级,必须时时刻刻提防着。所以用Grok 3 Beta就会感觉非常省心。
下面的提示词,不计Token地、把所有工作全都丢给Grok 3, 完全是把它当成生产队的驴来用。
而Grok 3 beta回答起来,也是不折不扣,照单全收,而且基本上给的建议和修改都非常靠谱、贴心和省心。人工智能大语言模型仍然离不开人类程序员,但却可以大大提高某些方面的工作效率。
当年我的第一篇CSDN博客初衷是因为当时要用VBA读取红外光谱的二进制谱图数据,然后用于直线加速器剂量的标定。当时对谱图的格式所知有限,唯一掌握的信息是mathworks fileExchange站上一个loadspectra文件。所以,当时就是想方设法要把matlab的语言转换成VBA。(企业为一个小项目需求专门购买Matlab license太不现实了,在管理员权限都需要通过免费彩虹表获取的情况下,用octave或scilab之类的替代都不容易;所以VBA就成了最现实可行的方案)。——当时借助Google搜索花了两周业余时间完成的工作,放在今天,借助大语言模型可能时间会缩短很多。还可能会考虑借助C++来做。但当时就只有那些选项。
我的前端代码:<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>四线格与米字格生成器(深绿色版)</title>
<style>
@font-face {
font-family: 'KaiTi';
src: url('./fonts/TW-Kai.ttf') format('truetype');
}
@font-face {
font-family: 'TeXGyreAdventor';
src: url('./fonts/texgyreadventor-regular.otf') format('opentype');
font-display: swap;
}
html, body {
font-family: 'TeXGyreAdventor', 'KaiTi', serif;
text-align: center;
padding: 20px;
background-color: white;
margin: 0;
}
#input-container {
margin-bottom: 20px;
}
#hanzi-input {
font-size: 28px;
padding: 10px;
width: 300px;
border: 2px solid #339933;
border-radius: 5px;
}
#generate-btn, #print-btn, #clear-btn {
font-size: 18px;
padding: 10px 20px;
margin: 0 10px;
background-color: #339933;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#grid-container {
display: flex;
flex-direction: column;
align-items: center;
}
.row {
display: flex;
position: relative;
width: 540px;
}
/* 四线格样式(深绿色系) */
.pinyin-row {
height: 28.8px;
display: flex;
align-items: center;
}
.pinyin-cell {
width: 60px;
height: 28.8px;
display: flex;
justify-content: center;
align-items: center;
}
.stroke-container {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
height: 28.8px;
align-items: center;
margin-left: 6px;
}
.stroke-svg {
width: 18.72px;
height: 18.72px;
margin-right: 4.68px;
z-index: 1;
}
.pinyin-row.first::before, .pinyin-row::after {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
border: 1px solid #339933;
z-index: 0;
}
.pinyin-row.first::before {
top: 0;
}
.pinyin-row::after {
bottom: 0;
}
.pinyin-row .line1, .pinyin-row .line2 {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
border: 1px solid #339933;
z-index: 0;
opacity: 0.25;
}
.pinyin-row .line1 {
top: 9.6px;
border-style: dashed;
}
.pinyin-row .line2 {
top: 19.2px;
}
.pinyin-cell span {
font-family: 'TeXGyreAdventor' !important;
font-size: 16.8px;
position: relative;
z-index: 1;
}
.black span {
color: black;
opacity: 1;
}
.gray span {
color: #ccc;
}
/* 米字格样式(深绿色系) */
.char-row {
margin-top: -1px;
}
.cell {
width: 60px;
height: 60px;
border-bottom: 1px solid #339933;
border-right: 1px solid #339933;
border-left: 1px solid #339933;
position: relative;
display: flex;
justify-content: center;
align-items: center;
font-size: 43.2px;
font-family: 'KaiTi', serif;
}
.cell:not(:first-child) {
border-left: none;
}
.char-row .cell {
border-top: 1px solid #339933;
}
.cell::before, .cell::after {
content: '';
position: absolute;
border: 1px dashed #339933;
z-index: 0;
opacity: 0.5;
}
.cell::before {
width: 100%;
height: 0;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.cell::after {
width: 0;
height: 100%;
left: 50%;
top: 0;
transform: translateX(-50%);
}
.diagonal1, .diagonal2 {
position: absolute;
width: 84.6px;
height: 0;
border: 1px dashed #339933;
z-index: 0;
opacity: 0.5;
}
.diagonal1 {
top: 0;
left: 0;
transform: rotate(45deg);
transform-origin: 0 0;
}
.diagonal2 {
bottom: 0;
left: 0;
transform: rotate(-45deg);
transform-origin: 0 100%;
}
.cell span {
position: relative;
z-index: 1;
}
.black span {
color: black;
opacity: 1;
}
.gray span {
color: #ccc;
}
@media print {
#input-container, #generate-btn, #print-btn, #clear-btn {
display: none;
}
#grid-container {
display: block;
}
html, body {
background-color: white;
margin: 0;
}
.char-row.page-break {
page-break-after: always;
}
}
</style>
</head>
<body>
<div id="input-container">
<input type="text" id="hanzi-input" value="字库有爨没寳怎么办寶氷麤鳳龘">
<button id="clear-btn">清除</button>
<button id="generate-btn">生成</button>
<button onclick="printToPDF()">打印到 PDF</button>
</div>
<div id="grid-container"></div>
<script type="text/javascript" src="./pinyin_dict_withtone.js"></script>
<script type="text/javascript" src="./pinyinUtil.js"></script>
<script src="./FileSaver.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hanzi-writer@3.5.0/dist/hanzi-writer.min.js"></script>
<script>
window.onload = function() {
generateGrid();
};
function createStrokeSVG(strokes, currentIndex) {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "18.72");
svg.setAttribute("height", "18.72");
svg.setAttribute("viewBox", "0 0 1024 1024");
const fullCharGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
fullCharGroup.setAttribute("transform", "scale(1, -1) translate(0, -1024)");
strokes.forEach(stroke => {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", stroke.path);
path.setAttribute("fill", "#ccc");
fullCharGroup.appendChild(path);
});
svg.appendChild(fullCharGroup);
const completedGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
completedGroup.setAttribute("transform", "scale(1, -1) translate(0, -1024)");
for (let i = 0; i < currentIndex; i++) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", strokes[i].path);
path.setAttribute("fill", "black");
completedGroup.appendChild(path);
}
svg.appendChild(completedGroup);
if (currentIndex < strokes.length) {
const currentPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
currentPath.setAttribute("d", strokes[currentIndex].path);
currentPath.setAttribute("fill", "#ff4444");
currentPath.setAttribute("transform", "scale(1, -1) translate(0, -1024)");
svg.appendChild(currentPath);
}
return svg;
}
function generateGrid() {
const input = document.getElementById('hanzi-input').value;
const gridContainer = document.getElementById('grid-container');
gridContainer.innerHTML = '';
const pinyinArray = pinyinUtil.getPinyin(input, ' ', true, false).split(' ');
for (let i = 0; i < input.length; i++) {
const char = input[i];
const pinyin = pinyinArray[i];
const pinyinRow = document.createElement('div');
pinyinRow.className = 'row pinyin-row';
if (i === 0) {
pinyinRow.classList.add('first');
} else {
pinyinRow.style.marginTop = '-1px';
}
const line1 = document.createElement('div');
line1.className = 'line1';
const line2 = document.createElement('div');
line2.className = 'line2';
pinyinRow.appendChild(line1);
pinyinRow.appendChild(line2);
const pinyinCell = document.createElement('div');
pinyinCell.className = 'pinyin-cell';
const span = document.createElement('span');
span.textContent = pinyin || '';
span.classList.add('black');
pinyinCell.appendChild(span);
pinyinRow.appendChild(pinyinCell);
const strokeContainer = document.createElement('div');
strokeContainer.className = 'stroke-container';
pinyinRow.appendChild(strokeContainer);
gridContainer.appendChild(pinyinRow);
const charRow = document.createElement('div');
charRow.className = 'row char-row';
for (let j = 0; j < 9; j++) {
const cell = document.createElement('div');
cell.className = 'cell';
const diag1 = document.createElement('div');
diag1.className = 'diagonal1';
const diag2 = document.createElement('div');
diag2.className = 'diagonal2';
cell.appendChild(diag1);
cell.appendChild(diag2);
const span = document.createElement('span');
if (j === 0) {
span.textContent = char;
cell.classList.add('black');
} else {
span.textContent = char;
cell.classList.add('gray');
}
cell.appendChild(span);
charRow.appendChild(cell);
}
if ((i + 1) % 9 === 0) {
charRow.classList.add('page-break');
}
gridContainer.appendChild(charRow);
const tempDiv = document.createElement('div');
document.body.appendChild(tempDiv);
const writer = new HanziWriter(tempDiv, {
width: 60,
height: 60,
showOutline: false,
});
writer.setCharacter(char).then(() => {
const strokes = writer._character.strokes;
const strokeCount = strokes.length;
const strokeCountSpan = document.createElement('span');
strokeCountSpan.textContent = `${strokeCount}画`;
strokeCountSpan.style.marginRight = '6px';
strokeContainer.appendChild(strokeCountSpan);
for (let k = 0; k < strokeCount; k++) {
const svg = createStrokeSVG(strokes, k);
svg.classList.add('stroke-svg');
strokeContainer.appendChild(svg);
}
document.body.removeChild(tempDiv);
}).catch(error => {
console.error(`无法加载汉字“${char}”的笔画数据:`, error);
});
}
}
document.getElementById('generate-btn').addEventListener('click', function() {
generateGrid();
});
document.getElementById('clear-btn').addEventListener('click', function() {
const input = document.getElementById('hanzi-input');
const gridContainer = document.getElementById('grid-container');
input.value = '';
gridContainer.innerHTML = '';
input.focus();
});
async function printToPDF() {
try {
const gridDiv = document.getElementById('grid-container');
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page {
margin: 20px;
size: A4 portrait;
background-color: white;
}
@font-face {
font-family: 'KaiTi';
src: url('/fonts/TW-Kai.ttf') format('truetype');
}
@font-face {
font-family: 'TeXGyreAdventor';
src: url('/fonts/texgyreadventor-regular.otf') format('opentype');
font-display: swap;
}
html, body {
font-family: 'TeXGyreAdventor', 'KaiTi', serif;
text-align: center;
padding: 20px;
background-color: white;
margin: 0;
}
#grid-container {
display: flex;
flex-direction: column;
align-items: center;
}
.row {
display: flex;
position: relative;
width: 540px;
}
.pinyin-row {
height: 28.8px;
display: flex;
align-items: center;
}
.pinyin-cell {
width: 60px;
height: 28.8px;
display: flex;
justify-content: center;
align-items: center;
}
.stroke-container {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
height: 28.8px;
align-items: center;
margin-left: 6px;
}
.stroke-svg {
width: 18.72px;
height: 18.72px;
margin-right: 4.68px;
z-index: 1;
}
.pinyin-row.first::before, .pinyin-row::after {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
border: 1px solid #339933;
z-index: 0;
}
.pinyin-row.first::before {
top: 0;
}
.pinyin-row::after {
bottom: 0;
}
.pinyin-row .line1, .pinyin-row .line2 {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
border: 1px solid #339933;
z-index: 0;
opacity: 0.25;
}
.pinyin-row .line1 {
top: 9.6px;
border-style: dashed;
}
.pinyin-row .line2 {
top: 19.2px;
}
.pinyin-cell span {
font-family: 'TeXGyreAdventor' !important;
font-size: 16.8px;
position: relative;
z-index: 1;
}
.black span {
color: black;
opacity: 1;
}
.gray span {
color: #ccc;
}
.char-row {
margin-top: -1px;
}
.cell {
width: 60px;
height: 60px;
border-bottom: 1px solid #339933;
border-right: 1px solid #339933;
border-left: 1px solid #339933;
position: relative;
display: flex;
justify-content: center;
align-items: center;
font-size: 43.2px;
font-family: 'KaiTi', serif;
}
.cell:not(:first-child) {
border-left: none;
}
.char-row .cell {
border-top: 1px solid #339933;
}
.cell::before, .cell::after {
content: '';
position: absolute;
border: 1px dashed #339933;
z-index: 0;
opacity: 0.5;
}
.cell::before {
width: 100%;
height: 0;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.cell::after {
width: 0;
height: 100%;
left: 50%;
top: 0;
transform: translateX(-50%);
}
.diagonal1, .diagonal2 {
position: absolute;
width: 84.6px;
height: 0;
border: 1px dashed #339933;
z-index: 0;
opacity: 0.5;
}
.diagonal1 {
top: 0;
left: 0;
transform: rotate(45deg);
transform-origin: 0 0;
}
.diagonal2 {
bottom: 0;
left: 0;
transform: rotate(-45deg);
transform-origin: 0 100%;
}
.cell span {
position: relative;
z-index: 1;
}
.black span {
color: black;
opacity: 1;
}
.gray span {
color: #ccc;
}
.char-row.page-break {
page-break-after: always;
}
</style>
</head>
<body>
${gridDiv.outerHTML}
</body>
</html>
`;
const response = await fetch('http://localhost:3000/generatepdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
},
body: JSON.stringify({
html: htmlContent,
timestamp: new Date().getTime()
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`PDF生成失败: ${errorText}`);
}
const blob = await response.blob();
const filename = `汉字练习-${new Date().toISOString().slice(0,10)}.pdf`;
saveAs(blob, filename);
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed; top:20px; right:20px; padding:12px; background:#339933; color:white; border-radius:5px;';
toast.textContent = '✓ PDF生成成功';
document.body.appendChild(toast);
setTimeout(() => document.body.removeChild(toast), 3000);
} catch (error) {
console.error('PDF生成错误:', error);
const errorDiv = document.createElement('div');
errorDiv.style.cssText = 'position:fixed; top:20px; left:20px; padding:12px; background:red; color:white; border-radius:5px;';
errorDiv.textContent = `错误: ${error.message}`;
document.body.appendChild(errorDiv);
setTimeout(() => document.body.removeChild(errorDiv), 5000);
}
}
</script>
</body>
</html> 后台server.js代码:const express = require('express');
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs').promises;
const { v4: uuidv4 } = require('uuid');
const cors = require('cors');
const app = express();
app.use('/fonts', express.static('public/fonts'));
const port = 3000;
// 配置 CORS,明确允许 http://localhost:3000
app.use(cors({
origin: '*',//'http://localhost:3000', // 确保与前端请求的源完全一致
methods: ['GET', 'POST', 'OPTIONS'], // 明确支持的方法,包括 OPTIONS(预检请求)
allowedHeaders: ['Content-Type', 'Authorization'],
//allowedHeaders: ['Content-Type'], // 允许的请求头
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: false // 如果不需要 cookie,可以设置为 false
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.static(path.join(__dirname, 'public')));
// 添加OPTIONS请求处理
app.options('*', cors());
// 托管 fonts 目录,确保 Puppeteer 可以通过 URL 访问字体文件
app.use('/fonts', express.static(path.join(__dirname, 'public', 'fonts')));
app.get('/', (req, res) => {
res.send('服务器运行正常!请访问 /index.html 或点击“打印到 PDF”按钮生成 PDF。');
});
app.post('/generatepdf', async (req, res) => {
console.log('Received request to generate PDF');
const { html } = req.body;
if (!html) {
console.error('Missing HTML content in request body');
return res.status(400).send('Missing HTML content');
}
try {
console.log('Launching Puppeteer...');
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
console.log('Puppeteer launched successfully');
const page = await browser.newPage();
console.log('Setting page content...');
await page.setContent(html, { waitUntil: 'networkidle0' });
console.log('Page content set');
console.log('Generating PDF...');
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
margin: { top: '20px', right: '20px', bottom: '20px', left: '20px' }
});
console.log('PDF buffer size:', pdfBuffer.length);
await browser.close();
console.log('PDF generated successfully');
// 保存 PDF 文件用于调试
const filename = `character-strokes-output-${uuidv4()}.pdf`;
await fs.writeFile(path.join(__dirname, filename), pdfBuffer);
console.log(`PDF saved to ${filename} for debugging`);
// 设置响应头并发送 PDF
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfBuffer.length,
'Content-Disposition': 'attachment; filename="character_strokes.pdf"'
});
res.end(pdfBuffer, 'binary');
} catch (error) {
console.error('Failed to generate PDF:', error);
res.status(500).send('Failed to generate PDF: ' + error.message);
}
});
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
}); 现在存在下面的问题:(1)我希望生成PDF文件的时候,A4页面每页放11个汉字,包括汉字的拼音和笔顺笔画所在四线格行、汉字所在的米字格行,应该作为一个整体,不能分页放;(2)汉字所在米字格行的最后一个灰色字符,希望删除掉,供学习字帖着默写描摹;(3)分页的时候,从第2页起,每页的第一个汉字,因为拼音所在的四线格行第一条横线跟上一页最后一个汉字米字格行的最下面横线是共用的,所以,会在分页部分缺失,所以,在发生分页的时候,每页的第一个汉字,它的四线格行的上面应该补充一条等距的横线,这条横线应该是实线,保持四条线上下等距分布;(4)生成的PDF文件,非常奇怪的是,在acrobat reader里面打开时正常的,但在firefox中直接打开,米字格行整行整格全都是色彩斑斓的虚影,不知道是什么原因,请尝试修复之。请根据前面的代码和问题的描述,逐个解决相关问题,提出修改代码的意见建议。并给出具体解决问题的步骤、给出每个步骤需要使用的完整的修改后的代码。
目前开放免费试用的大语言模型,似乎也只有Grok 3 Beta的算力能支撑起这样的折腾仍然表现不俗?
调成这个样子也算实属不易,
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>四线格与米字格生成器(深绿色版)</title>
<style>
@font-face {
font-family: 'KaiTi';
src: url('./fonts/TW-Kai.ttf') format('truetype');
}
@font-face {
font-family: 'TeXGyreAdventor';
src: url('./fonts/texgyreadventor-regular.otf') format('opentype');
font-display: swap;
}
html, body {
font-family: 'TeXGyreAdventor', 'KaiTi', serif;
text-align: center;
padding: 20px;
background-color: white;
margin: 0;
}
#input-container {
margin-bottom: 20px;
}
#hanzi-input {
font-size: 28px;
padding: 10px;
width: 300px;
border: 2px solid #339933;
border-radius: 5px;
}
#generate-btn, #print-btn, #clear-btn {
font-size: 18px;
padding: 10px 20px;
margin: 0 10px;
background-color: #339933;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#grid-container {
display: flex;
flex-direction: column;
align-items: center;
}
.row {
display: flex;
position: relative;
width: 540px;
}
.pinyin-row {
height: 28.8px;
display: flex;
align-items: center;
}
.pinyin-cell {
width: 60px;
height: 28.8px;
display: flex;
justify-content: center;
align-items: center;
}
.stroke-container {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
height: 28.8px;
align-items: center;
margin-left: 6px;
}
.stroke-svg {
width: 18.72px;
height: 18.72px;
margin-right: 4.68px;
z-index: 1;
}
.pinyin-row.page-first::before {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
top: 0px;
border: 1px solid #339933;
z-index: 0;
}
.pinyin-row::after {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
bottom: 0;
border: 1px solid #339933;
z-index: 0;
}
.pinyin-row .line1, .pinyin-row .line2 {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
border: 1px solid #339933;
z-index: 0;
opacity: 0.25;
}
.pinyin-row .line1 {
top: 9.6px;
border-style: dashed;
}
.pinyin-row .line2 {
top: 19.2px;
}
.pinyin-cell span {
font-family: 'TeXGyreAdventor' !important;
font-size: 16.8px;
position: relative;
z-index: 1;
}
.black span {
color: black;
opacity: 1;
}
.gray span {
color: #ccc;
}
.char-row {
margin-top: -1px;
}
.cell {
width: 60px;
height: 60px;
border-bottom: 1px solid #339933;
border-right: 1px solid #339933;
border-left: 1px solid #339933;
position: relative;
display: flex;
justify-content: center;
align-items: center;
font-size: 43.2px;
font-family: 'KaiTi', serif;
}
.cell:not(:first-child) {
border-left: none;
}
.char-row .cell {
border-top: 1px solid #339933;
}
.cell::before, .cell::after {
content: '';
position: absolute;
border: 1px dashed #339933;
z-index: 0;
opacity: 0.5;
}
.cell::before {
width: 100%;
height: 0;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.cell::after {
width: 0;
height: 100%;
left: 50%;
top: 0;
transform: translateX(-50%);
}
.diagonal1, .diagonal2 {
position: absolute;
width: 80px;
height: 0;
border: 0.125px dashed #339933;
z-index: 0;
opacity: 0.25;
}
.diagonal1 {
top: 0;
left: 0;
transform: rotate(48deg);
transform-origin: 0 0;
}
.diagonal2 {
bottom: 0;
left: 0;
transform: rotate(-48deg);
transform-origin: 0 100%;
}
.cell span {
position: relative;
z-index: 1;
}
.black span {
color: black;
opacity: 1;
}
.gray span {
color: #ccc;
}
@media print {
#input-container, #generate-btn, #print-btn, #clear-btn {
display: none;
}
#grid-container {
display: block;
}
html, body {
background-color: white;
margin: 0;
}
.char-row.page-break {
page-break-after: always;
}
}
</style>
</head>
<body>
<div id="input-container">
<input type="text" id="hanzi-input" value="字库有爨没寳怎么办寶氷麤鳳龘">
<button id="clear-btn">清除</button>
<button id="generate-btn">生成</button>
<button onclick="printToPDF()">打印到 PDF</button>
</div>
<div id="grid-container"></div>
<script type="text/javascript" src="./pinyin_dict_withtone.js"></script>
<script type="text/javascript" src="./pinyinUtil.js"></script>
<script src="./FileSaver.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hanzi-writer@3.5.0/dist/hanzi-writer.min.js"></script>
<script>
window.onload = function() {
generateGrid();
};
function createStrokeSVG(strokes, currentIndex) {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "18.72");
svg.setAttribute("height", "18.72");
svg.setAttribute("viewBox", "0 0 1024 1024");
svg.setAttribute("preserveAspectRatio", "xMidYMid meet");
const fullCharGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
fullCharGroup.setAttribute("transform", "scale(1, -1) translate(0, -1024)");
strokes.forEach(stroke => {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", stroke.path);
path.setAttribute("fill", "#ccc");
fullCharGroup.appendChild(path);
});
svg.appendChild(fullCharGroup);
const completedGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
completedGroup.setAttribute("transform", "scale(1, -1) translate(0, -1024)");
for (let i = 0; i < currentIndex; i++) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", strokes[i].path);
path.setAttribute("fill", "black");
completedGroup.appendChild(path);
}
svg.appendChild(completedGroup);
if (currentIndex < strokes.length) {
const currentPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
currentPath.setAttribute("d", strokes[currentIndex].path);
currentPath.setAttribute("fill", "#ff4444");
currentPath.setAttribute("transform", "scale(1, -1) translate(0, -1024)");
svg.appendChild(currentPath);
}
return svg;
}
function generateGrid() {
const input = document.getElementById('hanzi-input').value;
const gridContainer = document.getElementById('grid-container');
gridContainer.innerHTML = '';
const pinyinArray = pinyinUtil.getPinyin(input, ' ', true, false).split(' ');
for (let i = 0; i < input.length; i++) {
const char = input[i];
const pinyin = pinyinArray[i];
const pinyinRow = document.createElement('div');
pinyinRow.className = 'row pinyin-row';
if (i === 0 || i % 11 === 0) {
pinyinRow.classList.add('page-first');
} else {
pinyinRow.style.marginTop = '-1px';
}
const line1 = document.createElement('div');
line1.className = 'line1';
const line2 = document.createElement('div');
line2.className = 'line2';
pinyinRow.appendChild(line1);
pinyinRow.appendChild(line2);
const pinyinCell = document.createElement('div');
pinyinCell.className = 'pinyin-cell';
const span = document.createElement('span');
span.textContent = pinyin || '';
span.classList.add('black');
pinyinCell.appendChild(span);
pinyinRow.appendChild(pinyinCell);
const strokeContainer = document.createElement('div');
strokeContainer.className = 'stroke-container';
pinyinRow.appendChild(strokeContainer);
gridContainer.appendChild(pinyinRow);
const charRow = document.createElement('div');
charRow.className = 'row char-row';
for (let j = 0; j < 10; j++) { /* 修改为 10 个单元格 */
const cell = document.createElement('div');
cell.className = 'cell';
const diag1 = document.createElement('div');
diag1.className = 'diagonal1';
const diag2 = document.createElement('div');
diag2.className = 'diagonal2';
cell.appendChild(diag1);
cell.appendChild(diag2);
const span = document.createElement('span');
if (j === 0) {
span.textContent = char;
cell.classList.add('black');
} else if (j < 9) { /* 灰色汉字到第 9 个 */
span.textContent = char;
cell.classList.add('gray');
}
cell.appendChild(span);
charRow.appendChild(cell);
}
if ((i + 1) % 11 === 0) {
charRow.classList.add('page-break');
}
gridContainer.appendChild(charRow);
const tempDiv = document.createElement('div');
document.body.appendChild(tempDiv);
const writer = new HanziWriter(tempDiv, {
width: 60,
height: 60,
showOutline: false,
});
writer.setCharacter(char).then(() => {
const strokes = writer._character.strokes;
const strokeCount = strokes.length;
const strokeCountSpan = document.createElement('span');
strokeCountSpan.textContent = `${strokeCount}画`;
strokeCountSpan.style.marginRight = '6px';
strokeContainer.appendChild(strokeCountSpan);
for (let k = 0; k < strokeCount; k++) {
const svg = createStrokeSVG(strokes, k);
svg.classList.add('stroke-svg');
strokeContainer.appendChild(svg);
}
document.body.removeChild(tempDiv);
}).catch(error => {
console.error(`无法加载汉字“${char}”的笔画数据:`, error);
});
}
}
document.getElementById('generate-btn').addEventListener('click', function() {
generateGrid();
});
document.getElementById('clear-btn').addEventListener('click', function() {
const input = document.getElementById('hanzi-input');
const gridContainer = document.getElementById('grid-container');
input.value = '';
gridContainer.innerHTML = '';
input.focus();
});
async function printToPDF() {
try {
const gridDiv = document.getElementById('grid-container');
const input = document.getElementById('hanzi-input').value;
const totalPages = Math.ceil(input.length / 11); // 计算总页数,每页11个字符
// 分割 gridDiv 的内容为每页11个字符
const rows = gridDiv.querySelectorAll('.row');
let pageContent = '';
for (let page = 0; page < totalPages; page++) {
const startIdx = page * 22; // 每页有11个拼音行和11个字符行,共22行
const endIdx = Math.min(startIdx + 22, rows.length);
let pageRows = '';
for (let i = startIdx; i < endIdx; i++) {
pageRows += rows[i].outerHTML;
}
pageContent += `
<div class="page">
<div class="header">练习字帖 帅小呆 第 ${page + 1} 页 共 ${totalPages} 页</div>
<div class="grid-container">${pageRows}</div>
</div>
`;
}
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page {
margin: 20px 20px 20px 20px; /* 设置明确的顶部边距 */
size: A4 portrait;
background-color: white;
}
@font-face {
font-family: 'KaiTi';
src: url('http://localhost:3000/fonts/TW-Kai.ttf') format('truetype');
}
@font-face {
font-family: 'TeXGyreAdventor';
src: url('http://localhost:3000/fonts/texgyreadventor-regular.otf') format('opentype');
font-display: swap;
}
html, body {
font-family: 'TeXGyreAdventor', 'KaiTi', serif;
text-align: center;
padding: 0;
margin: 0;
background-color: white;
}
.page {
page-break-after: always;
position: relative;
width: 100%;
padding-top: 40px; /* 增加顶部内边距以容纳页眉 */
}
.header {
position: absolute;
top: 15px; /* 增加顶部偏移,确保页眉可见 */
left: 0;
width: 100%;
text-align: center;
font-size: 16px;
color: #333;
background-color: white;
padding: 5px 0;
z-index: 10;
}
.grid-container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px; /* 确保内容不与页眉重叠 */
}
.row {
display: flex;
position: relative;
width: 540px;
}
.pinyin-row {
height: 28.8px;
display: flex;
align-items: center;
}
.pinyin-cell {
width: 60px;
height: 28.8px;
display: flex;
justify-content: center;
align-items: center;
}
.stroke-container {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
height: 28.8px;
align-items: center;
margin-left: 6px;
}
.stroke-svg {
width: 18.72px;
height: 18.72px;
margin-right: 4.68px;
z-index: 1;
}
.pinyin-row.page-first::before {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
top: 0px;
border: 1px solid #339933;
z-index: 0;
}
.pinyin-row::after {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
bottom: 0;
border: 1px solid #339933;
z-index: 0;
}
.pinyin-row .line1, .pinyin-row .line2 {
content: '';
position: absolute;
width: 540px;
height: 0;
left: 0;
border: 1px solid #339933;
z-index: 0;
opacity: 0.25;
}
.pinyin-row .line1 {
top: 9.6px;
border-style: dashed;
}
.pinyin-row .line2 {
top: 19.2px;
}
.pinyin-cell span {
font-family: 'TeXGyreAdventor' !important;
font-size: 16.8px;
position: relative;
z-index: 1;
}
.black span {
color: black;
opacity: 1;
}
.gray span {
color: #ccc;
}
.char-row {
margin-top: -1px;
}
.cell {
width: 60px;
height: 60px;
border-bottom: 1px solid #339933;
border-right: 1px solid #339933;
border-left: 1px solid #339933;
position: relative;
display: flex;
justify-content: center;
align-items: center;
font-size: 43.2px;
font-family: 'KaiTi', serif;
}
.cell:not(:first-child) {
border-left: none;
}
.char-row .cell {
border-top: 1px solid #339933;
}
.cell::before, .cell::after {
content: '';
position: absolute;
border: 1px dashed #339933;
z-index: 0;
opacity: 0.5;
}
.cell::before {
width: 100%;
height: 0;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.cell::after {
width: 0;
height: 100%;
left: 50%;
top: 0;
transform: translateX(-50%);
}
.diagonal1, .diagonal2 {
position: absolute;
width: 80px;
height: 0;
border: 0.25px dashed #339933;
z-index: 0;
opacity: 0.25;
}
.diagonal1 {
top: 0;
left: 0;
transform: rotate(48deg);
transform-origin: 0 0;
}
.diagonal2 {
bottom: 0;
left: 0;
transform: rotate(-48deg);
transform-origin: 0 100%;
}
.cell span {
position: relative;
z-index: 1;
}
.black span {
color: black;
opacity: 1;
}
.gray span {
color: #ccc;
}
.char-row.page-break {
page-break-after: always;
}
.replace(/url\(['"]?\.\/fonts/g, 'url(/fonts')
.replace(/url\(['"]?fonts/g, 'url(/fonts');
</style>
</head>
<body>
${pageContent}
</body>
</html>
`;
// 发送到服务器时携带字体状态
const fontStatus = {
KaiTi: document.fonts.check('16px KaiTi'),
TeXGyreAdventor: document.fonts.check('16px TeXGyreAdventor')
};
const response = await fetch('http://localhost:3000/generatepdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
},
body: JSON.stringify({
html: htmlContent,
timestamp: new Date().getTime()
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`PDF生成失败: ${errorText}`);
}
const blob = await response.blob();
const filename = `汉字练习-${new Date().toISOString().slice(0,10)}.pdf`;
saveAs(blob, filename);
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed; top:20px; right:20px; padding:12px; background:#339933; color:white; border-radius:5px;';
toast.textContent = '✓ PDF生成成功';
document.body.appendChild(toast);
setTimeout(() => document.body.removeChild(toast), 3000);
} catch (error) {
console.error('PDF生成错误:', error);
const errorDiv = document.createElement('div');
errorDiv.style.cssText = 'position:fixed; top:20px; left:20px; padding:12px; background:red; color:white; border-radius:5px;';
errorDiv.textContent = `错误: ${error.message}`;
document.body.appendChild(errorDiv);
setTimeout(() => document.body.removeChild(errorDiv), 5000);
}
}
</script>
</body>
</html>
前端和后台,index.html 和server.js
const express = require('express');
const puppeteer = require('puppeteer');
const path = require('path');
const fs = require('fs').promises;
const { v4: uuidv4 } = require('uuid');
const cors = require('cors');
const app = express();
const port = 3000;
// 配置CORS
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: false
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.static(path.join(__dirname, 'public')));
// 托管字体文件
app.use('/fonts', express.static(path.join(__dirname, 'public', 'fonts'), {
setHeaders: (res, filePath) => {
const mimeTypes = {
'.ttf': 'font/ttf',
'.otf': 'font/otf'
};
const ext = path.extname(filePath);
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
}
}));
app.get('/', (req, res) => {
res.send('服务器运行正常!请访问 /index.html 或点击“打印到 PDF”按钮生成 PDF。');
});
app.post('/generatepdf', async (req, res) => {
console.log('Received request to generate PDF');
const { html } = req.body;
if (!html) {
console.error('Missing HTML content in request body');
return res.status(400).send('Missing HTML content');
}
try {
console.log('Launching Puppeteer...');
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
console.log('Puppeteer launched successfully');
const page = await browser.newPage();
// 设置页面内容
console.log('Setting page content...');
await page.setContent(html, { waitUntil: 'networkidle0' });
console.log('Page content set');
// 等待字体加载
await page.waitForFunction(() => {
return document.fonts.check('16px KaiTi') &&
document.fonts.check('16px TeXGyreAdventor');
}, { timeout: 10000 });
console.log('Generating PDF...');
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
margin: { top: '20px', right: '20px', bottom: '20px', left: '20px' }
});
console.log('PDF buffer size:', pdfBuffer.length);
await browser.close();
console.log('PDF generated successfully');
// 保存PDF文件用于调试
const filename = `character-strokes-output-${uuidv4()}.pdf`;
await fs.writeFile(path.join(__dirname, filename), pdfBuffer);
console.log(`PDF saved to ${filename} for debugging`);
// 设置响应头并发送PDF
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfBuffer.length,
'Content-Disposition': 'attachment; filename="character_strokes.pdf"'
});
res.end(pdfBuffer, 'binary');
} catch (error) {
console.error('Failed to generate PDF:', error);
res.status(500).send('Failed to generate PDF: ' + error.message);
}
});
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});