From 561c5158f118844b45f455eabf9ca267f23a9a08 Mon Sep 17 00:00:00 2001 From: zty Date: Tue, 7 Jul 2026 21:27:48 -0400 Subject: [PATCH] feat(source): RoomBarChart grouped bar chart component --- .../src/pages/source/RoomBarChart.tsx | 59 ++++ .../iapip-web/src/pages/source/source.css | 257 ++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 空气质量预测/源码/用户端/iapip-web/src/pages/source/RoomBarChart.tsx create mode 100644 空气质量预测/源码/用户端/iapip-web/src/pages/source/source.css diff --git a/空气质量预测/源码/用户端/iapip-web/src/pages/source/RoomBarChart.tsx b/空气质量预测/源码/用户端/iapip-web/src/pages/source/RoomBarChart.tsx new file mode 100644 index 0000000..e569132 --- /dev/null +++ b/空气质量预测/源码/用户端/iapip-web/src/pages/source/RoomBarChart.tsx @@ -0,0 +1,59 @@ +export interface RoomBar { + name: string + pct: (number | null)[] +} + +interface Props { + rooms: RoomBar[] + labels: string[] + colors: string[] + overColor?: string +} + +const CAP = 150 // 视觉封顶百分比,柱高 = min(pct,CAP) px;100% 线在 100px + +export default function RoomBarChart({ rooms, labels, colors, overColor = '#C0392B' }: Props) { + if (!rooms.length) { + return
暂无全屋数据
+ } + return ( +
+
+ {rooms.map(room => ( +
+
+
+ {room.pct.map((v, i) => { + if (v == null) { + return
+ } + const over = v >= 100 + const h = Math.max(2, Math.min(v, CAP)) + return ( +
+
{v.toFixed(0)}%
+
+
+ ) + })} +
+
{room.name}
+
+ ))} +
+
+ {labels.map((l, i) => ( + {l} + ))} + 100% 限值线 +
+
+ ) +} diff --git a/空气质量预测/源码/用户端/iapip-web/src/pages/source/source.css b/空气质量预测/源码/用户端/iapip-web/src/pages/source/source.css new file mode 100644 index 0000000..980bf5b --- /dev/null +++ b/空气质量预测/源码/用户端/iapip-web/src/pages/source/source.css @@ -0,0 +1,257 @@ +/* source.css — 污染源识别,温暖绿色风格 */ +.s-app { + --bg: #f0efea; + --panel: #ffffff; + --panel2: #f8f7f2; + --panel3: #eeedea; + --border: #e0ddd5; + --border2: #c8c4bb; + --ink: #1a1a1a; + --sub: #595959; + --faint: #8c8c8c; + --accent: #2C5F2E; + --accent2: #1e4a21; + --accent-soft: rgba(44,95,46,.08); + --accent-deep: #173a1a; + --good: #237804; + --good-soft: rgba(35,120,4,.1); + --warn: #d46b08; + --warn-soft: rgba(212,107,8,.12); + --bad: #B91C1C; + --bad-soft: rgba(185,28,28,.1); + --track: #e4e4e4; + --radius: 10px; + background: var(--bg); + color: var(--ink); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif; + font-size: 14px; + letter-spacing: .1px; +} +.s-app * { box-sizing: border-box; } + +.s-body { padding: 0; } +.s-grid { display: grid; gap: 16px; align-items: start; grid-template-columns: 390px 1fr; } + +/* Card */ +.card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px 20px; + box-shadow: 0 1px 4px rgba(0,0,0,.05); + overflow: hidden; +} +.card-h { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 14px; padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} +.card-t { font-size: 14px; font-weight: 700; display: flex; align-items: center; gap: 8px; color: var(--ink); } +.card-t .bar { width: 3px; height: 15px; border-radius: 2px; background: var(--accent); flex-shrink: 0; } +.card-step { + font-size: 10px; font-weight: 700; letter-spacing: .5px; + color: var(--accent); background: var(--accent-soft); + border: 1px solid rgba(44,95,46,.2); + border-radius: 5px; padding: 2px 8px; +} +.muted { color: var(--faint); font-size: 11px; font-weight: 500; } + +/* Fields */ +.fld { margin-bottom: 14px; } +.fld-lab { + font-size: 12px; font-weight: 600; color: var(--sub); + margin-bottom: 6px; display: flex; align-items: center; justify-content: space-between; +} +.fld-lab .v { font-weight: 700; color: var(--ink); font-size: 13px; } + +/* Room selector tabs */ +.rooms { display: flex; flex-wrap: wrap; gap: 6px; } +.room-b { + font-size: 12.5px; font-weight: 600; padding: 7px 14px; + border-radius: 6px; border: 1px solid var(--border2); + background: var(--panel); color: var(--sub); + cursor: pointer; transition: border-color .12s, color .12s, background .12s; + white-space: nowrap; +} +.room-b:hover { border-color: var(--accent); color: var(--accent); } +.room-b.on { background: var(--accent); border-color: var(--accent); color: #fff; } + +/* Number input group */ +.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.row2 > div { min-width: 0; } +.num { + display: flex; align-items: stretch; min-width: 0; + background: var(--panel); border: 1px solid var(--border); + border-radius: 7px; overflow: hidden; + transition: border-color .15s, box-shadow .15s; + outline: none; +} +.num:focus-within { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); } +.num input { + flex: 1; min-width: 0; border: none; background: transparent; + padding: 8px 10px; font-size: 14px; font-weight: 600; + color: var(--ink); outline: none; +} +.num .unit { + font-size: 11px; color: var(--faint); padding: 0 10px; + font-weight: 500; border-left: 1px solid var(--border); + display: flex; align-items: center; white-space: nowrap; + background: var(--panel2); +} + +/* Ventilation slider */ +.slider { + width: 100%; -webkit-appearance: none; appearance: none; + height: 5px; border-radius: 3px; background: var(--track); outline: none; +} +.slider::-webkit-slider-thumb { + -webkit-appearance: none; width: 17px; height: 17px; border-radius: 50%; + background: var(--accent); cursor: pointer; + box-shadow: 0 1px 4px rgba(0,0,0,.2); border: 3px solid #fff; +} +.slider-scale { display: flex; justify-content: space-between; font-size: 10px; color: var(--faint); margin-top: 4px; } + +/* Materials list */ +.mats { display: flex; flex-direction: column; gap: 6px; } +.mat { + display: flex; align-items: center; gap: 10px; + padding: 8px 10px; border: 1px solid var(--border); + border-radius: 8px; background: var(--panel2); + transition: border-color .12s; +} +.mat:hover { border-color: var(--border2); } +.mat.off { opacity: .38; } +.mat-chk { + width: 17px; height: 17px; border-radius: 5px; + border: 1.5px solid var(--border2); flex: 0 0 17px; + cursor: pointer; display: flex; align-items: center; justify-content: center; + background: var(--panel); +} +.mat-chk.on { background: var(--accent); border-color: var(--accent); } +.mat-chk svg { width: 11px; height: 11px; color: #fff; } +.mat-main { flex: 1; min-width: 0; } +.mat-nm { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.mat-cat { font-size: 10px; color: var(--faint); } +.mat-qty { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; } +.mat-qty input { + width: 52px; border: 1px solid var(--border); border-radius: 6px; + background: var(--panel); padding: 4px 6px; font-weight: 700; + font-size: 13px; text-align: right; color: var(--ink); outline: none; + transition: border-color .12s; +} +.mat-qty input:focus { border-color: var(--accent); } +.mat-qty .u { font-size: 10px; color: var(--faint); } + +/* Result table */ +.res-table { width: 100%; border-collapse: collapse; } +.res-table thead tr { background: var(--panel2); } +.res-table th, .res-table td { + border-bottom: 1px solid var(--border); + padding: 10px 10px; text-align: left; font-size: 13px; +} +.res-table th { color: var(--sub); font-weight: 600; font-size: 12px; } +.res-table td.over { color: var(--bad); font-weight: 700; } +.res-table tbody tr:last-child td { border-bottom: none; } + +/* Pollutant segment selector */ +.pol-seg { + display: flex; gap: 2px; background: var(--panel2); + border: 1px solid var(--border); border-radius: 7px; padding: 3px; +} +.pol-seg b { + font-size: 11.5px; font-weight: 600; padding: 5px 9px; + border-radius: 5px; color: var(--sub); cursor: pointer; transition: .12s; +} +.pol-seg b.on { background: var(--accent); color: #fff; } + +/* Contribution bars */ +.contrib { display: flex; flex-direction: column; gap: 10px; } +.cb { display: grid; grid-template-columns: 140px 1fr 80px; align-items: center; gap: 10px; } +.cb-nm { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.cb-nm .rk { + display: inline-block; width: 17px; height: 17px; + border-radius: 4px; font-size: 10px; font-weight: 800; + text-align: center; line-height: 17px; margin-right: 6px; color: #fff; +} +.cb-track { height: 16px; background: var(--track); border-radius: 4px; overflow: hidden; } +.cb-fill { height: 100%; border-radius: 4px; transition: width .35s cubic-bezier(.3,.8,.3,1); } +.cb-val { text-align: right; font-size: 12px; } +.cb-val .c { font-weight: 700; } + +/* Suggestion box */ +.sugg { + display: flex; gap: 13px; padding: 14px 16px; + border-radius: 8px; background: var(--bad-soft); border: 1px solid rgba(185,28,28,.2); +} +.sugg.sugg-ok { border-color: rgba(35,120,4,.2); background: rgba(35,120,4,.05); } +.sugg-ic { + width: 32px; height: 32px; flex: 0 0 32px; + border-radius: 7px; background: var(--accent); + color: #fff; display: flex; align-items: center; justify-content: center; +} +.sugg.sugg-ok .sugg-ic { background: var(--good); } +.sugg-ic svg { width: 17px; height: 17px; } +.sugg-tt { font-size: 13px; font-weight: 700; margin-bottom: 4px; } +.sugg-tx { font-size: 12.5px; color: var(--sub); line-height: 1.6; } +.sugg-tx b { color: var(--bad); } +.sugg.sugg-ok .sugg-tx b { color: var(--good); } +.sugg-tips { display: flex; flex-direction: column; gap: 7px; margin-top: 10px; } +.sugg-tip { font-size: 12px; color: var(--sub); display: flex; align-items: flex-start; gap: 8px; line-height: 1.6; } +.tip-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 7px; margin-top: 5px; } +.tip-dot.tip-eco { background: var(--good); } +.tip-dot.tip-pro { background: var(--accent); } +.sugg-act { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 12px; } + +/* Buttons */ +.s-btn { + font-size: 12.5px; font-weight: 600; border-radius: 6px; + padding: 7px 14px; cursor: pointer; + border: 1px solid var(--border2); + background: var(--panel); color: var(--ink); + white-space: nowrap; transition: border-color .12s, color .12s; +} +.s-btn:hover { border-color: var(--accent); color: var(--accent); } +.s-btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; } +.s-btn-primary:hover { background: var(--accent-deep); border-color: var(--accent-deep); } +.s-btn-outline-pro { border-color: var(--accent); color: var(--accent); font-weight: 700; text-decoration: none; display: inline-flex; align-items: center; } +.s-btn-outline-pro:hover { background: var(--accent-soft); } + +/* Status chips */ +.chip { + display: inline-flex; align-items: center; gap: 5px; + font-size: 12px; font-weight: 700; border-radius: 5px; + padding: 4px 10px; width: fit-content; +} +.chip::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; } +.chip-bad { color: var(--bad); background: var(--bad-soft); border: 1px solid rgba(185,28,28,.25); } +.chip-warn { color: var(--warn); background: var(--warn-soft); } +.chip-good { color: var(--good); background: var(--good-soft); } + +/* Layout */ +.stack { display: flex; flex-direction: column; gap: 16px; } +.toast { + position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%); + background: var(--ink); color: #fff; font-size: 13px; font-weight: 600; + padding: 11px 20px; border-radius: 8px; + box-shadow: 0 8px 24px rgba(0,0,0,.2); z-index: 80; + display: flex; align-items: center; gap: 9px; +} +.toast svg { width: 15px; height: 15px; color: var(--good); } + +@media(max-width:880px) { .s-grid { grid-template-columns: 1fr; } } + +/* 全屋污染柱状图 */ +.rbc{padding:4px 2px 0;} +.rbc-plot{display:flex;flex-wrap:wrap;gap:26px 30px;align-items:flex-end;} +.rbc-room{display:flex;flex-direction:column;align-items:center;gap:8px;} +.rbc-bars{position:relative;display:flex;align-items:flex-end;gap:7px;height:150px;padding-top:16px;} +.rbc-100{position:absolute;left:-6px;right:-6px;bottom:100px;border-top:1px dashed #C0392B;opacity:.55;} +.rbc-bar{position:relative;width:15px;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;} +.rbc-val{position:absolute;top:-2px;font-size:10px;font-weight:700;white-space:nowrap;} +.rbc-col{width:100%;border-radius:4px 4px 0 0;} +.rbc-na{height:2px;background:#d9dde2;} +.rbc-room-nm{font-size:12.5px;color:var(--ink,#1f2328);font-weight:600;} +.rbc-legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:20px;padding-top:14px;border-top:1px solid #eef0f2;} +.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;}