Compare commits

..
2 Commits
Author SHA1 Message Date
hz4th_coder a6c8de5820 fix: 修复表格模式无法显示的问题
1. 修复 registerFont API 调用 - @napi-rs/canvas 使用 GlobalFonts.registerFromPath 而非 registerFont
2. 确保 Express 服务端正确启动(之前误用 Python http.server 导致 POST 请求返回 501)
2026-07-24 16:17:32 +08:00
hz4th_coder 5f9357a182 feat: 新增表格边框样式控制功能
支持10种边框样式:
- all: 全部边框(默认)
- none: 无边框
- no-outer: 无外边线
- no-horizontal: 无横线
- no-vertical: 无竖线
- no-inner: 无内部线
- no-inner-horizontal: 无内部横线
- no-inner-vertical: 无内部竖线
- outer-only: 仅外边框
- header-only: 仅表头分隔线

前端和API均已支持
2026-07-17 13:37:43 +08:00
3 changed files with 65 additions and 28 deletions
+1
View File
@@ -673,6 +673,7 @@ function generateTable() {
theme: document.getElementById('tableTheme').value, theme: document.getElementById('tableTheme').value,
fontSize: parseInt(document.getElementById('tableFontSize').value) || 14, fontSize: parseInt(document.getElementById('tableFontSize').value) || 14,
stripeRows: document.getElementById('stripeRows').checked, stripeRows: document.getElementById('stripeRows').checked,
borderStyle: document.getElementById('borderStyle').value,
pixelRatio: 2 pixelRatio: 2
}; };
+16
View File
@@ -151,6 +151,22 @@ C, 20, 30, 40</pre>
<label><input type="checkbox" id="stripeRows" checked onchange="updateTable()"> 斑马纹</label> <label><input type="checkbox" id="stripeRows" checked onchange="updateTable()"> 斑马纹</label>
</div> </div>
</div> </div>
<div class="config-group">
<label>边框样式</label>
<select id="borderStyle" onchange="updateTable()">
<option value="all">全部边框</option>
<option value="none">无边框</option>
<option value="no-outer">无外边线</option>
<option value="no-horizontal">无横线</option>
<option value="no-vertical">无竖线</option>
<option value="no-inner">无内部线</option>
<option value="no-inner-horizontal">无内部横线</option>
<option value="no-inner-vertical">无内部竖线</option>
<option value="outer-only">仅外边框</option>
<option value="header-only">仅表头分隔线</option>
</select>
</div>
</div> </div>
<!-- 系列配置区 --> <!-- 系列配置区 -->
+48 -28
View File
@@ -6,8 +6,10 @@ const path = require('path');
// 注册中文字体 // 注册中文字体
try { try {
registerFont('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', { family: 'Noto Sans CJK SC' }); const { GlobalFonts } = require('@napi-rs/canvas');
registerFont('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc', { family: 'Noto Sans CJK SC', weight: 'bold' }); GlobalFonts.registerFromPath('/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', 'Noto Sans CJK SC');
GlobalFonts.registerFromPath('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc', 'Noto Sans CJK SC');
console.log('中文字体注册成功');
} catch (e) { } catch (e) {
console.warn('中文字体注册失败,将使用系统默认字体:', e.message); console.warn('中文字体注册失败,将使用系统默认字体:', e.message);
} }
@@ -655,7 +657,8 @@ function generateTableImage(params) {
borderWidth = 1, borderWidth = 1,
stripeRows = true, stripeRows = true,
pixelRatio = 2, pixelRatio = 2,
maxWidth = 1200 maxWidth = 1200,
borderStyle = 'all' // all, none, no-outer, no-horizontal, no-vertical, no-inner, no-inner-horizontal, no-inner-vertical, outer-only, header-only
} = params; } = params;
const tableData = parseTableData(data); const tableData = parseTableData(data);
@@ -763,35 +766,50 @@ function generateTableImage(params) {
ctx.strokeStyle = themeConfig.borderColor; ctx.strokeStyle = themeConfig.borderColor;
ctx.lineWidth = borderWidth; ctx.lineWidth = borderWidth;
// 边框(向内偏移半个线宽,确保边框完全在画布内) // 边框样式控制
const offset = borderWidth / 2; const drawOuter = !['none', 'no-outer', 'no-inner', 'no-inner-horizontal', 'no-inner-vertical', 'header-only'].includes(borderStyle);
ctx.strokeRect(offset, startY + offset, finalWidth - borderWidth, tableHeight - borderWidth); const drawHorizontal = !['none', 'no-horizontal', 'no-inner', 'no-inner-horizontal', 'outer-only', 'header-only'].includes(borderStyle);
const drawVertical = !['none', 'no-vertical', 'no-inner', 'no-inner-vertical', 'outer-only'].includes(borderStyle);
const drawHeaderLine = !['none', 'no-horizontal', 'no-inner', 'no-inner-horizontal'].includes(borderStyle);
// 横线 // 外边框
ctx.beginPath(); if (drawOuter) {
ctx.moveTo(0, startY + headerHeight); const offset = borderWidth / 2;
ctx.lineTo(finalWidth, startY + headerHeight); ctx.strokeRect(offset, startY + offset, finalWidth - borderWidth, tableHeight - borderWidth);
ctx.stroke(); }
rows.forEach((_, rowIdx) => { // 表头分隔线(横线)
const y = startY + headerHeight + (rowIdx + 1) * rowHeight; if (drawHeaderLine) {
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, y); ctx.moveTo(0, startY + headerHeight);
ctx.lineTo(finalWidth, y); ctx.lineTo(finalWidth, startY + headerHeight);
ctx.stroke(); ctx.stroke();
}); }
// 数据行横线
if (drawHorizontal) {
rows.forEach((_, rowIdx) => {
const y = startY + headerHeight + (rowIdx + 1) * rowHeight;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(finalWidth, y);
ctx.stroke();
});
}
// 竖线 // 竖线
x = 0; if (drawVertical) {
colWidths.forEach((width, i) => { x = 0;
if (i > 0) { colWidths.forEach((width, i) => {
ctx.beginPath(); if (i > 0) {
ctx.moveTo(x, startY); ctx.beginPath();
ctx.lineTo(x, startY + tableHeight); ctx.moveTo(x, startY);
ctx.stroke(); ctx.lineTo(x, startY + tableHeight);
} ctx.stroke();
x += width; }
}); x += width;
});
}
return canvas; return canvas;
} }
@@ -833,7 +851,8 @@ app.get('/api/table', (req, res) => {
borderWidth: parseInt(req.query.borderWidth) || 1, borderWidth: parseInt(req.query.borderWidth) || 1,
stripeRows: req.query.stripeRows !== 'false', stripeRows: req.query.stripeRows !== 'false',
pixelRatio: parseInt(req.query.pixelRatio) || 2, pixelRatio: parseInt(req.query.pixelRatio) || 2,
maxWidth: parseInt(req.query.maxWidth) || 1200 maxWidth: parseInt(req.query.maxWidth) || 1200,
borderStyle: req.query.borderStyle || 'all'
}; };
if (!params.data) { if (!params.data) {
@@ -951,7 +970,8 @@ app.get('/api/docs', (req, res) => {
borderWidth: { type: 'number', default: 1, description: '边框宽度' }, borderWidth: { type: 'number', default: 1, description: '边框宽度' },
stripeRows: { type: 'boolean', default: true, description: '是否斑马纹' }, stripeRows: { type: 'boolean', default: true, description: '是否斑马纹' },
pixelRatio: { type: 'number', default: 2, description: '像素倍率(清晰度)' }, pixelRatio: { type: 'number', default: 2, description: '像素倍率(清晰度)' },
maxWidth: { type: 'number', default: 1200, description: '最大宽度(px)' } maxWidth: { type: 'number', default: 1200, description: '最大宽度(px)' },
borderStyle: { type: 'string', default: 'all', options: ['all', 'none', 'no-outer', 'no-horizontal', 'no-vertical', 'no-inner', 'no-inner-horizontal', 'no-inner-vertical', 'outer-only', 'header-only'], description: '边框样式' }
}, },
returns: 'image/png', returns: 'image/png',
example: { example: {