|
|
@@ -2,66 +2,77 @@
|
|
|
<div class="ChemicalsStats panel-box">
|
|
|
<div class="corner-deco tl"></div><div class="corner-deco tr"></div>
|
|
|
<div class="corner-deco bl"></div><div class="corner-deco br"></div>
|
|
|
- <div class="panel-title">化学品库动态统计</div>
|
|
|
- <div class="cards-grid">
|
|
|
-
|
|
|
- <!-- 左上:存量总量 -->
|
|
|
- <div class="chem-card">
|
|
|
- <div class="card-icon">
|
|
|
- <svg width="46" height="46" viewBox="0 0 46 46" fill="none">
|
|
|
- <circle cx="23" cy="23" r="20" fill="rgba(72,215,255,0.1)" stroke="rgba(72,215,255,0.4)" stroke-width="1.5"/>
|
|
|
- <!-- 倾斜试管,旋转-40度 -->
|
|
|
- <g transform="rotate(-40,23,23)">
|
|
|
- <rect x="19" y="9" width="8" height="18" rx="1.5" fill="rgba(72,215,255,0.15)" stroke="#48d7ff" stroke-width="1.5"/>
|
|
|
- <path d="M19 27 Q19 35 23 35 Q27 35 27 27Z" fill="rgba(72,215,255,0.35)" stroke="#48d7ff" stroke-width="1.5"/>
|
|
|
- <rect x="17" y="7" width="12" height="3" rx="1" fill="rgba(72,215,255,0.2)" stroke="#48d7ff" stroke-width="1.2"/>
|
|
|
- <!-- 液体 -->
|
|
|
- <rect x="19" y="22" width="8" height="5" fill="rgba(72,215,255,0.5)"/>
|
|
|
- <path d="M19 27 Q19 35 23 35 Q27 35 27 27Z" fill="rgba(72,215,255,0.5)"/>
|
|
|
- </g>
|
|
|
- </svg>
|
|
|
+ <div class="panel-title">化学品库存动态统计</div>
|
|
|
+ <div class="chem-panel-body">
|
|
|
+ <div class="chem-stats-grid">
|
|
|
+
|
|
|
+ <!-- 左上:存量总量,全满青色圆环 + 斜杠图标 -->
|
|
|
+ <div class="chem-stat-item">
|
|
|
+ <div class="chem-ring-wrap">
|
|
|
+ <svg viewBox="0 0 44 44">
|
|
|
+ <g transform="rotate(-90 22 22)">
|
|
|
+ <circle class="ring-bg" cx="22" cy="22" r="17"/>
|
|
|
+ <circle class="ring-fg" cx="22" cy="22" r="17" stroke="#48d7ff"
|
|
|
+ stroke-dasharray="106.8" stroke-dashoffset="0"/>
|
|
|
+ </g>
|
|
|
+ <text x="12" y="27" class="chem-ring-icon" fill="#48d7ff">🧪</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-info">
|
|
|
+ <div class="chem-stat-value" style="color:#48d7ff">{{ formatNum(stats.totalAmount) }}<span class="unit"> L</span></div>
|
|
|
+ <div class="chem-stat-label">存量化学品总量</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="card-value cyan">{{ formatNum(stats.totalAmount) }}<span class="unit"> L</span></div>
|
|
|
- <div class="card-label">存量化学品总量</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
|
|
|
- <!-- 右上:管控类 -->
|
|
|
- <div class="chem-card card-red">
|
|
|
- <div class="donut-pct orange">{{ controlledPct }}%</div>
|
|
|
- <div class="donut-wrap" ref="chartControlled"></div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="card-value orange">{{ formatNum(stats.controlledAmount) }}<span class="unit"> L</span></div>
|
|
|
- <div class="card-label">管控类化学品</div>
|
|
|
+ <!-- 右上:管控类,橙红色圆环 + 百分比居中 -->
|
|
|
+ <div class="chem-stat-item item-red">
|
|
|
+ <div class="chem-ring-wrap">
|
|
|
+ <svg viewBox="0 0 44 44">
|
|
|
+ <g transform="rotate(-90 22 22)">
|
|
|
+ <circle class="ring-bg" cx="22" cy="22" r="17"/>
|
|
|
+ <circle class="ring-fg" cx="22" cy="22" r="17" stroke="#ff4d4f"
|
|
|
+ stroke-dasharray="106.8" :stroke-dashoffset="calcOffset(controlledPct)"/>
|
|
|
+ </g>
|
|
|
+ <text x="22" y="25" class="ring-pct" fill="#ff4d4f">{{ controlledPct }}%</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-info">
|
|
|
+ <div class="chem-stat-value" style="color:#ff4d4f">{{ formatNum(stats.controlledAmount) }}<span class="unit"> L</span></div>
|
|
|
+ <div class="chem-stat-label">管控类化学品</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- <!-- 左下:非管控类 -->
|
|
|
- <div class="chem-card card-green">
|
|
|
- <div class="donut-pct green">{{ uncontrolledPct }}%</div>
|
|
|
- <div class="donut-wrap" ref="chartUncontrolled"></div>
|
|
|
- <div class="card-body">
|
|
|
- <div class="card-value green">{{ formatNum(stats.uncontrolledAmount) }}<span class="unit"> L</span></div>
|
|
|
- <div class="card-label">非管控类化学品</div>
|
|
|
+ <!-- 左下:非管控类,绿色圆环 + 百分比居中 -->
|
|
|
+ <div class="chem-stat-item item-green">
|
|
|
+ <div class="chem-ring-wrap">
|
|
|
+ <svg viewBox="0 0 44 44">
|
|
|
+ <g transform="rotate(-90 22 22)">
|
|
|
+ <circle class="ring-bg" cx="22" cy="22" r="17"/>
|
|
|
+ <circle class="ring-fg" cx="22" cy="22" r="17" stroke="#36d399"
|
|
|
+ stroke-dasharray="106.8" :stroke-dashoffset="calcOffset(uncontrolledPct)"/>
|
|
|
+ </g>
|
|
|
+ <text x="22" y="25" class="ring-pct" fill="#36d399">{{ uncontrolledPct }}%</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ <div class="chem-stat-info">
|
|
|
+ <div class="chem-stat-value" style="color:#36d399">{{ formatNum(stats.uncontrolledAmount) }}<span class="unit"> L</span></div>
|
|
|
+ <div class="chem-stat-label">非管控类化学品</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- <!-- 右下:类目统计 -->
|
|
|
- <div class="chem-card">
|
|
|
- <div class="card-body center">
|
|
|
- <div class="card-value yellow">{{ stats.totalCategories }}</div>
|
|
|
- <div class="card-label">存量化学品总类目</div>
|
|
|
- <div class="card-sub">管控{{ stats.controlledCategories }}类 / 非管控{{ stats.uncontrolledCategories }}类</div>
|
|
|
+ <!-- 右下:类目统计 -->
|
|
|
+ <div class="chem-stat-item item-center">
|
|
|
+ <div class="chem-stat-value" style="color:#ffb020;font-size:28px;">{{ stats.totalCategories }}</div>
|
|
|
+ <div class="chem-stat-label">存量化学品总类目</div>
|
|
|
+ <div class="chem-stat-sub">管控{{ stats.controlledCategories }}类 / 非管控{{ stats.uncontrolledCategories }}类</div>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import * as echarts from 'echarts'
|
|
|
import { getChemicalStats } from '@/api'
|
|
|
|
|
|
export default {
|
|
|
@@ -74,8 +85,6 @@ export default {
|
|
|
},
|
|
|
controlledPct: 0,
|
|
|
uncontrolledPct: 0,
|
|
|
- chartC: null,
|
|
|
- chartU: null,
|
|
|
timer: null
|
|
|
}
|
|
|
},
|
|
|
@@ -84,12 +93,14 @@ export default {
|
|
|
this.timer = setInterval(this.loadData, 5 * 60 * 1000)
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
- if (this.chartC) this.chartC.dispose()
|
|
|
- if (this.chartU) this.chartU.dispose()
|
|
|
clearInterval(this.timer)
|
|
|
},
|
|
|
methods: {
|
|
|
formatNum(n) { return (n || 0).toLocaleString() },
|
|
|
+ // r=17, circumference = 2*π*17 ≈ 106.8
|
|
|
+ calcOffset(pct) {
|
|
|
+ return (106.8 * (1 - pct / 100)).toFixed(1)
|
|
|
+ },
|
|
|
async loadData() {
|
|
|
try {
|
|
|
const d = await getChemicalStats()
|
|
|
@@ -104,35 +115,9 @@ export default {
|
|
|
const total = this.stats.totalAmount || 1
|
|
|
this.controlledPct = Math.round(this.stats.controlledAmount / total * 1000) / 10
|
|
|
this.uncontrolledPct = Math.round(this.stats.uncontrolledAmount / total * 1000) / 10
|
|
|
- this.$nextTick(() => {
|
|
|
- this.renderDonut('chartControlled', this.controlledPct, '#ff6b35', '#ff8c5a', 'C')
|
|
|
- this.renderDonut('chartUncontrolled', this.uncontrolledPct, '#36d399', '#5de8b5', 'U')
|
|
|
- })
|
|
|
} catch (e) {
|
|
|
console.error('ChemicalsStats:', e)
|
|
|
}
|
|
|
- },
|
|
|
- renderDonut(refName, pct, color, colorLight, key) {
|
|
|
- const el = this.$refs[refName]
|
|
|
- if (!el) return
|
|
|
- const existing = key === 'C' ? this.chartC : this.chartU
|
|
|
- const chart = existing || echarts.init(el)
|
|
|
- if (key === 'C') this.chartC = chart
|
|
|
- else this.chartU = chart
|
|
|
- chart.setOption({
|
|
|
- series: [{
|
|
|
- type: 'pie',
|
|
|
- radius: ['55%', '82%'],
|
|
|
- center: ['50%', '50%'],
|
|
|
- startAngle: 90,
|
|
|
- silent: true,
|
|
|
- label: { show: false },
|
|
|
- data: [
|
|
|
- { value: pct, itemStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: colorLight }, { offset: 1, color: color }] }, shadowBlur: 6, shadowColor: color } },
|
|
|
- { value: 100 - pct, itemStyle: { color: 'rgba(255,255,255,0.05)' } }
|
|
|
- ]
|
|
|
- }]
|
|
|
- })
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -140,79 +125,98 @@ export default {
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.ChemicalsStats {
|
|
|
- height: 272px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
-.cards-grid {
|
|
|
+.chem-panel-body {
|
|
|
+ flex: 1;
|
|
|
+ padding: 8px 10px 10px;
|
|
|
+ min-height: 0;
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.chem-stats-grid {
|
|
|
flex: 1;
|
|
|
display: grid;
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
grid-template-rows: 1fr 1fr;
|
|
|
gap: 8px;
|
|
|
- padding: 6px 10px 10px;
|
|
|
- min-height: 0;
|
|
|
z-index:3;
|
|
|
}
|
|
|
|
|
|
-.chem-card {
|
|
|
- background: rgba(0, 15, 50, 0.6);
|
|
|
- border: 1px solid rgba(72, 180, 255, 0.12);
|
|
|
+.chem-stat-item {
|
|
|
+ background: rgba(2, 14, 46, 0.7);
|
|
|
+ border: 1px solid rgba(72, 180, 255, 0.15);
|
|
|
border-radius: 6px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- padding: 0 10px;
|
|
|
- gap: 6px;
|
|
|
+ padding: 0 14px;
|
|
|
+ gap: 12px;
|
|
|
overflow: hidden;
|
|
|
- &.card-red { border-color: rgba(255, 107, 53, 0.3); background: rgba(40, 8, 4, 0.6); }
|
|
|
- &.card-green { border-color: rgba(54, 211, 153, 0.3); background: rgba(4, 28, 18, 0.6); }
|
|
|
-}
|
|
|
|
|
|
-.card-icon { flex-shrink: 0; }
|
|
|
+ &.item-red { border-color: rgba(255, 77, 79, 0.3); background: rgba(35, 6, 6, 0.7); }
|
|
|
+ &.item-green { border-color: rgba(54, 211, 153, 0.3); background: rgba(4, 24, 16, 0.7); }
|
|
|
+ &.item-center {
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 5px;
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
-.donut-wrap {
|
|
|
+.chem-ring-wrap {
|
|
|
flex-shrink: 0;
|
|
|
width: 52px;
|
|
|
height: 52px;
|
|
|
+
|
|
|
+ svg { width: 100%; height: 100%; }
|
|
|
}
|
|
|
|
|
|
-.donut-pct {
|
|
|
- flex-shrink: 0;
|
|
|
- font-size: 11px;
|
|
|
+.ring-bg {
|
|
|
+ fill: none;
|
|
|
+ stroke: rgba(255, 255, 255, 0.08);
|
|
|
+ stroke-width: 3.5;
|
|
|
+}
|
|
|
+
|
|
|
+.ring-fg {
|
|
|
+ fill: none;
|
|
|
+ stroke-width: 3.5;
|
|
|
+ stroke-linecap: round;
|
|
|
+ transition: stroke-dashoffset 0.8s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.ring-pct {
|
|
|
+ font-size: 8.5px;
|
|
|
font-weight: 700;
|
|
|
- line-height: 1;
|
|
|
- white-space: nowrap;
|
|
|
- &.orange { color: #ff6b35; }
|
|
|
- &.green { color: #36d399; }
|
|
|
+ text-anchor: middle;
|
|
|
+ dominant-baseline: middle;
|
|
|
}
|
|
|
|
|
|
-.card-body {
|
|
|
+.chem-stat-info {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
- gap: 4px;
|
|
|
- min-width: 0;
|
|
|
- &.center { align-items: center; width: 100%; }
|
|
|
+ gap: 5px;
|
|
|
}
|
|
|
|
|
|
-.card-value {
|
|
|
- font-size: 20px;
|
|
|
+.chem-stat-value {
|
|
|
+ font-size: 22px;
|
|
|
font-weight: 700;
|
|
|
line-height: 1;
|
|
|
letter-spacing: 1px;
|
|
|
- .unit { font-size: 12px; font-weight: 400; }
|
|
|
- &.cyan { color: #48d7ff; text-shadow: 0 0 8px rgba(72,215,255,0.5); }
|
|
|
- &.orange { color: #ff6b35; text-shadow: 0 0 8px rgba(255,107,53,0.5); }
|
|
|
- &.green { color: #36d399; text-shadow: 0 0 8px rgba(54,211,153,0.5); }
|
|
|
- &.yellow { color: #ffb020; text-shadow: 0 0 8px rgba(255,176,32,0.5); font-size: 30px; }
|
|
|
+
|
|
|
+ .unit {
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #7eacc8;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.card-label {
|
|
|
+.chem-stat-label {
|
|
|
font-size: 11px;
|
|
|
color: #7eacc8;
|
|
|
}
|
|
|
|
|
|
-.card-sub {
|
|
|
+.chem-stat-sub {
|
|
|
font-size: 10px;
|
|
|
color: rgba(126, 172, 200, 0.65);
|
|
|
}
|