Files
medical-mall/pages/mall/admin/homePage/components/KpiMiniCard.uvue
2026-02-05 11:36:55 +08:00

220 lines
4.7 KiB
Plaintext

<template>
<view class="kpi-card">
<!-- Header -->
<view class="kpi-header">
<text class="kpi-title">{{ title }}</text>
<view v-if="tagText" class="kpi-tag">
<text class="kpi-tag-text">{{ tagText }}</text>
</view>
<!-- 可选:你想在右上角塞额外按钮/图标 -->
<slot name="headerRight"></slot>
</view>
<!-- Body -->
<view class="kpi-body">
<text class="kpi-main-value">{{ valuePrefix }}{{ valueText }}</text>
<!-- 中间“昨日 / 日环比”行(可完全替换) -->
<view v-if="metaLeft || metaRight" class="kpi-meta">
<text v-if="metaLeft" class="kpi-meta-text">{{ metaLeft }}</text>
<view v-if="metaRight" class="kpi-meta-right">
<text class="kpi-meta-text">{{ metaRight }}</text>
<text
v-if="trend !== 'none'"
class="kpi-trend-arrow"
:class="trendClass"
>
{{ trendArrow }}
</text>
</view>
<!-- 可选:完全自定义这行 -->
<slot name="meta"></slot>
</view>
<view class="kpi-divider"></view>
<!-- 底部一行:左文案 + 右数值 -->
<view class="kpi-footer">
<text class="kpi-footer-left">{{ footerLeftText }}</text>
<text class="kpi-footer-right">{{ footerRightText }}</text>
<!-- 可选:完全自定义 footer -->
<slot name="footer"></slot>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { computed } from 'vue'
const props = withDefaults(defineProps<{
// Header
title: string
tagText?: string
// Body main
valueText: string
valuePrefix?: string // 例如 "¥"
// Meta line (可替换)
metaLeft?: string // 例如 "昨日 4"
metaRight?: string // 例如 "日环比 0%"
trend?: 'up' | 'down' | 'flat' | 'none' // none = 不显示箭头
// Footer
footerLeftText: string // 例如 "本月订单量"
footerRightText: string // 例如 "181单"
}>(), {
tagText: '今日',
valuePrefix: '',
metaLeft: '',
metaRight: '',
trend: 'none'
})
const trendArrow = computed((): string => {
if (props.trend === 'up') return '▲'
if (props.trend === 'down') return '▼'
return ''
})
const trendClass = computed((): string => {
if (props.trend === 'up') return 'is-up'
if (props.trend === 'down') return 'is-down'
return 'is-flat'
})
</script>
<style>
.kpi-card{
background-color:#ffffff;
border-radius:4px;
padding:16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.3s;
height: 200px; /* 固定高度 */
min-width: 0; /* 允许由父级 grid 容器决定宽度,防止在 4 列布局时撑爆容器 */
display: flex;
flex-direction: column;
overflow: hidden;
}
.kpi-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
/* Header */
.kpi-header{
display:flex;
flex-direction: row;
align-items:center;
justify-content:space-between;
margin-bottom: 8px;
flex-shrink: 0;
.kpi-title{
font-size:14px;
color:#666666;
font-weight:400;
}
.kpi-tag{
padding:1px 6px;
border-radius:2px;
background-color: #e8f4ff;
}
.kpi-tag-text{
font-size:12px;
color:#1890ff;
}
}
/* Body */
.kpi-body{
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.kpi-main-value{
font-size:30px;
font-weight:500;
color:#262626;
line-height:1.2;
margin-bottom: 4px;
}
/* “昨日 / 日环比” */
.kpi-meta{
display:flex;
flex-direction: row;
align-items:center;
gap:8px;
padding-bottom:12px;
border-bottom:1px solid #f0f0f0;
margin-bottom: auto; /* 将 footer 顶到底部 */
flex-wrap: nowrap; /* 不允许换行,依靠父容器 min-width 保证空间 */
}
.kpi-meta-text{
font-size:12px;
color:#8c8c8c;
flex-shrink: 0;
}
.kpi-meta-right{
display:flex;
flex-direction: row;
align-items:center;
gap:4px;
flex-shrink: 0;
}
.kpi-trend-arrow{
font-size:12px;
font-weight: 500;
}
.kpi-trend-arrow.is-up{ color:#ff4d4f; }
.kpi-trend-arrow.is-down{ color:#52c41a; }
.kpi-trend-arrow.is-flat{ color:#8c8c8c; }
.kpi-divider{
display: none; /* 已整合到 meta 的 border-bottom */
}
/* Footer */
.kpi-footer{
display:flex;
flex-direction: row;
align-items:center;
justify-content:space-between;
flex-shrink: 0;
}
.kpi-footer-left{
font-size:12px;
color:#8c8c8c;
white-space: nowrap;
}
.kpi-footer-right{
font-size:12px;
color:#262626;
font-weight:500;
white-space: nowrap;
}
}
/* 响应式微调 */
@media screen and (max-width: 480px) {
.kpi-main-value {
font-size: 26px !important;
}
.kpi-card {
padding: 12px !important;
}
}
</style>