新增页面

This commit is contained in:
‘937886381’
2025-12-25 16:48:17 +08:00
parent 2d200dd1a6
commit 80deffbb42
406 changed files with 108362 additions and 189 deletions

View File

@@ -0,0 +1,485 @@
<template>
<div class="coreBar">
<div class="header-row">
<div class="base-title">各基地情况</div>
<div class="barTop">
<div class="right-container">
<div class="legend">
<span class="legend-item">
<span class="legend-icon line yield"></span>完成率
</span>
<span class="legend-item">
<span class="legend-icon square target"></span>预算
</span>
<span class="legend-item">
<span class="legend-icon square achieved"></span>实际·达标
</span>
<span class="legend-item">
<span class="legend-icon square unachieved"></span>实际·未达标
</span>
</div>
<div class="button-group">
<div class="item-button category-btn">
<span class="item-text">展示顺序</span>
</div>
<div class="dropdown-container">
<div class="item-button profit-btn active" @click.stop="isDropdownShow = !isDropdownShow">
<span class="item-text profit-text">{{ selectedSort || '请选择' }}</span>
<span class="dropdown-arrow" :class="{ 'rotate': isDropdownShow }"></span>
</div>
<div class="dropdown-options" v-if="isDropdownShow">
<div class="dropdown-option" v-for="(item, index) in profitOptions" :key="index"
@click.stop="selectProfit(item)">
{{ item.label }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="lineBottom" style="height: 100%; width: 100%">
<operatingLineBar :chartData="chartD" style="height: 99%; width: 100%" />
</div>
</div>
</template>
<script>
import operatingLineBar from './operatingLineBarSale.vue';
import * as echarts from 'echarts';
export default {
name: "Container",
components: { operatingLineBar },
props: ["chartData"],
emits: ['sort-change'], // 声明事件Vue3 推荐)
data() {
return {
activeButton: 0,
isDropdownShow: false,
selectedSort: null, // 选中的label
selectedSortValue: null, // 选中的value用于排序逻辑
profitOptions: [
{ label: '实际值:高~低', value: 1 },
{ label: '实际值:低~高', value: 2 },
{ label: '完成率:高~低', value: 3 },
{ label: '完成率:低~高', value: 4 },
]
};
},
computed: {
// 排序后的数据源核心根据selectedSortValue重新排序
currentDataSource() {
if (!this.chartData?.factory) return {};
// 深拷贝原始数据,避免修改原数据
const factory = JSON.parse(JSON.stringify(this.chartData.factory));
if (!factory.locations.length || !this.selectedSortValue) return factory;
// 构建带索引的数组,方便同步所有字段排序
const dataWithIndex = factory.locations.map((name, index) => ({
index,
name,
real: factory.reals[index],
target: factory.targets[index],
rate: factory.rates[index],
diff: factory.diff[index],
flag: factory.flags[index]
}));
// 根据选中的排序规则排序
switch (this.selectedSortValue) {
case 1: // 实际值:高~低
dataWithIndex.sort((a, b) => b.real - a.real);
break;
case 2: // 实际值:低~高
dataWithIndex.sort((a, b) => a.real - b.real);
break;
case 3: // 目标值:高~低
dataWithIndex.sort((a, b) => b.rate - a.rate);
break;
case 4: // 目标值:低~高
dataWithIndex.sort((a, b) => a.rate - b.rate);
break;
default:
return factory;
}
// 同步更新所有数组
factory.locations = dataWithIndex.map(item => item.name);
factory.reals = dataWithIndex.map(item => item.real);
factory.targets = dataWithIndex.map(item => item.target);
factory.rates = dataWithIndex.map(item => item.rate);
factory.diff = dataWithIndex.map(item => item.diff);
factory.flags = dataWithIndex.map(item => item.flag);
return factory;
},
locations() {
return this.currentDataSource.locations || [];
},
// 最终传递给图表的排序后数据
chartD() {
const data = this.currentDataSource;
const salesData = {
allPlaceNames: this.locations,
series: [
// 完成率(折线图)
{
name: '完成率',
type: 'line',
yAxisIndex: 1,
lineStyle: { color: 'rgba(40, 138, 255, .5)', width: 2 },
itemStyle: {
color: 'rgba(40, 138, 255, 1)',
borderColor: 'rgba(40, 138, 255, 1)',
borderWidth: 2,
radius: 4
},
areaStyle: {
opacity: 0.2,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(40, 138, 255, .9)' },
{ offset: 1, color: 'rgba(40, 138, 255, 0)' }
])
},
data: data.rates || [],
symbol: 'circle',
symbolSize: 6
},
// 目标(柱状图)
{
name: '预算',
type: 'bar',
yAxisIndex: 0,
barWidth: 14,
itemStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(130, 204, 255, 1)' },
{ offset: 1, color: 'rgba(75, 157, 255, 1)' }
]
},
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: data.targets || []
},
// 实际(柱状图)
{
name: '实际',
type: 'bar',
yAxisIndex: 0,
barWidth: 14,
label: {
show: true,
position: 'top',
offset: [30, 0],
width: 68,
height: 20,
formatter: (params) => {
const diff = data.diff || [];
const currentDiff = diff[params.dataIndex] || 0;
return `{rate|${currentDiff}}{text|差值}`;
},
backgroundColor: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(205, 215, 224, 0.6)' },
{ offset: 0.2, color: '#ffffff' },
{ offset: 1, color: '#ffffff' }
]
},
shadowColor: 'rgba(191,203,215,0.5)',
shadowBlur: 2,
shadowOffsetX: 0,
shadowOffsetY: 2,
borderRadius: 4,
borderColor: '#BFCBD577',
borderWidth: 0,
lineHeight: 20,
rich: {
text: {
width: 'auto',
padding: [5, 10, 5, 0],
align: 'center',
color: '#464646',
fontSize: 11,
lineHeight: 20
},
rate: {
width: 'auto',
padding: [5, 0, 5, 10],
align: 'center',
color: '#30B590',
fontSize: 11,
lineHeight: 20
}
}
},
itemStyle: {
color: (params) => {
const safeFlag = data.flags || [];
const currentFlag = safeFlag[params.dataIndex] || 0;
return currentFlag === 1
? {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(174, 239, 224, 1)' },
{ offset: 1, color: 'rgba(118, 218, 190, 1)' }
]
}
: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(253, 209, 129, 1)' },
{ offset: 1, color: 'rgba(249, 164, 74, 1)' }
]
};
},
borderRadius: [4, 4, 0, 0],
borderWidth: 0
},
data: data.reals || []
}
]
};
return salesData;
}
},
methods: {
selectProfit(item) {
// 更新选中的label和value
this.selectedSort = item.label;
this.selectedSortValue = item.value;
this.isDropdownShow = false;
// 向父组件传递排序事件(可选,保持原有逻辑)
this.$emit('sort-change', item.value);
}
},
// 监听父组件传入的chartData变化重置选中状态可选
watch: {
'chartData.factory': {
handler() {
// 若需要切换数据源后重置排序,可取消注释
// this.selectedSort = null;
// this.selectedSortValue = null;
},
deep: true
}
}
};
</script>
<style scoped lang="scss">
.coreBar {
display: flex;
flex-direction: column;
width: 100%;
padding: 12px;
.header-row {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 8px;
}
.base-title {
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 18px;
letter-spacing: 1px;
font-style: normal;
padding: 0 0 0 16px;
white-space: nowrap;
}
.barTop {
width: auto;
align-items: center;
gap: 16px;
.right-container {
display: flex;
align-items: center;
gap: 24px;
margin-right: 46px;
}
.legend {
display: flex;
gap: 16px;
align-items: center;
margin: 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 14px;
color: rgba(0, 0, 0, 0.8);
text-align: left;
font-style: normal;
white-space: nowrap;
}
.legend-icon {
display: inline-block;
}
.legend-icon.line {
width: 12px;
height: 2px;
position: relative;
&::before {
position: absolute;
content: "";
top: -2px;
left: 3px;
width: 6px;
border-radius: 50%;
height: 6px;
background-color: rgba(40, 138, 255, 1);
}
}
.legend-icon.square {
width: 8px;
height: 8px;
}
.yield {
background: rgba(40, 138, 255, 1);
}
.target {
background: #2889FF;
}
.achieved {
background: rgba(40, 203, 151, 1);
}
.unachieved {
background: rgba(255, 132, 0, 1);
}
.button-group {
display: flex;
position: relative;
gap: 2px;
align-items: center;
height: 24px;
background: #ecf4fe;
margin: 0;
.dropdown-container {
position: relative;
z-index: 10;
}
.item-button {
cursor: pointer;
height: 24px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
line-height: 24px;
font-style: normal;
letter-spacing: 2px;
overflow: hidden;
.item-text {
display: inline-block;
}
}
.category-btn {
width: 75px;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
background: #ffffff;
color: #0b58ff;
text-align: center;
}
.profit-btn {
width: 123px;
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
position: relative;
padding: 0 18px 0 8px;
background: #ffffff;
color: #0b58ff;
text-align: left;
&.active {
background: #3071ff;
color: rgba(249, 252, 255, .8);
}
.profit-text {
text-align: left;
width: 100%;
}
}
.dropdown-arrow {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-left: 6px solid currentColor;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-right: 4px solid transparent;
transition: transform 0.2s ease;
&.rotate {
transform: rotate(90deg);
}
}
.dropdown-options {
position: absolute;
top: 100%;
right: 0;
margin-top: 2px;
width: 123px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
.dropdown-option {
padding: 6px 12px;
font-size: 12px;
color: #333;
cursor: pointer;
text-align: left;
letter-spacing: 1px;
&:hover {
background: #f5f7fa;
color: #3071ff;
}
}
}
}
}
}
</style>