// Data entry & import pages
const { useState, useMemo, useEffect } = React;
const DE = window.__CARBON_DATA;
// =========================================================
// 1. 数据填报页
// =========================================================
function DataEntry() {
const { orgId, org, descendants } = useOrg();
const toast = useToast();
const confirm = useConfirm();
const [filters, setFilters] = useState({
period: '',
category: '',
scope: '',
status: '',
keyword: ''
});
const [page, setPage] = useState(1);
const pageSize = 10;
// Local mutable entries (so add/edit/delete actually changes things)
const [entries, setEntries] = useState(() => DE.ENTRIES.slice());
const [modal, setModal] = useState(null); // { mode: 'add'|'edit'|'view', data }
// filter entries
const filtered = useMemo(() => {
const inOrg = new Set(descendants);
return entries.filter(e => {
if (!inOrg.has(e.orgId)) return false;
if (filters.period && e.period !== filters.period) return false;
if (filters.category && e.categoryCode !== filters.category) return false;
if (filters.scope && String(e.scope) !== filters.scope) return false;
if (filters.status && e.status !== filters.status) return false;
if (filters.keyword) {
const k = filters.keyword.toLowerCase();
if (!e.factorName.toLowerCase().includes(k) && !e.orgName.toLowerCase().includes(k)) return false;
}
return true;
});
}, [entries, descendants, filters]);
const total = filtered.length;
const pageData = filtered.slice((page - 1) * pageSize, page * pageSize);
useEffect(() => { setPage(1); }, [filters, orgId]);
const handleAdd = () => {
setModal({
mode: 'add',
data: {
id: null,
orgId: descendants[descendants.length - 1] || orgId,
period: DE.PERIODS[DE.PERIODS.length - 1],
categoryCode: 'FUEL',
factorId: '',
quantity: '',
unit: '',
status: 'draft'
}
});
};
const handleSubmit = (data, validate) => {
const errors = {};
if (!data.orgId) errors.orgId = '请选择所属组织';
if (!data.period) errors.period = '请选择报告期';
if (!data.categoryCode) errors.categoryCode = '请选择排放类别';
if (!data.factorId) errors.factorId = '请选择排放因子';
if (data.quantity === '' || isNaN(Number(data.quantity))) errors.quantity = '请输入有效活动数据';
else if (Number(data.quantity) <= 0) errors.quantity = '活动数据应大于 0';
else if (Number(data.quantity) > 100000000) errors.quantity = '数值超出合理范围(异常上限 1 亿)';
if (validate) return errors;
if (Object.keys(errors).length) return errors;
const f = DE.FACTORS.find(x => x.id === data.factorId);
const org_ = DE.ORG_FLAT.find(o => o.id === data.orgId);
const newEntry = {
...data,
orgName: org_.name,
orgCode: org_.code,
factorName: f.name,
factorValue: f.value,
factorUnit: f.unit,
unit: f.unit.split('/')[1] || '',
scope: DE.EMISSION_CATEGORIES.find(c => c.code === f.category).scope,
quantity: Number(data.quantity),
emission: +(Number(data.quantity) * f.value).toFixed(2),
source: 'manual',
createBy: '张芷涵', createDate: new Date().toISOString().slice(0, 19).replace('T', ' '),
updateBy: '张芷涵', updateDate: new Date().toISOString().slice(0, 19).replace('T', ' '),
periodType: 'month'
};
if (modal.mode === 'add') {
newEntry.id = 'E' + String(Math.floor(Math.random() * 900000) + 100000);
setEntries(prev => [newEntry, ...prev]);
toast.push('新增填报成功');
} else {
setEntries(prev => prev.map(e => e.id === newEntry.id ? newEntry : e));
toast.push('修改填报成功');
}
setModal(null);
return {};
};
const handleDelete = async (row) => {
const ok = await confirm({
title: '删除确认',
message: <>确定删除 {row.orgName} 在 {row.period} 的"{row.factorName}"填报记录?删除后不可恢复。>
});
if (ok) {
setEntries(prev => prev.filter(e => e.id !== row.id));
toast.push('已删除');
}
};
const columns = [
{ key: 'orgName', title: '所属组织', minWidth: 180, render: (r) => (
)},
{ key: 'period', title: '报告期', width: 90 },
{ key: 'scope', title: '范围', width: 90, render: (r) => },
{ key: 'categoryCode', title: '排放类别', width: 130, render: (r) => DE.EMISSION_CATEGORIES.find(c => c.code === r.categoryCode)?.name },
{ key: 'factorName', title: '排放源/因子', minWidth: 200 },
{ key: 'quantity', title: '活动数据', width: 130, align: 'right', render: (r) => {fmt(r.quantity, 2)} {r.unit} },
{ key: 'factorValue', title: '因子值', width: 120, align: 'right', render: (r) => {fmt(r.factorValue, 4)} },
{ key: 'emission', title: '排放量 (tCO₂e)', width: 130, align: 'right', render: (r) => {fmt(r.emission, 2)} },
{ key: 'source', title: '来源', width: 80, center: 'center', align: 'center', render: (r) => {r.source === 'manual' ? '手工' : '导入'} },
{ key: 'status', title: '状态', width: 90, align: 'center', render: (r) => },
{ key: 'updateBy', title: '更新人', width: 90 },
{ key: 'actions', title: '操作', width: 160, render: (r) => (
)}
];
// Summary by scope from filtered
const sum = filtered.reduce((acc, e) => {
acc.total += e.emission;
acc[`s${e.scope}`] += e.emission;
return acc;
}, { total: 0, s1: 0, s2: 0, s3: 0 });
return (
<>
>
}
/>
当前组织 {org.name}({org.code})及其下属 {descendants.length - 1} 个单元的填报数据 ·
核算标准:{org.standard} ·
行业:{getIndustryShort(org.industry)}
{/* Summary strip */}
筛选范围内总排放
{fmt(sum.total, 0)}tCO₂e
共 {filtered.length} 条记录
{/* Filters */}
{modal && (
setModal(null)}
onSubmit={handleSubmit}
/>
)}
>
);
}
// Form modal
function EntryFormModal({ mode, initialData, onClose, onSubmit }) {
const [data, setData] = useState({ ...initialData, quantity: initialData.quantity || '' });
const [errors, setErrors] = useState({});
const readonly = mode === 'view';
const cat = data.categoryCode;
const factor = DE.FACTORS.find(f => f.id === data.factorId);
const org_ = DE.ORG_FLAT.find(o => o.id === data.orgId);
// Recommended factors based on org industry + selected category
const recommendedFactors = useMemo(() => {
if (!cat) return [];
const orgIndustry = org_?.industry || 'GENERAL';
return DE.FACTORS.filter(f =>
f.category === cat &&
f.status === 'enabled' &&
(f.industry === orgIndustry || f.industry === 'GENERAL')
).sort((a, b) => {
// industry-specific first
if (a.industry === orgIndustry && b.industry !== orgIndustry) return -1;
if (b.industry === orgIndustry && a.industry !== orgIndustry) return 1;
return 0;
});
}, [cat, org_?.industry]);
const emission = (data.quantity && factor) ? Number(data.quantity) * factor.value : 0;
const set = (k, v) => {
setData(d => ({ ...d, [k]: v }));
setErrors(e => ({ ...e, [k]: undefined }));
};
const submit = () => {
const errs = onSubmit(data, true);
if (errs && Object.keys(errs).length) { setErrors(errs); return; }
onSubmit(data, false);
};
const title = mode === 'add' ? '新增排放数据' : mode === 'edit' ? '编辑排放数据' : '查看排放数据';
// Possible leaf orgs (level 4) where data is normally filled
const orgOptions = DE.ORG_FLAT.filter(o => o.children?.length === 0 || o.level === 4 || true).map(o => ({
value: o.id,
label: ' '.repeat(o.level - 1) + o.name + ' (' + o.code + ')'
}));
return (
关闭 :
<>>}>
set('orgId', v)} options={orgOptions}
placeholder="选择组织单元" disabled={readonly}
style={{ width: '100%', minWidth: 'auto' }} />
set('period', v)}
options={DE.PERIODS.map(p => ({ value: p, label: p + ' 月度' }))}
placeholder="选择月度" disabled={readonly}
style={{ width: '100%', minWidth: 'auto' }} />
{
set('categoryCode', v);
set('factorId', '');
}} options={DE.EMISSION_CATEGORIES.map(c => ({ value: c.code, label: `[范围${c.scope}] ${c.name}` }))}
disabled={readonly}
placeholder="选择排放类别"
style={{ width: '100%', minWidth: 'auto' }} />
0 && !readonly ? `根据组织行业(${getIndustryShort(org_?.industry)})自动推荐 ${recommendedFactors.length} 个适用因子` : null}>
set('factorId', v)}
options={recommendedFactors.map(f => ({
value: f.id,
label: `${f.name} · ${f.value} ${f.unit}` + (f.industry !== 'GENERAL' ? ' [推荐]' : '')
}))}
disabled={readonly || !cat}
placeholder={!cat ? '请先选择排放类别' : '选择排放因子'}
style={{ width: '100%', minWidth: 'auto' }} />
set('quantity', v)} type="number"
disabled={readonly} placeholder="输入活动数据"
suffix={factor ? (factor.unit.split('/')[1] || '') : ''}
style={{ width: '100%' }} />
set('status', v)}
options={[
{ value: 'draft', label: '草稿(暂存)' },
{ value: 'submitted', label: '已提交(待审核)' }
]} disabled={readonly}
style={{ width: '100%', minWidth: 'auto' }} />
{/* Calculation preview */}
{factor && data.quantity && !isNaN(Number(data.quantity)) && (
计算预览(ISO 14064-1 / 行业指南公式:活动数据 × 排放因子 × GWP)
{fmt(Number(data.quantity), 2)} {factor.unit.split('/')[1] || ''} ×
{factor.value} {factor.unit} =
{fmt(emission, 2)} tCO₂e
c.code === factor.category)?.scope} />
因子来源:{factor.source} · 版本 {factor.version} · 生效 {factor.effectiveDate}
)}
{mode === 'view' && (
创建:{data.createBy} · {data.createDate}
最近更新:{data.updateBy} · {data.updateDate}
)}
);
}
// =========================================================
// 2. 批量导入页
// =========================================================
function DataImport() {
const toast = useToast();
const [step, setStep] = useState(1);
const [file, setFile] = useState(null);
const [parsing, setParsing] = useState(false);
const [result, setResult] = useState(null);
// Simulated file pick
const pickFile = () => {
setFile({
name: '排放数据导入模板_2025Q2.xlsx',
size: 248 * 1024,
rows: 124
});
setStep(2);
};
const startImport = () => {
setParsing(true);
setTimeout(() => {
setParsing(false);
setResult({
total: 124,
success: 118,
failed: 6,
skipped: 0,
errorRows: [
{ row: 12, org: 'ZL-PWR-TS01', factor: 'F099', reason: '排放因子编码不存在', col: 'F (排放因子编码)' },
{ row: 27, org: 'ZL-CHM-NJ01', factor: 'F003', reason: '活动数据非数值:「待补」', col: 'G (活动数据)' },
{ row: 43, org: 'ZL-XXX-001', factor: 'F001', reason: '组织编码 ZL-XXX-001 不存在或无权限', col: 'B (组织编码)' },
{ row: 68, org: 'ZL-STL-TS01', factor: 'F006', reason: '数值超出合理范围(>1,000,000)', col: 'G (活动数据)' },
{ row: 91, org: 'ZL-PWR-SH02', factor: 'F003', reason: '报告期格式错误:「2025/04」应为「2025-04」', col: 'E (报告期)' },
{ row: 105, org: 'ZL-CMT-WH', factor: '', reason: '必填字段「排放因子编码」为空', col: 'F (排放因子编码)' }
]
});
setStep(3);
}, 1800);
};
const reset = () => { setStep(1); setFile(null); setResult(null); };
return (
<>
location.hash = 'data-entry'}>返回填报} />
= 1 ? ' done' : '') + (step === 1 ? ' active' : '')}>
{step > 1 ? : '1'}
下载模板
= 2 ? ' done' : '') + (step === 2 ? ' active' : '')}>
{step > 2 ? : '2'}
上传文件
= 3 ? ' active' : '')}>
3
导入结果
{step === 1 && (
请按模板填写排放活动数据。模板包含 组织编码、报告期、排放类别、排放因子编码、活动数据、单位 等必填字段。
组织编码必须为当前用户有权限访问的组织节点。
标准排放数据导入模板
适用 ISO 14064-1 通用结构
字段:组织编码 · 组织完整路径 · 报告期 · 排放类别 · 排放因子编码 · 活动数据 · 数据单位 · 备注
({ value: i.code, label: i.name }))} style={{ width: '100%', marginBottom: 12 }} />
)}
{step === 2 && !parsing && (
{!file ? (
点击或拖拽文件到此处上传
支持 .xlsx, .xls, .csv 格式 · 单文件不超过 10MB · 单次最多 5000 行
) : (
{file.name}
{(file.size / 1024).toFixed(1)} KB · 检测到 {file.rows} 行数据
)}
)}
{step === 2 && parsing && (
正在解析与校验数据({file?.rows} 行)...
)}
{step === 3 && result && (
{result.failed > 0 && (
下载错误报告 (.xlsx)}>
第 {r.row} 行 },
{ key: 'org', title: '组织编码', width: 130 },
{ key: 'factor', title: '因子编码', width: 110 },
{ key: 'col', title: '错误列', width: 180 },
{ key: 'reason', title: '错误原因' }
]} data={result.errorRows} rowKey="row" />
)}
{result.success > 0 && (
)}
)}
>
);
}
window.DataEntry = DataEntry;
window.DataImport = DataImport;