Files
yudao-dev/src/views/home/components/purchase-Item.vue
2026-04-14 13:54:05 +08:00

561 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<div class="coreItem">
<div class="item" @click="handleRoute(item.route)" v-for="(item, index) in itemList" :key="index" v-if='index<4'>
<div class="name">{{ item.name }}</div>
<div class="item-content">
<div class="content-wrapper">
<div class="left">
<div class="number" style="color: rgba(103, 103, 103, 0.79);">{{ item.targetValue }}</div>
<div class="title" style="color: rgba(134, 134, 135, 1);">目标值</div>
</div>
<div class="line"></div>
<!-- 实际值根据 实际值目标值 动态绑定类名 -->
<div class="right">
<div class="number" :class="{
'number-exceed': item.progress >= 100,
'number-below': item.progress < 100
}">
{{ item.currentValue }}
</div>
<div class="title" style="color: rgba(134, 134, 135, 1);">实际值</div>
</div>
</div>
<div class="line"></div>
<!-- 进度条同步绑定类名 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{ width: item.progressWidth + '%' }" :class="{
'bar-exceed': item.progress >= 100,
'bar-below': item.progress < 100
}"></div>
</div>
</div>
<!-- 完成率同步绑定类名 -->
<div class="yield" style="display: flex;justify-content: space-between;">
<div class="progress-percent" :class="{
'percent-exceed': item.progress >= 100,
'percent-below': item.progress < 100
}">完成率</div>
<div class="progress-percent" :class="{
'percent-exceed': item.progress >= 100,
'percent-below': item.progress < 100
}">
{{ item.progress }}%
</div>
</div>
</div>
</div>
</div>
<div class="itemBottom">
<div class="item" v-for="(item, index) in itemList" :key="index" @click="handleRoute(item.route)" v-if='index>=4'>
<div class="unit">{{ item.name }}</div>
<div class="item-content">
<div class="content-wrapper">
<div class="left">
<div class="number">{{ item.targetValue }}</div>
<div class="title">目标值</div>
</div>
<div class="line"></div>
<!-- 实际值:根据与目标值的比较动态变色 -->
<div class="right">
<div class="number" :class="{
'exceed-target': item.progress > 100,
'below-target': item.progress < 100,
'equal-target': item.progress == 100
}">
{{ item.currentValue }}
</div>
<div class="title">实际值</div>
</div>
</div>
<!-- 进度条和百分比:同步变色逻辑 -->
<div class="progress-group">
<div class="progress-container">
<div class="progress-bar" :style="{ width: item.progress + '%' }" :class="{
'exceed-pro-target': item.progress > 100,
'below-pro-target': item.progress < 100,
'equal-pro-target': item.progress == 100
}"></div>
</div>
<div class="progress-percent" :class="{
'exceed-target': item.progress > 100,
'below-target': item.progress < 100,
'equal-target': item.progress == 100
}">
{{ item.progress }}%
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Container",
components: {},
props: {
finance: {
type: Object,
default: () => ({}) // 明确props默认值为空对象
},
dateData: {
type: Object,
default: () => ({})
}
},
data() {
return {
// 关键修复1初始化itemList为空数组必选否则初始状态下v-for报错且数据无法响应式更新
itemList: []
};
},
watch: {
finance: {
handler(newVal) {
// 关键修复2增强数据判断避免空值/无效值触发错误
if (newVal && Object.keys(newVal).length > 0) {
// 转换数据并赋值给itemList响应式更新
this.itemList = this.transformData(newVal);
console.log('finance更新itemList已同步', this.itemList);
} else {
// 当finance为空时重置itemList为空数组
this.itemList = [];
}
},
immediate: true, // 组件挂载时立即执行,初始化数据
deep: true // 深度监听finance对象内部属性变化
}
},
methods: {
// 解析rate字符串提取百分比数值
// 改进的 parseRateString 方法:能处理数字和字符串类型
parseRateString(rateValue) {
// 如果是 undefined 或 null返回默认值
if (rateValue === undefined || rateValue === null) {
return { displayText: '0%', progressValue: 0 };
}
// 如果是数字类型,直接处理
if (typeof rateValue === 'number') {
return {
displayText: `${rateValue.toFixed(2)}%`,
progressValue: Math.min(Math.max(rateValue, 0), 100)
};
}
// 如果是字符串类型,使用正则表达式处理
if (typeof rateValue === 'string') {
// 尝试匹配百分比数字,如"减亏93%"中的93
const match = rateValue.match(/(\d+(\.\d+)?)%/);
if (match) {
const percentValue = parseFloat(match[1]);
return {
displayText: rateValue,
progressValue: Math.min(Math.max(percentValue, 0), 100)
};
}
// 如果没有匹配到百分比,尝试解析纯数字
const numMatch = rateValue.match(/\d+(\.\d+)?/);
if (numMatch) {
const numValue = parseFloat(numMatch[0]);
return {
displayText: rateValue,
progressValue: Math.min(Math.max(numValue, 0), 100)
};
}
}
// 默认返回
return {
displayText: '0%',
progressValue: 0
};
},
transformData(rawData) {
// 定义指标映射关系,包括名称、对应的数据键和路由
const Mapping = [
{
key: 'operatingRevenue',
name: '营业收入·万元',
route: '/operatingRevenue/operatingRevenueIndex',
isPercentage: true // 需要加%符号
},
{
key: 'operatingIncome',
name: '经营性利润·万元',
route: '/operatingProfit/operatingProfit',
isPercentage: false // 不需要加%符号使用原始rate字符串
},
{
key: 'totalProfit',
name: '利润总额·万元',
route: '/totalProfit/totalProfit',
isPercentage: false // 不需要加%符号使用原始rate字符串
},
{
key: 'grossMargin',
name: '毛利率·%',
route: '/grossMargin/grossMargin',
isPercentage: true // 需要加%符号
},
{
key: 'accountsReceivable',
name: '应收账款·万元',
route: '/accountsReceivable/accountsReceivableIndex',
isPercentage: false // 需要加%符号
},
{
key: 'inventory',
name: '存货·万元',
route: '/inventoryAnalysis/inventoryAnalysisIndex',
isPercentage: false // 需要加%符号
}
];
// 遍历映射关系,转换数据
return Mapping.map(mappingItem => {
// 关键修复3兜底更严谨避免rawData[mappingItem.key]不存在导致报错
const data = rawData[mappingItem.key] || { rate: '0%', real: 0, target: 0 };
// 额外兜底避免data中的属性为undefined
const target = data.target || 0;
const real = data.real || 0;
const rate = data.rate || 0;
// 解析rate字符串
const parsedRate = this.parseRateString(rate);
// 进度条宽度限制在0-100之间
const progressWidth = Math.min(Math.max(parsedRate.progressValue, 0), 100);
return {
name: mappingItem.name,
targetValue: target,
currentValue: real,
progressWidth: progressWidth, // 用于进度条宽度
progress: rate, // 用于显示文本
route: mappingItem.route
};
});
},
handleRoute(route) {
if (route) {
this.$router.push({
path: route,
query: {
// 关键修复4dateData是对象需序列化后传递否则路由query无法正常接收对象
dateData: this.dateData
}
});
}
}
}
};
</script>
<style scoped lang="scss">
.coreItem {
display: flex;
gap: 8px;
.item {
width: 170px;
height: 168px;
background: #f9fcff;
padding: 12px 0px 0px 12px;
box-sizing: border-box;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
transform: translateY(-2px);
}
.name {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
margin-bottom: 2px;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 26px);
}
.content-wrapper {
display: flex;
flex-direction: column;
gap: 2px;
}
.line {
width: 149px;
height: 1px;
background: linear-gradient(to left, rgba(255, 0, 0, 0), #cbcbcb);
}
.left,
.right {
margin-top: 0px;
display: flex;
flex-direction: column;
gap: 2px;
width: 100%;
}
/* 实际值 - 基础样式(无颜色) */
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
line-height: 22px;
text-align: left;
font-style: normal;
}
/* 实际值 - 实际值≥目标值(绿色) */
.number-exceed {
color: rgba(54, 181, 138, 1) !important;
}
/* 实际值 - 实际值<目标值(黄色) */
.number-below {
color: rgba(249, 164, 74, 1) !important;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: left;
font-style: normal;
}
.progress-group {
display: flex;
align-items: center;
gap: 8px;
margin-top: 2px;
}
.progress-container {
width: 138px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
/* 进度条 - 基础样式(无颜色) */
.progress-bar {
height: 100%;
border-radius: 8px;
transition: width 0.5s ease;
}
/* 进度条 - 实际值≥目标值(绿色) */
.bar-exceed {
background: rgba(98, 213, 180, 1) !important;
opacity: 1 !important;
}
/* 进度条 - 实际值<目标值(黄色) */
.bar-below {
background: rgba(249, 164, 74, 1) !important;
opacity: 1 !important;
}
/* 百分比 - 基础样式(无颜色) */
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
line-height: 1;
}
/* 百分比 - 实际值≥目标值(绿色) */
.percent-exceed {
color: rgba(54, 181, 138, 1) !important;
}
/* 百分比 - 实际值<目标值(黄色) */
.percent-below {
color: rgba(249, 164, 74, 1) !important;
}
.yield {
width: 138px;
margin-top: 3px;
}
}
}
.itemBottom {
display: flex;
gap: 8px;
margin-top: 5px;
.item {
width: 350px;
height: 90px;
background: #f9fcff;
padding: 8px 8px 0px;
box-sizing: border-box;
cursor: pointer; // 提示可点击
transition: all 0.3s ease; // 动画过渡
&:hover {
box-shadow: 0px 4px 12px 2px #B5CDE5;
transform: translateY(-2px); // 轻微上浮增强交互感
}
.unit {
height: 18px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 18px;
color: #000000;
line-height: 18px;
letter-spacing: 1px;
text-align: left;
font-style: normal;
margin-bottom: 2px;
}
.item-content {
display: flex;
flex-direction: column;
justify-content: space-between;
height: calc(100% - 29px);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: space-around;
flex: 1;
}
.line {
width: 1px;
height: 46px;
background: linear-gradient(to bottom, rgba(255, 0, 0, 0), #cbcbcb);
}
.left,
.right {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 2px;
flex: 1;
}
.number {
height: 22px;
font-family: PingFangSC, PingFang SC;
font-weight: 600;
font-size: 24px;
color: rgba(103, 103, 103, 0.79);
/* 默认颜色(等于目标值时) */
line-height: 22px;
text-align: center;
font-style: normal;
}
.title {
height: 14px;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
line-height: 14px;
text-align: center;
font-style: normal;
}
.progress-group {
display: flex;
align-items: center;
gap: 8px;
}
.progress-container {
width: 280px;
height: 10px;
background: #ECEFF7;
border-radius: 8px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: rgba(98, 213, 180, 1);
/* 默认进度条颜色(等于目标值时) */
border-radius: 8px;
opacity: 0.7;
transition: width 0.5s ease; // 进度条动画
}
.progress-percent {
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 12px;
color: #868687;
/* 默认百分比颜色(等于目标值时) */
line-height: 1;
}
/* 实际值 > 目标值:绿色样式 */
.exceed-target {
color: rgba(98, 213, 180, 1) !important;
/* 文字绿色 */
// background: rgba(98, 213, 180, 1) !important;
/* 进度条绿色 */
opacity: 1 !important;
}
/* 实际值 < 目标值:黄色样式 */
.below-target {
color: rgba(249, 164, 74, 1) !important;
/* 文字黄色 */
// background: rgba(249, 164, 74, 1) !important;
/* 进度条黄色 */
opacity: 1 !important;
}
.exceed-pro-target {
// color: rgba(98, 213, 180, 1) !important;
/* 文字绿色 */
background: rgba(98, 213, 180, 1) !important;
/* 进度条绿色 */
opacity: 1 !important;
}
/* 实际值 < 目标值:黄色样式 */
.below-pro-target {
// color: rgba(249, 164, 74, 1) !important;
/* 文字黄色 */
background: rgba(249, 164, 74, 1) !important;
/* 进度条黄色 */
opacity: 1 !important;
}
/* 实际值 = 目标值:默认灰色(可自定义) */
.equal-target{
color: rgba(98, 213, 180, 1) !important;
/* 文字绿色 */
// background: rgba(98, 213, 180, 1) !important;
/* 进度条绿色 */
opacity: 1 !important;
}
.equal-pro-target {
// color: rgba(98, 213, 180, 1) !important;
/* 文字绿色 */
background: rgba(98, 213, 180, 1) !important;
/* 进度条绿色 */
opacity: 1 !important;
}
}
}
</style>