feat(PerformanceComparison): 优化性能对比表格样式和功能

- 重构表格样式,改进视觉层次和交互效果
- 调整变化值显示格式,将百分比和差值分开显示
- 增加表格行的悬停效果和斑马纹
- 改进数值格式化函数,添加单位显示
- 增强选择器交互效果和样式
- 添加NaN检查防止计算错误
This commit is contained in:
2025-10-14 17:43:05 +08:00
parent 3a529bafa8
commit 73c84f7b8d

View File

@@ -25,26 +25,27 @@
<table class="comparison-table"> <table class="comparison-table">
<thead> <thead>
<tr> <tr>
<th>核心指标</th> <th class="metric-col">核心指标</th>
<th>本期数据</th> <th>本期数据</th>
<th>{{ selectedPeriodLabel }}数据</th> <th>{{ selectedPeriodLabel }}数据</th>
<th>变化情况</th> <th class="change-col">变化情况</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="metric in comparedMetrics" :key="metric.key"> <tr v-for="metric in comparedMetrics" :key="metric.key">
<td>{{ metric.label }}</td> <td class="metric-col">
<span class="metric-label">{{ metric.label }}</span>
</td>
<td>{{ formatValue(metric.current, metric.unit) }}</td> <td>{{ formatValue(metric.current, metric.unit) }}</td>
<td>{{ formatValue(metric.previous, metric.unit) }}</td> <td>{{ formatValue(metric.previous, metric.unit) }}</td>
<td> <td class="change-col">
<div class="change-cell" :class="getChangeClass(metric.change.trend)"> <div class="change-cell">
<span class="change-value"> <span class="change-value" :class="getChangeClass(metric.change.trend)">
{{ formatChange(metric.change.diff, metric.unit) }} <span v-if="metric.change.trend === 'up'" class="trend-icon"></span>
({{ metric.change.percentage }}) <span v-if="metric.change.trend === 'down'" class="trend-icon"></span>
{{ metric.change.percentage }}
</span> </span>
<span v-if="metric.change.trend === 'up'" class="trend-icon"></span> <span class="change-diff">{{ formatChange(metric.change.diff, metric.unit) }}</span>
<span v-if="metric.change.trend === 'down'" class="trend-icon"></span>
<span v-if="metric.change.trend === 'neutral'" class="trend-icon">-</span>
</div> </div>
</td> </td>
</tr> </tr>
@@ -174,7 +175,9 @@ const comparedMetrics = computed(() => {
}); });
const calculateChange = (current, previous) => { const calculateChange = (current, previous) => {
if (previous === null || previous === undefined) return { diff: 'N/A', percentage: 'N/A', trend: 'neutral' }; if (previous === null || previous === undefined || isNaN(current) || isNaN(previous)) {
return { diff: 'N/A', percentage: 'N/A', trend: 'neutral' };
}
const diff = current - previous; const diff = current - previous;
let percentage; let percentage;
@@ -195,7 +198,7 @@ const calculateChange = (current, previous) => {
const formatValue = (value, unit) => { const formatValue = (value, unit) => {
if (value === null || value === undefined) return '-'; if (value === null || value === undefined) return '-';
if (unit === '分钟') { if (unit === '分钟') {
return Math.round(value / 60); // 将秒转换为分钟 return `${Math.round(value / 60)} 分钟`;
} }
if (unit === '%') { if (unit === '%') {
return `${value.toFixed(1)}%`; return `${value.toFixed(1)}%`;
@@ -205,57 +208,79 @@ const formatValue = (value, unit) => {
const formatChange = (diff, unit) => { const formatChange = (diff, unit) => {
if (typeof diff !== 'number') return ''; if (typeof diff !== 'number') return '';
const formattedDiff = formatValue(Math.abs(diff), unit); const prefix = diff > 0 ? '+' : '-';
return `${diff > 0 ? '+' : '-'}${formattedDiff}`; const absDiff = Math.abs(diff);
if (unit === '分钟') {
return `${prefix}${Math.round(absDiff / 60)} 分钟`;
}
if (unit === '%') {
return `${prefix}${absDiff.toFixed(1)}%`;
}
return `${prefix}${absDiff.toLocaleString()}`;
}; };
const getChangeClass = (trend) => { const getChangeClass = (trend) => {
if (trend === 'up') return 'text-positive'; if (trend === 'up') return 'positive';
if (trend === 'down') return 'text-negative'; if (trend === 'down') return 'negative';
return 'text-neutral'; return 'neutral';
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.performance-comparison { .performance-comparison {
background: white; background: #ffffff;
border-radius: 12px; border-radius: 12px;
padding: 1.5rem; padding: 2rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-top: 1rem; margin-top: 1.5rem;
border: 1px solid #e2e8f0;
} }
.comparison-header { .comparison-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 1.5rem; margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #f1f5f9;
h2 { h2 {
font-size: 1.4rem; font-size: 1.5rem;
font-weight: 600; font-weight: 700;
color: #1e293b; color: #1a202c;
margin: 0; margin: 0;
} }
.period-selector-wrapper { .period-selector-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.75rem;
label { label {
font-size: 0.9rem; font-size: 0.9rem;
color: #64748b; color: #718096;
} }
.period-select { .period-select {
padding: 0.5rem; padding: 0.6rem 1rem;
border-radius: 6px; border-radius: 8px;
border: 1px solid #e2e8f0; border: 1px solid #cbd5e0;
background-color: #f8fafc; background-color: #ffffff;
font-size: 0.9rem; font-size: 0.9rem;
cursor: pointer; cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s;
&:hover {
border-color: #a0aec0;
}
&:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}
} }
} }
} }
@@ -269,67 +294,108 @@ const getChangeClass = (trend) => {
border-collapse: collapse; border-collapse: collapse;
th, td { th, td {
padding: 0.85rem 1rem; padding: 1rem 1.25rem;
text-align: left; text-align: left;
border-bottom: 1px solid #f1f5f9; vertical-align: middle;
} }
thead th { thead th {
font-size: 0.8rem; font-size: 0.8rem;
color: #64748b; color: #718096;
font-weight: 500; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
background-color: #f8fafc; background-color: #f7fafc;
border-bottom: 2px solid #e2e8f0;
}
tbody tr {
transition: background-color 0.15s ease-in-out;
&:nth-child(odd) {
background-color: #fdfdff;
}
&:not(:last-child) {
border-bottom: 1px solid #f1f5f9;
}
&:hover {
background-color: #f0f5ff;
}
} }
tbody td { tbody td {
font-size: 0.95rem; font-size: 0.95rem;
color: #334155; color: #2d3748;
}
.metric-col {
min-width: 150px;
font-weight: 500;
} }
tbody tr:last-child td { .change-col {
border-bottom: none; width: 180px;
} }
}
.change-cell { .change-cell {
display: flex; display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
.change-value {
display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.35rem;
font-weight: 600; font-weight: 600;
padding: 0.25rem 0.6rem;
border-radius: 9999px;
font-size: 0.85rem;
&.positive {
background-color: #ecfdf5;
color: #065f46;
}
&.negative {
background-color: #fff1f2;
color: #9f1239;
}
&.neutral {
background-color: #f8fafc;
color: #475569;
}
.trend-icon { .trend-icon {
font-size: 1.1rem; font-size: 1rem;
} }
} }
.change-diff {
font-size: 0.8rem;
color: #718096;
margin-left: 0.25rem;
}
} }
.text-positive {
color: #10b981; /* 绿色 */
}
.text-negative {
color: #ef4444; /* 红色 */
}
.text-neutral {
color: #64748b; /* 灰色 */
}
.loading-state, .empty-state { .loading-state, .empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 3rem; padding: 4rem;
color: #64748b; color: #a0aec0;
font-size: 0.9rem; font-size: 1rem;
background-color: #f7fafc;
border-radius: 8px;
.loading-spinner { .loading-spinner {
width: 32px; width: 40px;
height: 32px; height: 40px;
border: 3px solid #e2e8f0; border: 4px solid #e2e8f0;
border-top: 3px solid #3b82f6; border-top: 4px solid #4299e1;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin-bottom: 1rem; margin-bottom: 1.5rem;
} }
} }