release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="enrollment-chart">
|
||||
<div class="enrollment-chart" :class="{ 'enrollment-chart--compact': compact }">
|
||||
<StateLoading v-if="loading" :rows="5" />
|
||||
<div v-else-if="items.length === 0" class="chart-empty-shell">
|
||||
<div class="chart-empty-body">
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<div v-else class="chart-body">
|
||||
<div class="chart-scroll">
|
||||
<div class="chart-plot">
|
||||
<div class="chart-plot" :style="chartPlotStyle">
|
||||
<svg class="chart-svg" :viewBox="`0 0 ${chartWidth} ${chartHeight}`" preserveAspectRatio="xMidYMid meet">
|
||||
<defs>
|
||||
<linearGradient :id="gradientTargetId" x1="0" y1="0" x2="0" y2="1">
|
||||
@@ -105,23 +105,40 @@ const props = withDefaults(
|
||||
items: EnrollmentBarItem[];
|
||||
loading?: boolean;
|
||||
emptyText?: string;
|
||||
compact?: boolean;
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
emptyText: "暂无入组数据",
|
||||
compact: false,
|
||||
}
|
||||
);
|
||||
|
||||
const compact = computed(() => props.compact);
|
||||
const showTarget = computed(() => props.mode === "center");
|
||||
|
||||
const chartWidth = 960;
|
||||
const chartHeight = 260;
|
||||
const axisPadding = {
|
||||
top: 28,
|
||||
right: 32,
|
||||
bottom: 48,
|
||||
left: 56,
|
||||
};
|
||||
const baseChartWidth = computed(() => (compact.value ? 560 : 960));
|
||||
const chartHeight = computed(() => (compact.value ? 240 : 260));
|
||||
const axisPadding = computed(() => ({
|
||||
top: compact.value ? 24 : 28,
|
||||
right: compact.value ? 24 : 32,
|
||||
bottom: compact.value ? 42 : 48,
|
||||
left: compact.value ? 46 : 56,
|
||||
}));
|
||||
|
||||
const chartWidth = computed(() => {
|
||||
if (!compact.value) return baseChartWidth.value;
|
||||
const itemWidth = showTarget.value ? 72 : 64;
|
||||
return Math.max(
|
||||
baseChartWidth.value,
|
||||
props.items.length * itemWidth + axisPadding.value.left + axisPadding.value.right,
|
||||
);
|
||||
});
|
||||
|
||||
const chartPlotStyle = computed(() => ({
|
||||
"--chart-min-width": `${chartWidth.value}px`,
|
||||
"--chart-aspect": `${chartWidth.value} / ${chartHeight.value}`,
|
||||
}));
|
||||
|
||||
const gradientTargetId = `enroll-target-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const gradientActualId = `enroll-actual-${Math.random().toString(36).slice(2, 8)}`;
|
||||
@@ -146,36 +163,37 @@ const yTicks = computed(() => {
|
||||
});
|
||||
|
||||
const axisMax = computed(() => Math.max(1, yTicks.value[0]?.value ?? 1));
|
||||
const plotWidth = computed(() => chartWidth - axisPadding.left - axisPadding.right);
|
||||
const plotHeight = computed(() => chartHeight - axisPadding.top - axisPadding.bottom);
|
||||
const axisLeft = axisPadding.left;
|
||||
const axisRight = chartWidth - axisPadding.right;
|
||||
const axisTop = axisPadding.top;
|
||||
const axisBottom = chartHeight - axisPadding.bottom;
|
||||
const labelY = axisBottom + 20;
|
||||
const valueGap = 10;
|
||||
const barRadius = 6;
|
||||
const plotWidth = computed(() => chartWidth.value - axisPadding.value.left - axisPadding.value.right);
|
||||
const plotHeight = computed(() => chartHeight.value - axisPadding.value.top - axisPadding.value.bottom);
|
||||
const axisLeft = computed(() => axisPadding.value.left);
|
||||
const axisRight = computed(() => chartWidth.value - axisPadding.value.right);
|
||||
const axisTop = computed(() => axisPadding.value.top);
|
||||
const axisBottom = computed(() => chartHeight.value - axisPadding.value.bottom);
|
||||
const labelY = computed(() => axisBottom.value + (compact.value ? 18 : 20));
|
||||
const valueGap = computed(() => (compact.value ? 8 : 10));
|
||||
const barRadius = computed(() => (compact.value ? 5 : 6));
|
||||
|
||||
const bandWidth = computed(() => (props.items.length ? plotWidth.value / props.items.length : plotWidth.value));
|
||||
const barWidth = computed(() => Math.min(52, bandWidth.value * 0.5));
|
||||
const barWidth = computed(() => Math.min(compact.value ? 44 : 52, bandWidth.value * 0.5));
|
||||
|
||||
const barCenter = (index: number) => axisLeft + bandWidth.value * index + bandWidth.value / 2;
|
||||
const barCenter = (index: number) => axisLeft.value + bandWidth.value * index + bandWidth.value / 2;
|
||||
const barLeft = (index: number) => barCenter(index) - barWidth.value / 2;
|
||||
const barHeight = (value: number) => {
|
||||
if (value <= 0) return 0;
|
||||
return (value / axisMax.value) * plotHeight.value;
|
||||
};
|
||||
const barTop = (value: number) => axisTop + plotHeight.value - barHeight(value);
|
||||
const tickY = (value: number) => axisTop + ((axisMax.value - value) / axisMax.value) * plotHeight.value;
|
||||
const barTop = (value: number) => axisTop.value + plotHeight.value - barHeight(value);
|
||||
const tickY = (value: number) => axisTop.value + ((axisMax.value - value) / axisMax.value) * plotHeight.value;
|
||||
|
||||
const valueY = (item: EnrollmentBarItem) => {
|
||||
const anchor = showTarget.value ? Math.max(item.actual, item.target || 0) : item.actual;
|
||||
return barTop(anchor) - valueGap;
|
||||
return barTop(anchor) - valueGap.value;
|
||||
};
|
||||
|
||||
const truncateLabel = (label: string) => {
|
||||
if (label.length <= 8) return label;
|
||||
return `${label.slice(0, 7)}...`;
|
||||
const limit = compact.value ? 7 : 8;
|
||||
if (label.length <= limit) return label;
|
||||
return `${label.slice(0, limit - 1)}...`;
|
||||
};
|
||||
|
||||
const formatNumber = (value: number) => new Intl.NumberFormat("zh-CN").format(value || 0);
|
||||
@@ -228,7 +246,8 @@ const formatNumber = (value: number) => new Intl.NumberFormat("zh-CN").format(va
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, #fafbfd 0%, #f6f8fb 100%);
|
||||
position: relative;
|
||||
aspect-ratio: 960 / 260;
|
||||
width: 100%;
|
||||
aspect-ratio: var(--chart-aspect);
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@@ -297,6 +316,33 @@ const formatNumber = (value: number) => new Intl.NumberFormat("zh-CN").format(va
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.enrollment-chart--compact .chart-plot {
|
||||
width: max(100%, var(--chart-min-width));
|
||||
min-height: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.enrollment-chart--compact .chart-scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.enrollment-chart--compact .tick-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.enrollment-chart--compact .bar-value {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.enrollment-chart--compact .bar-value-actual {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.enrollment-chart--compact .bar-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.bar-label {
|
||||
font-size: 11px;
|
||||
|
||||
Reference in New Issue
Block a user