简介:《Smartbi报表设置文档》面向使用Smartbi制作电子表格报表的设计人员与开发人员,围绕报表从模板引用到预览发布的完整设置流程展开,适合需要规范报表样式、补全参数校验逻辑的初、中级使用者参考。资源包共1个docx文件,约1.05MB,以图文步骤与可复用代码片段为主,体量轻便,便于随查随用。内容覆盖引用模板、页面设置与冻结行处理、隔行不同颜色、列宽自适应、参数设置与参数排版等环节,其中列宽全局自适应给出可直接套用的JavaScript宏,结束日期不早于开始日期且间隔不超过90天的参数校验也附有完整宏示例,并提示参数筛选项居左、单行不超过6个等排版细节。无论是首次搭建报表样式,还是为已有报表补充交互校验与版式优化,都能从中找到对应做法。目前已有1304人学习下载,适合作为Smartbi报表设置的操作手册与宏脚本参考。
1. 一份 Excel 里的 Smartbi 页签,撑起整套报表设置流程
多数人做报表的顺序是反的:先在 Excel 里把样式画满,回头补参数、补刷新逻辑,最后发现预览时列宽全炸开,结束日期比开始日期还早也能查出一堆数据。这份 Smartbi 报表设置文档走的是另一条路——先在 Excel 的 Smartbi 页签里引用模板把骨架钉死,再叠页面设置、隔行换色、列宽自适应脚本、参数校验宏,最后才谈参数排版和预览发布。它面向的是每天要在 Excel 里出固定格式报表的人,尤其适合接手别人模板、天天被追问“格式为什么不一致”的岗位。下面按真实操作顺序拆,代码段可以直接抄。
2. 引用模板与页面设置:把报表骨架先钉死
报表出问题的根源,八成不在最后一步预览,而在最初五分钟:模板引错了、冻结行算错了、隔行色填充方式选错了。这三件事一旦定下来,后面所有脚本都是在它们上面打补丁,补丁越多越难维护。所以这一章不讲宏,只讲骨架怎么搭才算稳。
2.1 模板引用:先另存为,再动第一笔
Smartbi 页签下的“模板”是一个共享素材入口,里面通常已经绑好了数据集、表头样式、单元格格式、参数控件占位。正确动作是打开模板之后立刻另存为一份自己的报表,再在副本上改样式。直接改模板的后果不是当场报错,而是下次别人引用同一份模板时,发现表头被你换成了另一套字段名,排查起来毫无线索。
| 动作 | 操作位置 | 注意点 |
|---|---|---|
| 登录 | Excel → Smartbi 页签 → 登录 | 服务器地址与账号由管理员分配,切环境后必须重新登录,否则模板列表是空的 |
| 打开模板 | Smartbi 页签 → 模板 → 选分类 → 打开 | 打开即挂载数据集,此时改样式会污染共享素材 |
| 另存新报表 | 报表另存为 / 另存到个人目录 | 命名带业务名和日期,例如“销售明细_月度_202401” |
| 校验绑定 | 数据区右键查看数据集字段 | 字段名和类型必须与模板一致,类型被改过会导致数字列变文本 |
引用完模板,第一件事不是改颜色,是确认三样东西:数据集连接是否指向你自己的库、参数控件是否已经生成占位单元格、表头行里有没有残留的上一个业务字段。常见的做法是把模板里的示例数据先刷一遍,看预览能不能出数,能出数再改样式。如果模板一打开预览就报数据集无权限,先找管理员要权限,别急着在 Excel 里改公式——那不是样式问题。
另存的时候建议保留一份“原始模板副本”不动,只在自己的报表上迭代。我见过太多人把模板改成四不像之后,想回头看看原版长什么样,结果模板本身也被覆盖了,只能去回收站捞。这类后悔药没必要吃。
2.2 页面设置:冻结 1-5 行的行号账
冻结这件事看起来最简单,也最容易算错一行。文档里写得很明确:第一行是空白行,所以冻结的是 1-5 行。这意味实际冻结线落在第 6 行的顶端,不是第 5 行。Excel 里选中第 5 行点冻结,冻住的是 1-4 行;选中第 6 行点冻结,才是 1-5 行。差一行,预览的时候表头就会跟着滚走半截。
操作路径就三步:选中 A6 单元格 → 视图 → 冻结窗格 → 冻结拆分窗格。为什么强调选 A6 而不是选第 6 行整行?因为冻结同时会锁列,选中 A6 相当于只锁行不锁列,参数区在左侧时不会被一起冻住;如果参数是竖排在左边一列,那就该选 B6,把第一列也锁上。这个细节文档没展开,但实际排版里躲不掉。
第一行为什么要留空白,值得单独说一句。Smartbi 渲染报表时,参数控件区、工具条提示、行内按钮都要占位,留一行空行相当于给渲染层留了缓冲带。有人图省事把表头直接写在第 1 行,冻结是省了,但参数多的时候控件会压到表头上,看起来像数据错位,实际是布局没留余量。
| 设置项 | 取值 | 说明 |
|---|---|---|
| 冻结窗格 | 选中 A6 后冻结 | 锁 1-5 行;参数在左列时改选 B6 |
| 顶端标题行 | $1:$5 | 打印时每页重复表头 |
| 缩放 | 调整为 1 页宽 | 纵向页数不限,横向压到一页,避免列被截断 |
| 页边距 | 窄 | 宽表必须窄边距,否则最后一列总差几毫米 |
打印标题行和冻结行是两套配置,互不影响。冻结只管屏幕上滚,打印标题行只管纸面上重复。有人以为冻了 1-5 行打印就会自动重复表头,结果打出来第二页直接是数据,回头还得重新设一遍。
2.3 隔行不同颜色:条件格式优先,别手动填色
隔行换色能让长报表的横向阅读不串行,这个需求本身没问题,问题在实现方式。手动隔一行填一次色的做法,在行数固定的静态表上看着没问题,一旦数据行数变化,颜色就和数据错位了:原来第 6 行是浅灰,数据多了一行之后,原本该是浅灰的那行变成白底,整张表的斑马纹从中间断掉。
推荐用条件格式。选中数据区A6:Z2000(上界按你预估的最大行数取,取大一点没坏处),开始 → 条件格式 → 新建规则 → 使用公式确定要设置格式的单元格,公式填:
// 判断当前行是否为偶数行,偶数行填充浅色 =MOD(ROW(),2)=0格式里把填充色设成一个固定的浅灰,比如 RGB(242,242,242),不要用主题色。原因是主题色在导出 Excel 或者别的客户端打开时,跟随主题变化,斑马纹可能变成蓝的、绿的,甚至和表头色撞上。固定 RGB 值走哪都是同一个颜色。
设置的时候有两个细节容易翻车。第一是起始行的奇偶:条件格式的 ROW() 返回的是绝对行号,如果你的数据从第 6 行开始,第 6 行是偶数,第一行数据就会被染色,视觉上像是“表头下面第一行是灰的”,需要把公式改成=MOD(ROW(),2)=1来错开。第二是应用范围不要从 A1 开始选,选了会把表头一起卷进去,表头本身已经有底色,再叠一层条件格式,结果是表头变成斑马纹的第一段。
| 方式 | 行数变化时 | 导出表现 | 维护成本 |
|---|---|---|---|
| 手动填色 | 颜色错位 | 稳定 | 每次改行数都要重填 |
| 条件格式(固定 RGB) | 自动跟随 | 稳定 | 设一次不用管 |
| 条件格式(主题色) | 自动跟随 | 可能变色 | 需要额外验证 |
数据区如果有合并单元格,条件格式按合并区域左上角单元格的行号判定,整块合并区会用同一个颜色,这时候斑马纹会看起来“一块一块”的。这种表就别硬上隔行色,改成按分组合并加边框更稳。
3. 列宽自适应:一段通用 main 函数怎么改才不出事
列宽是报表里最玄学的一环。同一份报表,在你机器上列宽刚好,到别人机器上第一列就被挤成三个字换一行。这不是 Smartbi 的锅,多半是自适应脚本写得太“全局”——它把所有列都按内容撑到最大,而某个备注字段里恰好有一条三百字的说明。这一章把那段通用 main 函数拆开,说清每一行的含义、哪里该改、边界在哪。
3.1 autoFitColumns 的调用链与 counts 的取值
文档给的通用代码如下:
function main(spreadsheetReport) { var sheet = spreadsheetReport.workbook.worksheets.get(0); if (sheet === "" || sheet === null) { return; } var counts = sheet.cells.maxDisplayRange.columnCount; // 获取电子表格列数 sheet.autoFitColumns(0, counts); // 从 0 开始,到 counts 列结束 // sheet.autoFitColumn(3); // 只对某列进行自适应,第一列为 0 }逐行说。spreadsheetReport.workbook是当前报表挂载的工作簿对象,后面所有取 sheet、取单元格的动作都从这里出发。worksheets.get(0)取第一张工作表,索引从 0 开始,所以第一个 sheet 是 0 不是 1。maxDisplayRange是当前有内容的最大显示区域,columnCount是这个区域的列数——注意它是“数”,不是“最大索引”,10 列内容的 columnCount 是 10,最大索引是 9。
autoFitColumns(0, counts)的意思是从第 0 列开始,一直自适应到第 counts 列。这里传 counts 而不是 counts-1,多算一列一般无害,因为超出内容范围的列自适应后宽度也不会异常;但如果你的表右边紧挨着另一个区域,多这一列可能把它也一起撑开。
第一段判空sheet === "" || sheet === null是老写法,实际取不到 sheet 时返回的是undefined或者空对象,跟空字符串比永远不相等。稳妥点改成:
function main(spreadsheetReport) { var sheet = spreadsheetReport.workbook.worksheets.get(0); if (!sheet) { // 取不到 sheet 直接退出,避免后续报 null 引用 return; } var counts = sheet.cells.maxDisplayRange.columnCount; if (!counts || counts <= 0) { // 空表没有可自适应的列,直接返回 return; } sheet.autoFitColumns(0, counts); }参数说明:counts来自maxDisplayRange,如果报表刚打开还没渲染完,这个值可能是 0 或者很小的数,自适应会失效,表现为“第一次打开列宽不对,点一次刷新就正常了”。遇到这种时序问题,别去改脚本逻辑,先确认这段宏挂在哪个事件上——挂在报表加载完成后触发的宏里,比挂在渲染前稳得多。
3.2 局部列自适应:调试期先单列试
全局自适应写起来省事,调起来痛苦:你不知道是哪一列把整张表撑爆的。调试阶段建议把全局那行注释掉,改用单列自适应,一列一列试:
function main(spreadsheetReport) { var sheet = spreadsheetReport.workbook.worksheets.get(0); if (!sheet) { return; } // 调试期只对第 4 列(索引 3)自适应,其余列保持手工设定的宽度 sheet.autoFitColumn(3); }autoFitColumn(3)里的 3 是列索引,第一列是 0,所以第 4 列对应 3。这个数字和 Excel 里的列号差 1,A 列是 0,B 列是 1,很容易看错。调的时候建议在 Excel 里先看一眼要调的列是第几列(A=1),减 1 再填进去。
调的顺序我一般是这样:先把关键列(金额、数量、日期)单独自适应,看它们撑到多宽;再放开全局自适应,对比差异,差异特别大的那几列就是罪魁祸首,单独给它们设固定宽度或者加截断。这个套路比盯着整张表瞎猜快得多。
3.3 边界:合并单元格、超长文本和性能
自适应不是万能的,它的判定依据是单元格里的文本长度和字体,遇到下面三种情况就会失灵。
合并单元格。自适应只认单个单元格的内容,跨列合并的区域它按最左列的宽度算,结果就是合并区看起来被压扁或者被拉得特别宽。表格里如果有大段说明放在合并单元格里,最好把这列从自适应范围里排除掉,手动定宽。
超长文本。一个备注字段里塞了三百字,自适应会把这列撑到几百像素,整张表横向要滚动才能看完。解决办法不是不给它自适应,而是自适应之后做一次收口:
function main(spreadsheetReport) { var sheet = spreadsheetReport.workbook.worksheets.get(0); if (!sheet) { return; } var counts = sheet.cells.maxDisplayRange.columnCount; var limit = Math.min(counts, 30); // 只处理前 30 列,后面的列不动 sheet.autoFitColumns(0, limit); for (var i = 0; i < limit; i++) { var w = sheet.getColumnWidth(i); if (w > 320) { // 320 像素是经验上限,超过就压回来 sheet.setColumnWidth(i, 320); } } }limit用Math.min兜一下,防止列数特别多的时候逐列遍历拖慢渲染;320这个上限按你的字号和屏幕宽度调,一般正文 11 号字、320 像素能放下二十来个汉字,够用。收口之后超长文本会显示不全,配合单元格的自动换行(不是自适应宽度,是 wrap text)一起用,行高会自动撑开,比横向滚动友好。
性能。列数上百、数据行几千的表,全局自适应会明显卡顿,因为它是逐列测量。常见的优化是只对表头行和数据前若干行做采样,或者干脆关掉自适应、改成按字段类型预设宽度(金额列 90、日期列 100、文本列 180),一次配好不再动。后者牺牲一点灵活性,换来的是每次打开都快。
4. 参数联动校验:90 天窗口的宏怎么写才不翻车
参数这块的需求通常很朴素:结束日期不能早于开始日期,且两者相差不超过 90 天。听起来两行 if 就完了,实际写起来会撞上三个问题——怎么拦住刷新、日期字符串怎么解析、报错提示为什么用户看不见。这一章按这三个问题拆。
4.1 重写 doRefresh:先存原函数,再做守卫
校验必须挂在“刷新”这个动作上,而不是参数控件的值变化事件上。原因是用户可能先改开始日期、再改结束日期,中间那一次值变化是合法但无意义的组合,在这个时机拦他属于误伤。点刷新才是一次完整的查询意图,这时候校验最准。
doRefresh是刷新入口,重写它的标准写法是先保存原函数引用:
function main(spreadsheetReport) { // 只在第一次进入时保存原始刷新函数,避免重复包装导致递归 if (!spreadsheetReport._jhy_doRefresh) { spreadsheetReport._jhy_doRefresh = spreadsheetReport.doRefresh; } spreadsheetReport.doRefresh = function (fromButton, delayMask) { // 校验逻辑放在这里,不通过就直接 return,不再往下走 // ... this._jhy_doRefresh(fromButton, delayMask); // 校验通过,调原始函数真正刷新 }; }_jhy_doRefresh这个属性名是自定义的,换个名字也行,作用是暂存原函数。如果没有if (!...)这层守卫,宏每执行一次就把当前(已经被包装过的)doRefresh再存一遍,包装层越套越多,点一次刷新弹好几次提示,甚至栈溢出。这个坑我在别的脚本里踩过,现象是“第一次点刷新正常,第二次点弹两个框,第三次弹三个”,排查半天才反应过来是重复包装。
fromButton和delayMask是两个入参,前者标识是否由按钮触发,后者控制刷新时的遮罩动画,原样透传给原函数就行,不要自己造值。有人为了“跳过遮罩加快刷新”硬传false,结果按钮状态和加载动画对不上,用户以为没点上,连点三次。
4.2 日期解析与 90 天差值:几个必须写对的细节
文档给的校验代码如下:
function main(spreadsheetReport) { if (!spreadsheetReport._jhy_doRefresh) { spreadsheetReport._jhy_doRefresh = spreadsheetReport.doRefresh; } spreadsheetReport.doRefresh = function (fromButton, delayMask) { // 根据参数名称获取参数值 // 结束日期不早于开始日期 var endtime = new Date(spreadsheetReport.getParameterValue("结束日期 2").replace(/\-/g, "/")); var starttime = new Date(spreadsheetReport.getParameterValue("开始日期 2").replace(/\-/g, "/")); if (endtime < starttime) { setTimeout(function () { alert("结束时间不能早于起始时间"); }, 100); return; } // 结束日期不大于开始日期 90 天 if (endtime.getTime() - starttime.getTime() > 1000 * 3600 * 24 * 90) { setTimeout(function () { alert("两个参数的差不能超过 90 天"); }, 100); return; } this._jhy_doRefresh(fromButton, delayMask); }; }第一个细节:replace(/\-/g, "/")。new Date("2024-01-01")在某些解析器里按 UTC 处理,new Date("2024/01/01")按本地时区处理,两者差 8 小时。日期比较本身不受影响,但如果你后面要做“相差天数”的取整计算,8 小时会让结果差一天。统一换成斜杠是最省事的兼容写法。
第二个细节:参数名带空格。"结束日期 2"中间有个空格,这个空格必须和参数面板里的名称逐字符一致,包括全角半角。手打很容易漏,正确做法是去参数定义里复制名称粘贴过来。名称对不上时getParameterValue返回null,null.replace直接抛异常,整个刷新被中断,用户看到的是“点了刷新没反应”。
第三个细节:setTimeout(..., 100)。刷新过程中页面上有一层加载遮罩,alert如果立刻弹,会被遮罩盖住或者被后续的渲染流程吞掉,用户只看到页面闪了一下。延后 100 毫秒,等遮罩层稳定再弹,提示才能被看见。这个数字不用精确,80 到 200 之间都行,太小容易看不见,太大用户会觉得卡。
第四个细节:差值用getTime()拿毫秒再比,别拿 Date 对象直接相减。1000 * 3600 * 24 * 90是 90 天的毫秒数,写成7776000000也行,但拆开写更好读,改天数的时候只动最后一个数字。注意“不超过 90 天”和“小于 90 天”是两回事,需求说“不大于”,那判断就该是>才拦截,等于 90 天要放行。
| 参数 | 示例值 | 类型 | 注意点 |
|---|---|---|---|
| 开始日期 2 | 2024-01-01 | 字符串 | 名称含空格,从参数定义复制 |
| 结束日期 2 | 2024-03-31 | 字符串 | 空值时先判空,再解析 |
| 90 天常量 | 1000×3600×24×90 | 数字 | 改窗口天数只动最后一位 |
补一句判空,实际项目里参数允许为空的情况很常见。getParameterValue取到空串或null时,new Date得到的是 Invalid Date,任何比较都返回 false,校验形同虚设。稳妥写法是在解析前加一层:
var rawEnd = spreadsheetReport.getParameterValue("结束日期 2"); var rawStart = spreadsheetReport.getParameterValue("开始日期 2"); if (!rawEnd || !rawStart) { return; // 参数没填完整,不拦刷新,交给后端或必填校验处理 }4.3 参数排版:一行 6 个上限与第二行保底
参数排版的规则文档写得很直白:筛选项居左,每行平均分配,一行不超过 6 个,如果是 2 行,第二行至少保留 2 个。这条规则背后是视觉平衡,不是随便定的。一行 7 个以上的时候,单个控件的宽度会被压到只能显示三四个字,日期控件的日历图标直接被裁掉;两行排版时如果第二行只剩 1 个,视觉上会像“掉队”,看起来像排版出错。
| 参数个数 | 建议每行 | 行数 | 说明 |
|---|---|---|---|
| 1-6 | 全部一行 | 1 | 居左排列,控件之间留均等间距 |
| 7-8 | 4 + 4 | 2 | 第二行 4 个,视觉平衡 |
| 9-10 | 5 + 5 | 2 | 每行 5 个,控件宽度够用 |
| 11-12 | 6 + 5 或 6 + 6 | 2 | 第二行不少于 5 个 |
| 13 以上 | 拆成两行以上 | 3+ | 每行不超 6 个,末行不少于 2 个 |
实际操作里还有一个隐含约束:日期区间类的两个参数(开始日期、结束日期)必须挨着放,中间不要插别的控件。用户看参数是扫视的,两个日期被一个下拉框隔开,每次都要愣一下。排版的时候把成对的参数先拎出来当一组,再往行里填。
5. 避坑与排查:五条踩过的记录
这一章全是翻车现场。每条按“现象→原因→解决”写,都是我在不同报表上真实遇到过的,症状描述尽量按你能看到的表象来写,方便对号入座。
5.1 宏不生效与刷新异常
现象一:宏写完了,保存也提示成功,但点刷新完全没反应,既不拦也不提示。原因通常是宏的作用范围挂错了——Smartbi 里客户端宏和服务端宏是两套执行环境,spreadsheetReport这个对象只在客户端宏里可用,写到服务端宏里就是一堆未定义变量,还不报错。解决:在宏的第一行加一句alert("宏已加载")打点,能弹出来说明宏本身被执行了,问题在后面的逻辑;弹不出来就是挂载位置或者触发事件选错了,回去检查宏的类型和绑定对象。
现象二:第一次点刷新正常,第二次点弹两个提示框,第三次弹三个。原因是重写doRefresh的时候没加守卫,每执行一次宏就多包一层。解决:用_jhy_doRefresh这种自定义属性暂存原函数,外面套一层if (!spreadsheetReport._jhy_doRefresh),保证只包装一次。改完之后要清一次浏览器缓存或者重新打开报表,否则旧的多层包装还在内存里。
现象三:日期比较永远不生效,结束日期比开始日期早也能查。原因有两个,一是参数名对不上,getParameterValue返回null,new Date(null)得到 1970 年,跟谁都比他小;二是参数值里带时分秒,比如2024-01-01 00:00:00,斜杠替换之后依然解析失败。解决:取值之后先判空再解析,alert把取到的原始值打出来看一眼,格式不对就让上游把参数格式统一成yyyy-MM-dd。
5.2 样式与排版的坑
现象四:屏幕预览列宽正好,导出 Excel 或者打印出来某一列被挤成两行。原因是列宽自适应在浏览器端按像素算,导出时按字符宽度换算,字体不一致就会差一截。解决:对关键列(金额、编号、日期)不要依赖自适应,直接设固定宽度,自适应只用在文本描述类列上;导出前用打印预览过一遍,别只看屏幕。
现象五:隔行换色在预览里正常,导出之后斑马纹消失或者变色。原因是用的是主题色,导出时主题映射丢失。解决:条件格式里的填充色改成固定 RGB,别用“浅蓝,个性色 1,淡色 60%”这类主题色;同时确认条件格式的应用范围没有超出实际数据区太多,范围过大时部分导出引擎会截断。
现象六:参数排两行,第二行只剩一个控件,看着像排版崩了。原因是最初按 7 个参数排的,后来业务加了一个,变成 8 个,谁也没回头调布局。解决:参数个数是动态的,排版也要定期回看,按“一行不超 6 个、两行时第二行不少于 2 个”的规则重排;参数增删之后把整块参数区重新拖一遍,别只往末尾追加。
提示:排查宏相关问题,最快的手段是在关键节点
alert打点,把变量值直接弹出来看。日志看不到的时候,弹窗就是唯一的黑匣子。
6. 发布前的验收与一个提速习惯
预览发布这一步,很多人点一下“预览”看到有数就发布了,结果第二天业务方打开说“怎么跟昨天不一样”。发布前我固定走一遍清单,五件事,每件都对应一类线上问题。
| 验收项 | 检查方法 | 不通过的表现 |
|---|---|---|
| 参数边界 | 手动把结束日期设成早于开始日期,点刷新 | 没弹提示,直接出数 |
| 90 天窗口 | 把区间拉到 91 天,点刷新 | 提示没弹,或者 90 天整被误拦 |
| 空参数 | 清空开始日期直接刷新 | 页面卡住或报脚本错误 |
| 列宽 | 换一台分辨率不同的机器打开 | 关键列被压成两行 |
| 打印 | 打印预览看第 2 页 | 表头没重复,或者最后一列被截断 |
这张表不用每次全过,但参数和打印这两项我一次都没省过。参数校验的 bug 上线后才发现的代价最大,业务方按错误区间跑出来的数据会直接进汇报材料。
提速的习惯只有一个:把调试用的宏和正式宏分开。日常改报表的时候,我在宏的第一行留一个开关变量,var DEBUG = true;,后面所有alert打点都包在if (DEBUG)里。调试完把DEBUG改成false,打点全部静默,不用一行行删注释,也不用担心漏删一个弹窗把用户吓一跳。这个开关还有一个副作用好处:下次这张报表再出问题,把DEBUG改回true就能直接进入排查状态,不用重新想当初在哪打的点。
另一个值得固化的动作是列宽预设。凡是出过“列宽在别人机器上炸开”这种问题的报表,我都会在自适应之后补一段固定宽度覆盖,把金额列、日期列、编号列钉死,只放描述类列去自适应。自适应是给不确定内容用的,确定内容的东西不该交给它猜。
说个我自己的教训:早几年做一张月度汇总表,参数校验写完只测了“结束早于开始”这一种情况,90 天窗口那一段代码因为平时用不到就跳过了,结果上线第一周业务方把区间拉到半年,脚本没拦住,后端跑了个全表扫描,报表直接超时。从那以后我每次写完参数校验,都强制把两个分支各触发一遍,哪怕需求里写着“可选”。希望帮到你。
本文还有配套的精品资源,点击获取