From da9aceb56772db006bccd3e95254bd62ea36393e Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Mon, 13 Jul 2026 15:18:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Excel=E5=AF=BC=E5=85=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E5=88=97=E6=98=A0=E5=B0=84?= =?UTF-8?q?+=E6=99=BA=E8=83=BD=E5=88=97=E5=90=8D=E6=A3=80=E6=B5=8B=20-=20?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=90=8E=E8=87=AA=E5=8A=A8=E8=AF=BB=E5=8F=96?= =?UTF-8?q?Excel=E5=88=97=E5=90=8D=EF=BC=8C=E4=B8=8B=E6=8B=89=E6=A1=86?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E6=98=A0=E5=B0=84=20-=20=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E5=8C=B9=E9=85=8D:=20=E7=A7=91=E7=9B=AE/=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E2=86=92kpi=5Fcode,=20=E6=9C=9F=E9=97=B4=E2=86=92period,=20?= =?UTF-8?q?=E9=87=91=E9=A2=9D=E2=86=92actual=5Fvalue=20-=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=BB=9F=E4=B8=80=E6=9C=9F=E9=97=B4(=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=97=A0=E6=9C=9F=E9=97=B4=E5=88=97=E6=97=B6)=20-=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=8E=A5=E5=8F=97=20kpi=5Fcol/period=5Fcol/v?= =?UTF-8?q?alue=5Fcol/default=5Fperiod=20=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/data.py | 26 +++++++---- frontend/src/api/index.ts | 4 +- frontend/src/views/DataManage.vue | 71 ++++++++++++++++++++++++++++++- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/backend/app/api/data.py b/backend/app/api/data.py index 74211df1..85b22abe 100644 --- a/backend/app/api/data.py +++ b/backend/app/api/data.py @@ -15,14 +15,24 @@ router = APIRouter(prefix="/api/cma/data", tags=["数据对接"], ) @router.post("/import-excel") -async def import_excel(file: UploadFile = File(...), db: Session = Depends(get_db)): +async def import_excel(file: UploadFile = File(...), + kpi_col: str = Query("kpi_code", description="Excel中KPI编码列名"), + period_col: str = Query("period", description="Excel中期间列名"), + value_col: str = Query("actual_value", description="Excel中实际值列名"), + default_period: str = Query(None, description="如文件无期间列,统一使用此值"), + db: Session = Depends(get_db)): content = await file.read() df = pd.read_excel(io.BytesIO(content)) - required = ["kpi_code", "period", "actual_value"] - if not all(c in df.columns for c in required): - raise HTTPException(400, f"Excel必须包含列: {required}") - + required = [kpi_col, value_col] + if not default_period: + required.append(period_col) + + missing = [c for c in required if c not in df.columns] + if missing: + raise HTTPException(400, + f"Excel缺少列: {missing}。当前文件列: {list(df.columns)}") + if len(df) == 0: raise HTTPException(400, "Excel文件为空,没有数据行") @@ -33,9 +43,9 @@ async def import_excel(file: UploadFile = File(...), db: Session = Depends(get_d count = 0 skipped = [] for idx, row in df.iterrows(): - kpi_code = str(row.get("kpi_code", "")).strip() - period = str(row.get("period", "")).strip() - value = row.get("actual_value") + kpi_code = str(row.get(kpi_col, "")).strip() + period = str(row.get(period_col, default_period or "")).strip() if period_col in df.columns else (default_period or "").strip() + value = row.get(value_col) if not kpi_code or not period or pd.isna(value): skipped.append(f"第{idx+2}行: 缺少必填字段") diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 09699ba6..b830d5de 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -62,10 +62,10 @@ export const templateApi = { } export const dataApi = { - importExcel: (file: File) => { + importExcel: (file: File, qs?: string) => { const form = new FormData() form.append('file', file) - return api.post('/data/import-excel', form) + return api.post(`/data/import-excel${qs ? '?' + qs : ''}`, form) }, listSources: () => api.get('/data/sources'), createSource: (data: any) => api.post('/data/sources', data), diff --git a/frontend/src/views/DataManage.vue b/frontend/src/views/DataManage.vue index 5e194908..bad10f3f 100644 --- a/frontend/src/views/DataManage.vue +++ b/frontend/src/views/DataManage.vue @@ -14,6 +14,41 @@
{{ f.name }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 导入 {{ files.length }} 个文件
@@ -100,11 +135,14 @@ const activeTab = ref('import') const files = ref([]) const uploading = ref(false) const result = ref('') +const detectedCols = ref([]) +const colMap = ref({ kpi: 'kpi_code', period: 'period', value: 'actual_value', defaultPeriod: '' }) function handleFile(f: any) { - // el-upload multiple 模式下每次 onChange 传单个文件 if (f.raw && !files.value.some(ex => ex.name === f.raw.name && ex.size === f.raw.size)) { files.value.push(f.raw) + // 从第一个文件检测列名 + if (files.value.length === 1) detectColumns(f.raw) } } function handleRemove(f: any) { @@ -113,14 +151,42 @@ function handleRemove(f: any) { function removeFile(i: number) { files.value.splice(i, 1) } +async function detectColumns(file: File) { + // 用 Web 方式读 Excel 列名(仅第一行),供用户映射 + try { + const buf = await file.arrayBuffer() + const XLSX = (await import('xlsx')).default + const wb = XLSX.read(buf, { type: 'array' }) + const ws = wb.Sheets[wb.SheetNames[0]] + const rows: any[] = XLSX.utils.sheet_to_json(ws, { header: 1 }) + if (rows.length > 0) { + detectedCols.value = (rows[0] as string[]).filter(Boolean) + // 尝试智能匹配 + const all = detectedCols.value.map(c => c.toLowerCase().replace(/[\\s ]/g, '')) + const kpiIdx = all.findIndex(c => /^(kpi_?code|编码|科目|项目|指标名称?)$/.test(c)) + const periodIdx = all.findIndex(c => /^(period|期间|月份?|日期|年月)$/.test(c)) + const valIdx = all.findIndex(c => /^(actual_?value|金额|数值|本月数|本期金额?|实际值)$/.test(c)) + if (kpiIdx >= 0) colMap.value.kpi = detectedCols.value[kpiIdx] + if (periodIdx >= 0) colMap.value.period = detectedCols.value[periodIdx] + if (valIdx >= 0) colMap.value.value = detectedCols.value[valIdx] + } + } catch (_) { /* 预览列名失败不影响导入 */ } +} async function doImport() { if (files.value.length === 0) return uploading.value = true result.value = '' const msgs: string[] = [] + const params: Record = { + kpi_col: colMap.value.kpi || 'kpi_code', + value_col: colMap.value.value || 'actual_value', + } + if (colMap.value.period && colMap.value.period !== '__fixed__') params['period_col'] = colMap.value.period + if (colMap.value.period === '__fixed__' && colMap.value.defaultPeriod) params['default_period'] = colMap.value.defaultPeriod + const qs = new URLSearchParams(params).toString() for (const f of files.value) { try { - const r: any = await dataApi.importExcel(f) + const r: any = await dataApi.importExcel(f, qs) const skipped = r.skipped || 0 const total = r.total || 0 msgs.push(`📄 ${f.name}: ${total}条导入${skipped > 0 ? `, ${skipped}条跳过` : ''}`) @@ -129,6 +195,7 @@ async function doImport() { } } files.value = [] + detectedCols.value = [] result.value = msgs.join('\n') uploading.value = false }