技术标签: c# .net 从零开始搭建报表工具 开源 程序人生 云计算 产品经理
本篇文章是从零开始搭建报表工具系列文章的第一篇,这一篇主要实现一下实际工作环境中真实遇到的问题,在实际工作中经常会存在需要把 Excel
的数据转化为Word
文档的操作,利用 Excel
中作为数据源为报表提供数据支撑。为此,本章将优先开发数据填报部分。
Excel
导入方式实现。体验网址:http://121.41.170.62/login
项目还在一点点完善中,仅供学习参考!可能无法访问。
当前Excel数据源仅支持静态表格
,如果你的数据源是列表方式,请查看后续文章动态表单
部分。
静态表格:不存在任何新增行或插入列的表格,所有的位置都是固定不变的。
准备一个Excel静态表格
作为数据源,这里以一个授权委托书.xlsx
为例,内容像这样
进入模板制作\输入模板\新增模板,输入模板名称并上传此授权委托书.xlsx
,提交保存。
Word文件
作为输出,这里以一个授权委托书.docx
为例,内容像这样当然这不是最终形态,我们需要在输出模板中加上标记,方便系统找到位置,系统采用双英文括号作为标记,像这样:{
{标记}}
,做完标记的模板长这样
进入模板制作\输出模板\找到刚上传的授权委托书
,点击上传,提交保存。
点击绑定,绑定相对应的位置,可点击静态按钮唤醒输入模板直接选择。应用完成绑定。
在线填报\选择使用模板,选择授权委托书-【单次采集】
,在绑定的对应位置添加数据
点击开始生成
按钮等待成功,成功会自动下载。
使用Excel文件获取Html代码,再使用前端进行渲染。
public static clsGeneralResponse ConvertExcelToHtml(string filePath)
{
clsGeneralResponse r = new clsGeneralResponse();
try
{
StringBuilder sb = new StringBuilder();
using (FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
var workbook = new XSSFWorkbook(file);
var sheet = workbook.GetSheetAt(0);
sb.Append("<table><tbody>");
int maxRowCount = 0;
int maxColumnCount = 0;
for (int i = 0; i <= sheet.LastRowNum; i++)
{
maxRowCount++;
var row = sheet.GetRow(i);
if (row == null)
{
continue;
}
int columnCount = row.LastCellNum;
if (columnCount > maxColumnCount)
{
maxColumnCount = columnCount;
}
}
int mergedRegionsCount = sheet.NumMergedRegions;
bool[,] mergedCells = new bool[maxRowCount, maxColumnCount];
for (int i = 0; i < mergedRegionsCount; i++)
{
var mergedRegion = sheet.GetMergedRegion(i);
int startRow = mergedRegion.FirstRow;
int endRow = mergedRegion.LastRow;
int startColumn = mergedRegion.FirstColumn;
int endColumn = mergedRegion.LastColumn;
for (int row = startRow; row <= endRow; row++)
{
for (int col = startColumn; col <= endColumn; col++)
{
mergedCells[row, col] = true;
}
}
}
for (int i = 0; i < maxRowCount; i++)
{
var row = sheet.GetRow(i);
sb.Append("<tr style=\"text-align:center\">");
for (int j = 0; j < maxColumnCount; j++)
{
var col_name = GetExcelColumnHeader(j);
var cellvalue = "";
if (row == null)
{
}
else
{
var cell = row.GetCell(j);
cellvalue = (cell?.ToString() ?? "");
}
var td = "<td class=\"selectTd\" location=\"" + (col_name + (i + 1)) + "\" lable style=\"display:none;\">" + cellvalue + "</td>";
if (cellvalue.Contains("Alert:"))
{
td = "<td οnclick=\"EventAction(this,'" + cellvalue + "')\" class=\"selectTd\" location=\"" + (col_name + (i + 1)) + "\" lable style=\"display:none;\"></td>";
}
if (!mergedCells[i, j])
{
td = td.Replace("display:none;", "").Replace("lable", "");
}
else
{
int colspan = 1;
int rowspan = 1;
for (int k = 0; k < mergedRegionsCount; k++)
{
var mergedRegion = sheet.GetMergedRegion(k);
int startRow = mergedRegion.FirstRow;
int endRow = mergedRegion.LastRow;
int startColumn = mergedRegion.FirstColumn;
int endColumn = mergedRegion.LastColumn;
if (i == startRow && j == startColumn)
{
rowspan = mergedRegion.LastRow - mergedRegion.FirstRow + 1;
colspan = mergedRegion.LastColumn - mergedRegion.FirstColumn + 1;
td = td.Replace("display:none;", "").Replace("lable", "colspan=\"" + colspan + "\" rowspan=\"" + rowspan + "\"");
}
else
{
continue;
}
}
}
sb.Append(td);
}
sb.Append("</tr>");
}
}
HtmlDocument doc = new HtmlDocument();
doc.LoadHtml(sb.ToString());
var rows = doc.DocumentNode.SelectNodes("//tr");
var rowscount = rows.Count;
int maxCols = 0;
if (rows != null)
{
foreach (var row in rows)
{
int cols = row.SelectNodes(".//td")?.Count ?? 0;
maxCols = Math.Max(cols, maxCols);
}
}
if (rowscount < 10)
{
for (int i = 0; i < 10; i++)
{
var td = "";
for (int j = 0; j < maxCols; j++)
{
td += "<td></td>";
}
sb.Append("<tr>" + td + "</tr>");
}
}
sb.Append("</tbody></table>");
r.Code = 200;
r.Msg = "ok";
r.Data = sb.ToString();
}
catch (Exception ex)
{
r.Code = 500;
r.Msg = ex.Message;
r.Data = "error";
}
return r;
}
前端异步获取HTML并渲染,后端返回的HTML为Table 组件部分
。
$.ajax({
url: '/api/xxx/getHtml', // 你的处理HTML的服务器端url
type: 'POST',
data: JSON.stringify({
temid: '模板id' }),
dataType: 'json',
contentType: 'application/json',
success: function (response) {
if (response.code == 200) {
console.log(response.data)
//此处需自行渲染HTML到你的容器中,并处理允许编辑TD,方便采集录入数据
.....
} else {
alert(response.msg, "error");
}
},
error: function (error) {
console.log(error);
}
});
提交数据逻辑,使用Td
标签的location
标记作为name
进行提交,value
为当前Td
的内容。
var cellData = [];
$("tr").each(function (rowIndex, rowElement) {
if (rowIndex != 0) {
$(rowElement).find("td").each(function (cellIndex, cellElement) {
//location标记了表格原始位置 A1
var cellLocation = $j(cellElement).attr("location");
if (cellLocation !== undefined) {
var cellValue = $(cellElement).text();
cellData.push({
name: cellLocation,
value: cellValue
})
}
});
}
});
接收前端提交数据,后端把采集的数据放到绑定位置中,并返回文件。
此处使用了DocumentFormat.OpenXml及MiniWord,用来完成读取Word标记及替换标记内容。
[Authorize]
[HttpPost("/api/xxx/start")]
[ServiceFilter(typeof(RequestAuditFilter))]
public async Task<IActionResult> StartReportGeneration([FromBody] GenerateReportsParam generateParams)
{
try
{
var template = await _context.TemplateTable.Find(generateParams.TemplateId);
if (template == null || string.IsNullOrEmpty(template.OutputBind))
{
var templateErrorMsg = template == null ? "未找到该模板." : "检测到输出模板未进行绑定操作.";
return Json(SetResult(500, templateErrorMsg, "error"));
}
var keyValues = JsonConvert.DeserializeObject<List<KeyValue>>(template.OutputBind ?? "");
var renderItems = generateParams.Data.Select(item => new RenderItems {
Key = item.Name, Value = item.Value }).ToList();
var renderDictionary = keyValues.ToDictionary(kv => kv.Key, kv => (object)(renderItems.FirstOrDefault(ri => ri.Key == kv.Value)?.Value ?? ""));
var templateDirectory = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", template.OutputFilepath);
var datePath = DateTime.Now.ToString("yyyy-MM-dd");
var newFile = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + ".docx";
var reportDirectory = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", datePath, UserId, "report");
var reportPath = Path.Combine(reportDirectory, newFile);
Directory.CreateDirectory(reportDirectory);
MiniWord.SaveAsByTemplate(reportPath, templateDirectory, renderDictionary);
var memoryStream = new MemoryStream();
using (var fileStream = new FileStream(reportPath, FileMode.Open))
{
fileStream.CopyTo(memoryStream);
}
memoryStream.Position = 0;
var reportFileName = (string.IsNullOrEmpty(template.Name) ? "" : template.Name) + DateTime.Now.ToString("yyyyMMddHHmmss") + ".docx";
var reportRelativePath = $"uploads/{
datePath}/{
UserId}/report/{
newFile}";
_context.Add(new TemplateRecordTable
{
Id = Guid.NewGuid().ToString(),
Userid = Convert.ToInt32(UserId),
Tmeid = template.Id,
Name = reportFileName,
Data = JsonConvert.SerializeObject(generateParams.Data),
Inputpath = template.InputFilepath,
Outputpath = template.OutputFilepath,
OutputType = 0,
Outputfile = reportRelativePath,
Addtime = DateTime.Now
});
await _context.SaveChangesAsync();
return File(memoryStream, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", reportFileName);
}
catch (Exception ex)
{
return Json(SetResult(500, ex.Message, "error"));
}
}
开始发送请求,等待后端返回文件。
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/xxx/start', true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem('token'));
xhr.setRequestHeader('Content-type', 'application/json');
xhr.onload = function (e) {
if (this.status == 200) {
var blob = this.response;
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
//处理文件名称
const now = new Date();
const date = now.getDate().toString();
const time = now.toTimeString().substring(0, 5);
var contentDisposition = xhr.getResponseHeader('Content-Disposition');
var filename = '默认文件名' + date + time + '.docx'; // 这是默认文件名
if (contentDisposition) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
link.download = filename;
link.click();
} else if (this.status == 401) {
//登录失效...
} else if (this.status == 403) {
//请求受限,今日机会已用尽.
}
else if (this.status == 500) {
//生成失效...
}
};
xhr.send(JSON.stringify(json));
文章浏览阅读6.2k次。.xsa文件的生成Vivado工程建立你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。1.我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:全新的界面设计 ,将会带来全新的写作体验;在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;增加_.xsa
文章浏览阅读475次。一、HTMLParser.net是什么?HTMLParser.net是HtmlParser的JAVA版本的dot net版本。二、HTMLParser可以用来做什么?HtmlParser是用来改造或者提取HTML,通过HtmlParser可以高速,快捷的从Html页面中分离出你想要的内容。三、HTMLParser的核心模块是org.htmlparser.Parser类,这个类实际完成了对于HTML_htmlparser.net
文章浏览阅读1.3w次,点赞6次,收藏48次。1. 简介QEMU(quick emulator)是一款由法布里斯·贝拉(Fabrice Bellard)等人编写的免费的可执行硬件虚拟化的(hardware virtualization)开源托管虚拟机(VMM)。QEMU 是一个托管的虚拟机镜像,它通过动态的二进制转换,模拟CPU,并且提供一组设备模型,使它能够运行多种未修改的客户机OS,可以通过与KVM一起使用进而接近本地速度运行虚拟机(接近真实电脑的速度)。QEMU还可以为user-level的进程执行CPU仿真,进而允许了为一种架构编译的程序_qemu
文章浏览阅读1.4w次,点赞14次,收藏78次。软件:Visual C++版本:6.0语言:简体中文大小:34.26M安装环境:Win11/Win10/Win8/Win7硬件要求:[email protected] 内存@4G(或更高)下载通道①百度网盘丨下载链接:提取码:dg2n[更多软件]:点击进入管家「软件目录」!_visual c++安装教程
文章浏览阅读2.7w次,点赞2次,收藏8次。新路由3 newifi3 d2 高恪魔改固件,请在breed中先刷入底包,然后启动路由器进入底包系统后,再在底包系统里面网页web升级固件,选择魔改进行升级,切记必须这样操作。压缩包包含了底包和固件解压密码 123下载地址:https://u13909188.pipipan.com/fs/13909188-384246318..._新路由3高恪5.0nat1
文章浏览阅读298次。导读:我们生活在一个嘈杂、混乱的世界中。生活中,我们有很多“权威”和“专家”,他们标榜自己是内行人,宣称自己掌握着该领域的真理,而我们需要做的只有两个字——接受。但事实上..._唯快不破的人为什么定
文章浏览阅读7.6k次,点赞2次,收藏9次。Instrumentation是一种直接修改程序二进制文件的方法。其可以用于程序的调试,优化,安全等等。对这个词一般的翻译是“插桩”,但这更多使用于软件测试领域。【找一些相关的例子】Dyninst可以动态或静态的修改程序的二进制代码。动态修改是在目标进程运行时插入代码(dynamic binary instrumentation)。静态修改则是直接向二进制文件插入代码(static b_dyninst
文章浏览阅读2.9k次。部署asp网站到云服务器 内容精选换一换通常情况下,需要结合客户的实际业务环境和具体需求进行业务改造评估,建议您进行服务咨询。这里仅描述一些通用的策略供您参考,主要分如下几方面进行考虑:业务迁移不管您的业务是否已经上线华为云,业务迁移的策略是一致的。建议您将时延敏感型,有快速批量就近部署需求的业务迁移至IEC;保留数据量大,且需要长期稳定运行的业务在中心云上。迁移方法请参见如何计算隔离独享计算资源..._nas asp网站
文章浏览阅读4.7k次。/** 方法一 * 将bitmap转为数组的方法 * * @param bitmap 图片 * @return 返回数组 */ public byte[] getBytesByBitmap(Bitmap bitmap) { ByteBuffer buffer = ByteBuffer.allocate(bitmap.ge..._bitmap转数组
文章浏览阅读6.6k次,点赞2次,收藏6次。IDEA修改SVN地址 SVN地址改变了,在IDEA上的项目地址还没有修改 第一步:选中项目,右键Subversion --> Relocate第二步:From URL路径保持不变(修改To URL为最新路径)第三步:选中项目,右键Subversion --> Update Directory第四步:勾选Update修改URL为最新的即可SVN地址改变了,在IDEA上的项目地址还没有修改_idea修改svn地址
文章浏览阅读3.8k次。欧拉图及欧拉路径欧拉图 如果图G上有一条经过所有顶点、所有边的闭路径(边不重复,顶点可以重复)充分必要条件 无向图:G连通,所有顶点的度都是偶数有向图:G弱连通,每个顶点出度与入度相等欧拉路径 如果图G上有一条经过所有顶点、所有边的路径(边不重复,顶点可以重复)充分必要条件 无向图:G连通,恰有两个顶点的度是奇数有向图:G连通,恰有两个顶点的出度与入度不相等,其中一个出度比入度多_哈密顿通路度为偶数
文章浏览阅读68次。两个命令:svn info :显示版本库信息,svn的下载url等。svn co https://xxxxx/xxx wodemulu (通过我的目录制定co的文件夹)svn st:显示修改的文件。=-=========================================第一章 安装1. 采用源文件编译安装。源文件共两个(可下载完传入linux),为:s..._can't lunch modelsim make sure