diff --git a/空气质量预测/源码/用户端/iapip-web/src/pages/source/ContribPie.tsx b/空气质量预测/源码/用户端/iapip-web/src/pages/source/ContribPie.tsx
new file mode 100644
index 0000000..ca7e1df
--- /dev/null
+++ b/空气质量预测/源码/用户端/iapip-web/src/pages/source/ContribPie.tsx
@@ -0,0 +1,59 @@
+export interface PieItem {
+ name: string
+ rate: number
+ isSrc: boolean
+}
+
+interface Props {
+ items: PieItem[]
+ colors?: string[]
+}
+
+const DEFAULT_COLORS = ['#C0392B', '#E67E22', '#E0A100', '#159A86', '#2E7FC1', '#6FB1E0', '#9AA7B4']
+
+const R = 74
+const CX = 90
+const CY = 90
+
+function arc(cx: number, cy: number, r: number, a0: number, a1: number) {
+ const p = (a: number) => [cx + r * Math.cos((a - 90) * Math.PI / 180), cy + r * Math.sin((a - 90) * Math.PI / 180)]
+ const [x0, y0] = p(a0)
+ const [x1, y1] = p(a1)
+ const large = a1 - a0 > 180 ? 1 : 0
+ return `M ${cx} ${cy} L ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} Z`
+}
+
+export default function ContribPie({ items, colors = DEFAULT_COLORS }: Props) {
+ const total = items.reduce((s, it) => s + it.rate, 0)
+ if (!items.length || total <= 0) {
+ return
该污染物无材料释放
+ }
+ const color = (i: number) => colors[Math.min(i, colors.length - 1)]
+ let acc = 0
+ const slices = items.map((it, i) => {
+ const a0 = acc / total * 360
+ acc += it.rate
+ const a1 = acc / total * 360
+ return { d: arc(CX, CY, R, a0, a1), c: color(i), pct: it.rate / total * 100 }
+ })
+ return (
+
+
+
+ {items.map((it, i) => (
+
+
+ {it.name}{it.isSrc && (污染源)}
+ {(it.rate * 100).toFixed(1)}%
+
+ ))}
+
+
+ )
+}
diff --git a/空气质量预测/源码/用户端/iapip-web/src/pages/source/source.css b/空气质量预测/源码/用户端/iapip-web/src/pages/source/source.css
index 980bf5b..939d8ad 100644
--- a/空气质量预测/源码/用户端/iapip-web/src/pages/source/source.css
+++ b/空气质量预测/源码/用户端/iapip-web/src/pages/source/source.css
@@ -255,3 +255,13 @@
.rbc-lg{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted,#8a8f98);}
.rbc-lg i{width:11px;height:11px;border-radius:3px;display:inline-block;}
.rbc-lg-dash{width:14px;height:0;border-top:1px dashed #C0392B;border-radius:0;}
+
+/* 材料贡献饼图 */
+.pie-wrap{display:flex;gap:22px;align-items:center;flex-wrap:wrap;padding:6px 2px 2px;}
+.pie-svg{flex-shrink:0;}
+.pie-legend{display:flex;flex-direction:column;gap:8px;min-width:200px;flex:1;}
+.pie-lg{display:flex;align-items:center;gap:9px;font-size:13px;}
+.pie-lg i{width:12px;height:12px;border-radius:3px;flex-shrink:0;}
+.pie-lg-nm{flex:1;color:var(--ink,#1f2328);}
+.pie-src{color:#C0392B;font-weight:700;}
+.pie-lg-v{font-weight:700;color:var(--ink,#1f2328);}