|
|
@@ -0,0 +1,316 @@
|
|
|
+<template>
|
|
|
+ <div class="panel chemicals-stats">
|
|
|
+ <div class="border-beam"></div>
|
|
|
+ <svg class="pc tl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
|
|
|
+ <svg class="pc tr" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
|
|
|
+ <svg class="pc bl" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
|
|
|
+ <svg class="pc br" viewBox="0 0 14 14"><path d="M0 14 V0 H14" fill="none" stroke="rgba(30,144,255,0.85)" stroke-width="1.5"/></svg>
|
|
|
+
|
|
|
+ <div class="panel-header">
|
|
|
+ <div class="panel-header-icon">⚗️</div>
|
|
|
+ <span class="panel-title">化学品库存动态统计</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div style="padding:12px 20px 15px;display:flex;gap:15px;height:calc(100% - 82px)">
|
|
|
+ <!-- 左侧: 饼图 -->
|
|
|
+ <div style="flex:1;min-width:0;display:flex;flex-direction:column">
|
|
|
+ <div ref="chemPieChart" style="flex:1;min-height:0"></div>
|
|
|
+ </div>
|
|
|
+ <!-- 右侧: 存量统计 -->
|
|
|
+ <div style="flex:0 0 380px;display:flex;flex-direction:column;gap:14px;justify-content:center">
|
|
|
+ <!-- 总量 -->
|
|
|
+ <div class="chem-stat-card total">
|
|
|
+ <div class="csc-label-a">存量化学品总量</div>
|
|
|
+ <div class="csc-value-a">{{ totalAmount.toLocaleString() }}<span class="csc-unit">L</span></div>
|
|
|
+ </div>
|
|
|
+ <!-- 管控 -->
|
|
|
+ <div class="chem-stat-card ctrl">
|
|
|
+ <div style="display:flex;justify-content:space-between;align-items:flex-start">
|
|
|
+ <div>
|
|
|
+ <div class="csc-label">管控类化学品</div>
|
|
|
+ <div class="csc-value">{{ ctrlAmount }}<span class="csc-unit">L</span></div>
|
|
|
+ </div>
|
|
|
+ <div class="csc-pct-ring">
|
|
|
+ <svg viewBox="0 0 60 60" width="80" height="80">
|
|
|
+ <circle cx="30" cy="30" r="24" fill="none" stroke="rgba(239,68,68,0.12)" stroke-width="6"/>
|
|
|
+ <circle cx="30" cy="30" r="24" fill="none" stroke="#ef4444" stroke-width="6"
|
|
|
+ :stroke-dasharray="ctrlDash"
|
|
|
+ stroke-linecap="round" transform="rotate(-90 30 30)"
|
|
|
+ style="filter:drop-shadow(0 0 4px rgba(239,68,68,0.7))"/>
|
|
|
+ <text x="30" y="35" text-anchor="middle" fill="#ef4444" font-size="11" font-weight="700" font-family="Arial">{{ ctrlPct }}%</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="csc-sub">占总量 {{ ctrlPct }}% · 共 {{ ctrlCategories }} 类目</div>
|
|
|
+ </div>
|
|
|
+ <!-- 非管控 -->
|
|
|
+ <div class="chem-stat-card free">
|
|
|
+ <div style="display:flex;justify-content:space-between;align-items:flex-start">
|
|
|
+ <div>
|
|
|
+ <div class="csc-label">非管控类化学品</div>
|
|
|
+ <div class="csc-value" style="color:#00d8ff">{{ freeAmount }}<span class="csc-unit">L</span></div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <svg viewBox="0 0 60 60" width="80" height="80">
|
|
|
+ <circle cx="30" cy="30" r="24" fill="none" stroke="rgba(0,216,255,0.12)" stroke-width="6"/>
|
|
|
+ <circle cx="30" cy="30" r="24" fill="none" stroke="#00d8ff" stroke-width="6"
|
|
|
+ :stroke-dasharray="freeDash"
|
|
|
+ stroke-linecap="round" transform="rotate(-90 30 30)"
|
|
|
+ style="filter:drop-shadow(0 0 4px rgba(0,216,255,0.7))"/>
|
|
|
+ <text x="30" y="35" text-anchor="middle" fill="#00d8ff" font-size="11" font-weight="700" font-family="Arial">{{ freePct }}%</text>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="csc-sub" style="color:rgba(0,216,255,0.55)">占总量 {{ freePct }}% · 共 {{ freeCategories }} 类目</div>
|
|
|
+ </div>
|
|
|
+ <!-- 类目总数 -->
|
|
|
+ <div class="chem-stat-card cat">
|
|
|
+ <div class="csc-label">存量化学品总类目数</div>
|
|
|
+ <div style="display:flex;align-items:baseline;gap:12px;margin-top:8px">
|
|
|
+ <div class="csc-value" style="color:#ffd740">{{ ctrlCategories + freeCategories }}<span class="csc-unit">类</span></div>
|
|
|
+ <div style="display:flex;gap:6px;align-items:center">
|
|
|
+ <span style="font-size:22px;color:rgba(239,68,68,0.8)">管控 {{ ctrlCategories }}类</span>
|
|
|
+ <span style="font-size:20px;color:rgba(168,204,232,0.3)">|</span>
|
|
|
+ <span style="font-size:22px;color:rgba(0,216,255,0.8)">非管控 {{ freeCategories }}类</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import * as echarts from 'echarts'
|
|
|
+
|
|
|
+const C = 2 * Math.PI * 24 // circumference r=24 ~150.8
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'ChemicalsStats',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ totalAmount: 1229,
|
|
|
+ ctrlAmount: 269,
|
|
|
+ freeAmount: 960,
|
|
|
+ ctrlCategories: 5,
|
|
|
+ freeCategories: 5,
|
|
|
+ chart: null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ ctrlPct() {
|
|
|
+ return Math.round(this.ctrlAmount / (this.totalAmount || 1) * 1000) / 10
|
|
|
+ },
|
|
|
+ freePct() {
|
|
|
+ return Math.round(this.freeAmount / (this.totalAmount || 1) * 1000) / 10
|
|
|
+ },
|
|
|
+ ctrlDash() {
|
|
|
+ const arc = Math.round(this.ctrlPct / 100 * C * 10) / 10
|
|
|
+ return `${arc} ${C}`
|
|
|
+ },
|
|
|
+ freeDash() {
|
|
|
+ const arc = Math.round(this.freePct / 100 * C * 10) / 10
|
|
|
+ return `${arc} ${C}`
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.$nextTick(() => this.initChart())
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ if (this.chart) this.chart.dispose()
|
|
|
+ window.removeEventListener('resize', this.handleResize)
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ initChart() {
|
|
|
+ this.chart = echarts.init(this.$refs.chemPieChart, null, { renderer: 'canvas', devicePixelRatio: 2 })
|
|
|
+ this.chart.setOption({
|
|
|
+ backgroundColor: 'transparent',
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'item',
|
|
|
+ formatter: '{b}: {c}L ({d}%)',
|
|
|
+ backgroundColor: 'rgba(3,14,42,0.92)',
|
|
|
+ borderColor: 'rgba(30,144,255,0.3)',
|
|
|
+ textStyle: { color: '#a8cce8', fontSize: 22 }
|
|
|
+ },
|
|
|
+ graphic: [
|
|
|
+ // { type: 'text', left: 'center', top: '36%', style: { text: String(this.totalAmount), fill: '#ffd740', font: 'bold 44px Arial', textAlign: 'center' } },
|
|
|
+ // { type: 'text', left: 'center', top: '55%', style: { text: '化学品总量(L)', fill: 'rgba(168,204,232,0.65)', font: '20px Arial', textAlign: 'center' } }
|
|
|
+ ],
|
|
|
+ legend: {
|
|
|
+ show:false,
|
|
|
+ orient: 'horizontal', bottom: '2%', icon: 'circle',
|
|
|
+ itemWidth: 24, itemHeight: 24, itemGap: 30,
|
|
|
+ textStyle: { color: '#a8cce8', fontSize: 22 }
|
|
|
+ },
|
|
|
+ series: [{
|
|
|
+ type: 'pie', radius: ['32%', '62%'], center: ['50%', '52%'],
|
|
|
+ avoidLabelOverlap: true,
|
|
|
+ label: {
|
|
|
+ show: true, position: 'outside',
|
|
|
+ formatter: params => `{name|${params.name}}\n{val|${params.value}L ${params.percent}%}`,
|
|
|
+ rich: {
|
|
|
+ name: { fontSize: 24, color: '#a8cce8', lineHeight: 30 },
|
|
|
+ val: { fontSize: 22, color: '#ffd740', lineHeight: 26 }
|
|
|
+ },
|
|
|
+ distanceToLabelLine: 10
|
|
|
+ },
|
|
|
+ labelLine: { show: true, length: 25, length2: 35, lineStyle: { color: 'rgba(168,204,232,0.5)' } },
|
|
|
+ data: [
|
|
|
+ { value: this.ctrlAmount, name: '管控类', itemStyle: { color: '#ef4444', shadowBlur: 10, shadowColor: 'rgba(239,68,68,0.6)' } },
|
|
|
+ { value: this.freeAmount, name: '非管控类', itemStyle: { color: '#00d8ff', shadowBlur: 10, shadowColor: 'rgba(0,216,255,0.6)' } }
|
|
|
+ ],
|
|
|
+ emphasis: { label: { fontSize: 26, fontWeight: 'bold' }, scale: true, scaleSize: 8 }
|
|
|
+ }]
|
|
|
+ })
|
|
|
+ window.addEventListener('resize', this.handleResize)
|
|
|
+ },
|
|
|
+ handleResize() {
|
|
|
+ if (this.chart) this.chart.resize()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.chemicals-stats {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border-radius: 15px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: $bg-panel;
|
|
|
+ border: 1px solid $border;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- Border beam ---- */
|
|
|
+.border-beam {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ pointer-events: none;
|
|
|
+ border-radius: inherit;
|
|
|
+ overflow: hidden;
|
|
|
+ z-index: 2;
|
|
|
+
|
|
|
+ &::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ top: 0; left: -100%;
|
|
|
+ width: 40%; height: 3px;
|
|
|
+ background: linear-gradient(90deg, transparent, rgba(30,144,255,0.9), rgba(0,216,255,0.7), transparent);
|
|
|
+ animation: beamTop 5s linear infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ &::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0; right: -100%;
|
|
|
+ width: 40%; height: 3px;
|
|
|
+ background: linear-gradient(90deg, transparent, rgba(0,216,255,0.7), rgba(30,144,255,0.9), transparent);
|
|
|
+ animation: beamBottom 5s linear infinite 2.5s;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes beamTop { from { left: -40%; } to { left: 100%; } }
|
|
|
+@keyframes beamBottom { from { right: -40%; } to { right: 100%; } }
|
|
|
+
|
|
|
+/* ---- Corner ornaments ---- */
|
|
|
+.pc {
|
|
|
+ position: absolute;
|
|
|
+ width: 35px; height: 35px;
|
|
|
+ z-index: 3; pointer-events: none;
|
|
|
+ &.tl { top: 0; left: 0; }
|
|
|
+ &.tr { top: 0; right: 0; transform: scaleX(-1); }
|
|
|
+ &.bl { bottom: 0; left: 0; transform: scaleY(-1); }
|
|
|
+ &.br { bottom: 0; right: 0; transform: scale(-1); }
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- Panel header ---- */
|
|
|
+.panel-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 25px;
|
|
|
+ padding: 20px 30px 18px;
|
|
|
+ border-bottom: 1px solid $border;
|
|
|
+ background: linear-gradient(90deg, rgba(0,60,160,0.18), transparent);
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-header-icon {
|
|
|
+ width: 65px; height: 65px;
|
|
|
+ border-radius: 12px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ font-size: 32px;
|
|
|
+ background: linear-gradient(135deg, rgba(30,144,255,0.25), rgba(0,216,255,0.15));
|
|
|
+ border: 1px solid rgba(30,144,255,0.35);
|
|
|
+ animation: iconGlow 3s ease-in-out infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes iconGlow {
|
|
|
+ 0%, 100% { box-shadow: 0 0 15px rgba(30,144,255,0.3); }
|
|
|
+ 50% { box-shadow: 0 0 35px rgba(30,144,255,0.7), 0 0 12px rgba(0,216,255,0.3); }
|
|
|
+}
|
|
|
+
|
|
|
+.panel-title {
|
|
|
+ font-size: 30px;
|
|
|
+ font-weight: 600;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ color: $cyan;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- Stat cards ---- */
|
|
|
+.chem-stat-card {
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 16px 20px;
|
|
|
+ border: 1px solid $border;
|
|
|
+ background: $bg-card;
|
|
|
+
|
|
|
+ &.total { border-color: rgba(0,216,255,0.3); background: rgba(0,216,255,0.05); }
|
|
|
+ &.ctrl { border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.05); }
|
|
|
+ &.free { border-color: rgba(0,216,255,0.3); background: rgba(0,216,255,0.05); }
|
|
|
+ &.cat { border-color: rgba(255,215,64,0.2); background: rgba(255,215,64,0.04); }
|
|
|
+}
|
|
|
+
|
|
|
+.csc-label {
|
|
|
+ font-size: 24px;
|
|
|
+ color: $text-dim;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ letter-spacing: 1px;
|
|
|
+}
|
|
|
+.csc-label-a{
|
|
|
+ font-size: 24px;
|
|
|
+ color: $text-dim;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.csc-value {
|
|
|
+ font-size: 44px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #ef4444;
|
|
|
+ line-height: 1.1;
|
|
|
+}
|
|
|
+.csc-value-a{
|
|
|
+ font-size: 44px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #fff;
|
|
|
+ line-height: 1.1;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.csc-unit {
|
|
|
+ font-size: 24px;
|
|
|
+ font-weight: 400;
|
|
|
+ color: $text-dim;
|
|
|
+ margin-left: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.csc-sub {
|
|
|
+ font-size: 22px;
|
|
|
+ color: rgba(239,68,68,0.55);
|
|
|
+ margin-top: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.csc-pct-ring { flex-shrink: 0; }
|
|
|
+</style>
|