// 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) => (
{r.orgName}
{r.orgCode}
)}, { 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} 条记录
范围一
{fmt(sum.s1, 0)}tCO₂e
范围二
{fmt(sum.s2, 0)}tCO₂e
范围三
{fmt(sum.s3, 0)}tCO₂e
{/* Filters */}
setFilters({ ...filters, scope: v })} placeholder="全部范围" options={[{ value: '1', label: '范围一' }, { value: '2', label: '范围二' }, { value: '3', label: '范围三' }]} style={{ width: 130 }} /> setFilters({ ...filters, status: v })} placeholder="全部状态" options={[{ value: 'draft', label: '草稿' }, { value: 'submitted', label: '已提交' }, { value: 'approved', label: '已审核' }]} style={{ width: 130 }} /> setFilters({ ...filters, keyword: v })} placeholder="搜索排放源 / 组织..." style={{ width: 220 }} />
共 {filtered.length} 条
{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('period', v)} options={DE.PERIODS.map(p => ({ value: p, label: p + ' 月度' }))} placeholder="选择月度" disabled={readonly} style={{ width: '100%', minWidth: 'auto' }} /> 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%' }} /> ({ 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} 行数据
超出行业典型范围 10x 的数值将被标记
)}
)} {step === 2 && parsing && (
正在解析与校验数据({file?.rows} 行)...
)} {step === 3 && result && (
总行数
{result.total}
成功导入
{result.success}
失败行数
{result.failed}
跳过
{result.skipped}
{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 && (
成功导入 {result.success} 条记录,均归属到对应组织节点,操作日志已记录。 您可以前往 location.hash = 'data-entry'} style={{ cursor: 'pointer' }}>填报记录 查看,或对失败行修正后重新导入。
)}
)} ); } window.DataEntry = DataEntry; window.DataImport = DataImport;