整体方案:模板标记选区→上传解析→按标记坐标定向提取单元格,分 4 大步骤,适配 Excel 文件上传解析 + Luckysheet 在线模板两种场景,精准过滤灰色非采集单元格。
一、整体设计思路(核心原理)
1. 前置:在模板中给「红色采集框」绑定元数据(关键)
不能靠颜色识别(Excel 颜色易修改、色差干扰),推荐 2 种标记方案二选一
方案 A【推荐:选区坐标入库】
- 在前端 Luckysheet 里,用户鼠标框选红色录入区域,调用
luckysheet.getRange()拿到所有红框坐标数组([{row:[s,e],col:[s,e]}]); - 将每个 sheet 的采集区域坐标、单元格规则 和模板文件绑定存入数据库:
模板ID → [采集区域列表];
例:当前表格红框坐标:
- C2:D2 → row1,1,col2,3
- C5:D6 → row4,5,col2,3
- D7 → row6,6,col3,3
- C8:D9 → row7,8,col2,3
- 灰色禁用单元格不录入坐标,解析时自动跳过。
方案 B【单元格批注标记】
Excel/Luckysheet 给红色单元格加批注:need_collect=1,灰色单元格不加批注;解析文件时遍历单元格,仅提取批注带采集标识的单元格。
❌ 不推荐靠单元格背景色区分:用户修改红颜色值、格式刷变色会导致解析失效。
2. 上传文件阶段
用户上传 Excel(.xlsx/.xls),后端先匹配数据库里该报表模板对应的采集坐标清单。
3. 文件解析阶段
后端使用 POI (Java)/openpyxl (Python) 打开 Excel,只循环遍历预存的采集坐标单元格,不在坐标清单里的单元格(灰色区域)直接跳过不取数。
4. 数据结构化落地
按项目名称(行维度)→字段(C/D列)组装成 JSON 入库。
二、分步落地实现(分前端 + 后端)
步骤 1:前端 Luckysheet 配置模板(标记红框采集区,存坐标)
1.1 用户框选红框,自动获取选区坐标
javascript
// 用户框选完红框后,点击【保存模板采集区域】按钮触发
function saveCollectRange(){
// 获取所有选中红框区域
const collectRanges = luckysheet.getRange();
// 1. 列下标转Excel列名、行+1转Excel真实行号
const excelRanges = collectRanges.map(range=>{
const {row,column} = range;
const startRow = row[0]+1,endRow = row[1]+1;
const startCol = numToABC(column[0]),endCol = numToABC(column[1]);
return `${startCol}${startRow}:${endCol}${endRow}`
})
// 2. 把【模板ID+excelRanges采集区域数组】传给后端入库
axios.post('/api/template/saveRange',{
templateId:"G4B_III",
sheetName:"Sheet1",
collectAreas:excelRanges
})
}
// 列数字→A/B/C工具函数
function numToABC(n){
let str='';while(n>=0){str=String.fromCharCode(n%26+65)+str;n=Math.floor(n/26)-1}return str;
}
示例入库后数据库存储:templateId:G4B_III,collectAreas:"C2:D2","C5:D6","D7:D7","C8:D9"
1.2 预览 / 修改采集区:代码自动选中红框
javascript
// 后端取出采集坐标,转回luckysheet下标,自动高亮红框
const areaList = ["C2:D2","C5:D6"];
// 坐标转luckysheet下标(A1→col:0,row:0),调用setRange自动选中
步骤 2:后端 Excel 文件解析(Java POI 示例,只提取标记区域)
2.1 逻辑流程
- 根据上传文件名 / 报表编码查库,取出该模板
collectAreas:["C2:D2","C5:D6"...]; - POI 打开 Excel,遍历每个采集区域;
- 只读取区域内单元格值,灰色无标记单元格直接忽略。
2.2 关键代码片段(Java POI)
java
// 1.从数据库查出该模板所有采集区域
List<String> collectAreas = templateMapper.getCollectAreaByTemplateId("G4B_III");
// 2.解析每个区域
for(String area:collectAreas){
// 拆分 C5:D6 → 起始单元格C5、结束D6
String[] split = area.split(":");
CellRangeAddress range = CellRangeAddress.valueOf(area);
// 循环区域内所有行、列
for(int r=range.getFirstRow();r<=range.getLastRow();r++){
Row row = sheet.getRow(r);
for(int c=range.getFirstColumn();c<=range.getLastColumn();c++){
Cell cell = row.getCell(c,Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
String cellValue = cell.getStringCellValue();
// 按行项目+列字段组装数据
saveCellData(row,c,cellValue);
}
}
}
步骤 3:数据映射结构化(业务组装)
表格规则:
- B 列(项目名称:1. 衍生工具...2. 证券融资...)作为数据主键;
- C 列 = 名义本金,D 列 = 风险加权资产;提取后输出结构:
json
[
{"itemName":"1.与非中央交易对手的衍生工具...","C_名义本金":"","D_风险加权资产":""},
{"itemName":"2.与非中央交易对手的证券融资交易...","C_名义本金":"","D_风险加权资产":""},
{"itemName":"3.信用估值调整风险","D_风险加权资产":""},
{"itemName":"4.与中央交易对手交易形成的信用风险","C_名义本金":"","D_风险加权资产":""},
{"reportDate":"2025年12月","unit":"万元"}
]
三、补充优化方案(2 种备选)
备选 1:若无法提前配置模板坐标(无模板库)
- 前端导出模板时,红色单元格统一自定义单元格格式 / 批注标识
needCollect=1; - 后端全量遍历 Excel 所有单元格,仅提取批注包含 needCollect 的单元格,灰色单元格无批注直接跳过。
备选 2:基于 Luckysheet 在线填报(不上传 Excel,在线填表)
用户在线在 Luckysheet 填表,前端监听红框区域单元格值变更,只收集标记区域数据,直接提交后端,无需解析 Excel 文件。
Java+Vue+Luckysheet 完整 Demo
整体架构:
- 前端 Vue3 + Luckysheet:在线配置报表、框选红色采集区域、保存采集坐标到后端、预览模板
- 后端 SpringBoot + POI:保存模板采集坐标、接收 Excel 上传、按预存红框坐标解析 Excel,只提取红框数据
- 业务:G4B 报表,红框 = 采集单元格、灰色 = 跳过不采集
一、后端 SpringBoot(Java)
1. pom.xml 依赖
xml
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- POI解析Excel -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- lombok简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 实体类 TemplateEntity(内存模拟库,正式换 Mybatis+MySQL)
java
import lombok.Data;
import java.util.List;
@Data
public class TemplateEntity {
// 模板编码 G4B_III
private String templateId;
// sheet页名称
private String sheetName;
// 采集区域:["C2:D2","C5:D6","D7:D7","C8:D9"] Excel区域字符串
private List<String> collectAreas;
}
3. 内存存储工具(代替数据库)
java
import java.util.HashMap;
import java.util.Map;
public class TemplateCache {
public static Map<String, TemplateEntity> templateMap = new HashMap<>();
}
4. Controller 核心接口(3 个接口:保存选区、获取模板、解析 Excel)
java
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.*;
@RestController
@RequestMapping("/api/template")
@CrossOrigin // 跨域
public class TemplateController {
// 1. 前端保存红框采集区域到后端
@PostMapping("/saveRange")
public Map<String, Object> saveTemplate(@RequestBody TemplateEntity entity) {
TemplateCache.templateMap.put(entity.getTemplateId(), entity);
return Map.of("code",200,"msg","保存采集区域成功");
}
// 2. 根据模板ID查询已保存的采集区域(前端回显红框)
@GetMapping("/get/{templateId}")
public TemplateEntity getTemplate(@PathVariable String templateId) {
return TemplateCache.templateMap.get(templateId);
}
// 3. 上传Excel,按预存红框区域解析,只提取红框数据
@PostMapping("/parseExcel")
public Map<String,Object> parseExcel(@RequestParam String templateId, @RequestParam MultipartFile file) throws Exception {
TemplateEntity template = TemplateCache.templateMap.get(templateId);
if(template == null){
return Map.of("code",500,"msg","模板不存在,请先配置采集区域");
}
List<String> areaList = template.getCollectAreas();
List<Map<String,Object>> resultData = new ArrayList<>();
try(InputStream is = file.getInputStream();
Workbook workbook = WorkbookFactory.create(is)){
Sheet sheet = workbook.getSheet(template.getSheetName());
// 循环每一个红框区域
for(String areaStr : areaList){
CellRangeAddress range = CellRangeAddress.valueOf(areaStr);
// 遍历区域内所有单元格
for(int r = range.getFirstRow(); r <= range.getLastRow(); r++){
Row row = sheet.getRow(r);
if(row == null) continue;
for(int c = range.getFirstColumn(); c <= range.getLastColumn(); c++){
Cell cell = row.getCell(c, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
String cellVal = cell.getStringCellValue().trim();
Map<String,Object> cellInfo = new HashMap<>();
cellInfo.put("excelRange",areaStr);
cellInfo.put("cellAddr",getColName(c)+(r+1));
cellInfo.put("value",cellVal);
resultData.add(cellInfo);
}
}
}
}
return Map.of("code",200,"data",resultData,"msg","解析完成,仅返回红框采集数据");
}
// 列下标转A/B/C (0→A,1→B,2→C)
private String getColName(int colIndex){
StringBuilder sb = new StringBuilder();
int n = colIndex;
while(n >=0){
sb.insert(0,(char)('A'+(n%26)));
n = n/26 -1;
}
return sb.toString();
}
}
二、Vue3 前端(Vite+Vue3+Luckysheet)
1. 安装依赖
bash
npm install luckysheet axios
2. 单页面代码 TemplateG4B.vue
vue
<template>
<div>
<div style="margin:10px">
<el-button @click="saveCollectRange">保存选中红框采集区域</el-button>
<el-button @click="loadTemplate">加载已保存模板(自动选中红框)</el-button>
<el-upload :before-upload="uploadExcel">
<el-button type="primary">上传Excel解析红框数据</el-button>
</el-upload>
</div>
<!-- luckysheet渲染容器 -->
<div id="luckysheet" style="width:100%;height:700px;border:1px solid #ccc"></div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import axios from 'axios'
import luckysheet from 'luckysheet'
import 'luckysheet/dist/plugins/css/index.css'
const TEMPLATE_ID = 'G4B_III'
let luckysheetIns = null
// 列数字转ABC
const numToABC = (n)=>{
let s = ''
while(n>=0){
s = String.fromCharCode(n%26+65)+s
n = Math.floor(n/26)-1
}
return s
}
// ABC转列下标
const ABCtoNum = (str)=>{
let res = -1
for(let s of str){
res = res*26 + s.charCodeAt()-'A'+1
}
return res-1
}
// A1:D5 拆分成luckysheet range格式 {row:[s,e],column:[s,e]}
const areaToRange = (areaStr)=>{
const [start,end] = areaStr.split(':')
// 拆分行列
const parsePos = (pos)=>{
let colStr = pos.replace(/[0-9]/g,'')
let row = Number(pos.replace(/[A-Z]/g,''))-1
let col = ABCtoNum(colStr)
return {row,col}
}
const s = parsePos(start)
const e = parsePos(end)
return {
row:[s.row,e.row],
column:[s.col,e.col]
}
}
// 初始化luckysheet
onMounted(()=>{
luckysheetIns = luckysheet.create({
container:'luckysheet',
showSheetBar:true,
// 导入你的G4B模板数据(可从excel导入,这里省略)
data:[[]]
})
})
// 1. 保存当前选中红框选区到后端
const saveCollectRange = async()=>{
// 获取用户框选的多个区域
const selectRanges = luckysheet.getRange()
// 转成 C2:D2 格式字符串
const excelAreas = selectRanges.map(item=>{
const sc = numToABC(item.column[0])
const ec = numToABC(item.column[1])
const sr = item.row[0]+1
const er = item.row[1]+1
return `${sc}${sr}:${ec}${er}`
})
// 请求后端保存
await axios.post('http://localhost:8080/api/template/saveRange',{
templateId:TEMPLATE_ID,
sheetName:'Sheet1',
collectAreas:excelAreas
})
alert('红框采集区域保存成功!')
}
// 2. 读取后端模板,自动选中红框
const loadTemplate = async()=>{
const res = await axios.get(`http://localhost:8080/api/template/get/${TEMPLATE_ID}`)
const {collectAreas} = res.data
// 转为luckysheet选中格式
const luckRanges = collectAreas.map(item=>areaToRange(item))
// 自动选中所有红框
luckysheet.setRange(luckRanges)
}
//3. 上传Excel文件解析
const uploadExcel = async(file)=>{
const form = new FormData()
form.append('templateId',TEMPLATE_ID)
form.append('file',file)
const res = await axios.post('http://localhost:8080/api/template/parseExcel',form)
console.log('解析结果(仅红框数据)',res.data.data)
alert('解析成功,控制台查看红框采集数据')
return false
}
</script>
三、使用操作步骤
- 后端启动 SpringBoot(端口 8080)
- 前端启动 vue 项目
- 操作流程: ① 在 Luckysheet 表格里,鼠标
Ctrl+多选红框单元格(你截图里所有红色采集区)② 点击【保存选中红框采集区域】→ 后端存入该模板的采集坐标③ 下次打开页面点击【加载已保存模板】,程序自动框选所有红框④ 上传用户填报好的 Excel 文件 → 后端只解析预存红框区域单元格,灰色单元格直接跳过不读取,返回所有红框的数据
四、关键业务扩展
- 正式环境替换内存存储: 把
TemplateCache换成 MySQL+Mybatis,templateId主键存储每个报表的采集区域。 - 灰色禁用单元格: 在模板配置时,灰色单元格设置
luckysheet.setCellReadOnly(row,col,true),前端不可编辑,解析天然忽略。 - **数据结构化:**解析后根据 B 列项目名称 + C/D 列字段,组装成业务 JSON(项目 - 名义本金 - 风险加权资产)。