|
@@ -2,26 +2,6 @@
|
|
|
<div class="growth-stage-timeline">
|
|
<div class="growth-stage-timeline">
|
|
|
<div class="growth-stage-timeline__scroll" ref="scrollRef">
|
|
<div class="growth-stage-timeline__scroll" ref="scrollRef">
|
|
|
<div class="growth-stage-timeline__inner" :style="innerStyle">
|
|
<div class="growth-stage-timeline__inner" :style="innerStyle">
|
|
|
- <!-- 生育期背景:同一 period 连续多列合并为一格,标题与描述跨列居中 -->
|
|
|
|
|
- <div
|
|
|
|
|
- class="growth-stage-timeline__bg"
|
|
|
|
|
- :style="{ gridTemplateColumns: gridCols }"
|
|
|
|
|
- >
|
|
|
|
|
- <div
|
|
|
|
|
- v-for="(run, ri) in periodRuns"
|
|
|
|
|
- :key="'bg-run-' + ri"
|
|
|
|
|
- class="growth-stage-timeline__bg-cell growth-stage-timeline__bg-cell--period"
|
|
|
|
|
- :style="{ gridColumn: `span ${run.span}` }"
|
|
|
|
|
- >
|
|
|
|
|
- <div class="growth-stage-timeline__period-title">
|
|
|
|
|
- {{ run.periodTitle }}
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="growth-stage-timeline__period-sub">
|
|
|
|
|
- {{ run.periodSubtitle }}
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
<!-- 轨道:横线 + 节点 + 拖动手柄 -->
|
|
<!-- 轨道:横线 + 节点 + 拖动手柄 -->
|
|
|
<div class="growth-stage-timeline__track" ref="trackRef">
|
|
<div class="growth-stage-timeline__track" ref="trackRef">
|
|
|
<div class="growth-stage-timeline__track-line" aria-hidden="true"></div>
|
|
<div class="growth-stage-timeline__track-line" aria-hidden="true"></div>
|
|
@@ -61,7 +41,13 @@
|
|
|
aria-hidden="true"
|
|
aria-hidden="true"
|
|
|
></div>
|
|
></div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="growth-stage-timeline__handle-body">
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="growth-stage-timeline__handle-body"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'growth-stage-timeline__handle-body--wide':
|
|
|
|
|
+ activeLabelIsShort,
|
|
|
|
|
+ }"
|
|
|
|
|
+ >
|
|
|
<span class="growth-stage-timeline__handle-bar"></span>
|
|
<span class="growth-stage-timeline__handle-bar"></span>
|
|
|
<span class="growth-stage-timeline__handle-bar"></span>
|
|
<span class="growth-stage-timeline__handle-bar"></span>
|
|
|
<span class="growth-stage-timeline__handle-bar"></span>
|
|
<span class="growth-stage-timeline__handle-bar"></span>
|
|
@@ -79,6 +65,10 @@
|
|
|
v-for="(stage, i) in normalizedStages"
|
|
v-for="(stage, i) in normalizedStages"
|
|
|
:key="'lb-' + i"
|
|
:key="'lb-' + i"
|
|
|
class="growth-stage-timeline__label-col"
|
|
class="growth-stage-timeline__label-col"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'growth-stage-timeline__label-col--wide':
|
|
|
|
|
+ isShortStageLabel(stage.label),
|
|
|
|
|
+ }"
|
|
|
>
|
|
>
|
|
|
<div
|
|
<div
|
|
|
class="growth-stage-timeline__label-text"
|
|
class="growth-stage-timeline__label-text"
|
|
@@ -101,6 +91,30 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 生育期卡片:与上方阶段列同网格对齐,随横向滚动联动 -->
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="growth-stage-timeline__bg"
|
|
|
|
|
+ :style="{ gridTemplateColumns: gridCols }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(run, ri) in periodRuns"
|
|
|
|
|
+ :key="'bg-run-' + ri"
|
|
|
|
|
+ class="growth-stage-timeline__bg-cell growth-stage-timeline__bg-cell--period"
|
|
|
|
|
+ :class="{
|
|
|
|
|
+ 'growth-stage-timeline__bg-cell--active':
|
|
|
|
|
+ ri === activePeriodRunIndex,
|
|
|
|
|
+ }"
|
|
|
|
|
+ :style="{ gridColumn: `span ${run.span}` }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="growth-stage-timeline__period-title">
|
|
|
|
|
+ {{ run.periodTitle }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="growth-stage-timeline__period-sub">
|
|
|
|
|
+ {{ run.periodSubtitle }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -186,19 +200,57 @@ const colCount = computed(() =>
|
|
|
Math.max(1, normalizedStages.value.length)
|
|
Math.max(1, normalizedStages.value.length)
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
-const gridCols = computed(() =>
|
|
|
|
|
- `repeat(${colCount.value}, minmax(${props.minColWidth}px, 1fr))`
|
|
|
|
|
|
|
+/** time_discribe 文案过短(<3 字)时,该列占 3 个普通点宽度,便于下方标签展示 */
|
|
|
|
|
+const SHORT_LABEL_MAX_LEN = 3;
|
|
|
|
|
+const SHORT_LABEL_COL_WEIGHT = 3;
|
|
|
|
|
+
|
|
|
|
|
+function isShortStageLabel(label) {
|
|
|
|
|
+ return String(label ?? "").length < SHORT_LABEL_MAX_LEN;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getStageColumnWeight(stage) {
|
|
|
|
|
+ return isShortStageLabel(stage?.label) ? SHORT_LABEL_COL_WEIGHT : 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const columnWeights = computed(() =>
|
|
|
|
|
+ normalizedStages.value.map((stage) => getStageColumnWeight(stage))
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+const totalColumnWeight = computed(() =>
|
|
|
|
|
+ Math.max(
|
|
|
|
|
+ 1,
|
|
|
|
|
+ columnWeights.value.reduce((sum, weight) => sum + weight, 0)
|
|
|
|
|
+ )
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+const gridCols = computed(() => {
|
|
|
|
|
+ const stages = normalizedStages.value;
|
|
|
|
|
+ if (!stages.length) {
|
|
|
|
|
+ return `minmax(${props.minColWidth}px, 1fr)`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return stages
|
|
|
|
|
+ .map((stage) => {
|
|
|
|
|
+ const weight = getStageColumnWeight(stage);
|
|
|
|
|
+ return `minmax(${weight * props.minColWidth}px, ${weight}fr)`;
|
|
|
|
|
+ })
|
|
|
|
|
+ .join(" ");
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
/** 左右留白:手柄 translate(-50%) 与气泡在首尾否则会溢出滚动宽度,且气泡换行会拉高整列导致视觉上“掉下去” */
|
|
/** 左右留白:手柄 translate(-50%) 与气泡在首尾否则会溢出滚动宽度,且气泡换行会拉高整列导致视觉上“掉下去” */
|
|
|
const INNER_EDGE_PAD_PX = 12;
|
|
const INNER_EDGE_PAD_PX = 12;
|
|
|
|
|
|
|
|
-const innerStyle = computed(() => ({
|
|
|
|
|
- boxSizing: "border-box",
|
|
|
|
|
- minWidth: `${colCount.value * props.minColWidth + INNER_EDGE_PAD_PX * 2}px`,
|
|
|
|
|
- paddingLeft: `${INNER_EDGE_PAD_PX}px`,
|
|
|
|
|
- paddingRight: `${INNER_EDGE_PAD_PX}px`,
|
|
|
|
|
-}));
|
|
|
|
|
|
|
+const innerStyle = computed(() => {
|
|
|
|
|
+ const minContentWidth = columnWeights.value.reduce(
|
|
|
|
|
+ (sum, weight) => sum + weight * props.minColWidth,
|
|
|
|
|
+ 0
|
|
|
|
|
+ );
|
|
|
|
|
+ return {
|
|
|
|
|
+ boxSizing: "border-box",
|
|
|
|
|
+ minWidth: `${minContentWidth + INNER_EDGE_PAD_PX * 2}px`,
|
|
|
|
|
+ paddingLeft: `${INNER_EDGE_PAD_PX}px`,
|
|
|
|
|
+ paddingRight: `${INNER_EDGE_PAD_PX}px`,
|
|
|
|
|
+ };
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
function periodKey(stage) {
|
|
function periodKey(stage) {
|
|
|
const s = stage || {};
|
|
const s = stage || {};
|
|
@@ -237,6 +289,20 @@ const periodRuns = computed(() => {
|
|
|
return runs;
|
|
return runs;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+/** 生育期卡片列宽与上方阶段列对齐:每段占 span 列,fr 比例与阶段网格一致 */
|
|
|
|
|
+const periodGridCols = computed(() => {
|
|
|
|
|
+ const runs = periodRuns.value;
|
|
|
|
|
+ if (!runs.length) {
|
|
|
|
|
+ return `minmax(${props.minColWidth}px, 1fr)`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return runs
|
|
|
|
|
+ .map(
|
|
|
|
|
+ (run) =>
|
|
|
|
|
+ `minmax(${run.span * props.minColWidth}px, ${run.span}fr)`
|
|
|
|
|
+ )
|
|
|
|
|
+ .join(" ");
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
function clampStageIndex(v) {
|
|
function clampStageIndex(v) {
|
|
|
const last = Math.max(0, colCount.value - 1);
|
|
const last = Math.max(0, colCount.value - 1);
|
|
|
return Math.min(Math.max(0, v), last);
|
|
return Math.min(Math.max(0, v), last);
|
|
@@ -250,6 +316,10 @@ function middleStageIndex() {
|
|
|
|
|
|
|
|
const activeIndex = ref(0);
|
|
const activeIndex = ref(0);
|
|
|
|
|
|
|
|
|
|
+const activeLabelIsShort = computed(() =>
|
|
|
|
|
+ isShortStageLabel(normalizedStages.value[activeIndex.value]?.label)
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
watch(
|
|
watch(
|
|
|
[colCount, () => props.modelValue],
|
|
[colCount, () => props.modelValue],
|
|
|
() => {
|
|
() => {
|
|
@@ -272,17 +342,54 @@ watch(
|
|
|
/** 手柄水平位置:0~1,对应轨道宽度 */
|
|
/** 手柄水平位置:0~1,对应轨道宽度 */
|
|
|
const handleRatio = ref(0);
|
|
const handleRatio = ref(0);
|
|
|
|
|
|
|
|
|
|
+function stageIndexToPeriodRunIndex(stageIdx) {
|
|
|
|
|
+ let start = 0;
|
|
|
|
|
+ for (let ri = 0; ri < periodRuns.value.length; ri++) {
|
|
|
|
|
+ const span = periodRuns.value[ri].span;
|
|
|
|
|
+ if (stageIdx >= start && stageIdx < start + span) {
|
|
|
|
|
+ return ri;
|
|
|
|
|
+ }
|
|
|
|
|
+ start += span;
|
|
|
|
|
+ }
|
|
|
|
|
+ return 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 视口中心(手柄)所在生育期段索引,随滚动/拖动实时高亮 */
|
|
|
|
|
+const activePeriodRunIndex = computed(() =>
|
|
|
|
|
+ stageIndexToPeriodRunIndex(ratioToIndex(handleRatio.value))
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
function indexToRatio(idx) {
|
|
function indexToRatio(idx) {
|
|
|
- const n = colCount.value;
|
|
|
|
|
- if (n <= 1) return 0.5;
|
|
|
|
|
- return (idx + 0.5) / n;
|
|
|
|
|
|
|
+ const weights = columnWeights.value;
|
|
|
|
|
+ const total = totalColumnWeight.value;
|
|
|
|
|
+ if (weights.length <= 1) return 0.5;
|
|
|
|
|
+ let start = 0;
|
|
|
|
|
+ for (let i = 0; i < idx; i++) {
|
|
|
|
|
+ start += weights[i] ?? 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ const center = start + (weights[idx] ?? 1) / 2;
|
|
|
|
|
+ return center / total;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function ratioToIndex(r) {
|
|
function ratioToIndex(r) {
|
|
|
- const n = colCount.value;
|
|
|
|
|
|
|
+ const weights = columnWeights.value;
|
|
|
|
|
+ const total = totalColumnWeight.value;
|
|
|
|
|
+ const n = weights.length;
|
|
|
if (n <= 1) return 0;
|
|
if (n <= 1) return 0;
|
|
|
- const idx = Math.round(r * n - 0.5);
|
|
|
|
|
- return Math.min(Math.max(0, idx), n - 1);
|
|
|
|
|
|
|
+ const target = Math.min(Math.max(0, r), 1) * total;
|
|
|
|
|
+ let acc = 0;
|
|
|
|
|
+ let bestIdx = 0;
|
|
|
|
|
+ let bestDist = Infinity;
|
|
|
|
|
+ for (let i = 0; i < n; i++) {
|
|
|
|
|
+ const center = acc + (weights[i] ?? 1) / 2;
|
|
|
|
|
+ const dist = Math.abs(target - center);
|
|
|
|
|
+ if (dist < bestDist) {
|
|
|
|
|
+ bestDist = dist;
|
|
|
|
|
+ bestIdx = i;
|
|
|
|
|
+ }
|
|
|
|
|
+ acc += weights[i] ?? 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ return bestIdx;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 禁止在轨道上横向拖滚:仅在手柄拖动时由脚本更新 scrollLeft */
|
|
/** 禁止在轨道上横向拖滚:仅在手柄拖动时由脚本更新 scrollLeft */
|
|
@@ -317,27 +424,116 @@ function getScrollLayout() {
|
|
|
return { scrollEl, cw, maxScroll, pad, contentW };
|
|
return { scrollEl, cw, maxScroll, pad, contentW };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 将轨道上的比例位置(0~1,与手柄 left 一致)滚到视口水平中央 */
|
|
|
|
|
-function scrollToCenterRatio(ratio, behavior = "auto") {
|
|
|
|
|
- const m = getScrollLayout();
|
|
|
|
|
- if (!m) return;
|
|
|
|
|
- const { scrollEl, cw, maxScroll, pad, contentW } = m;
|
|
|
|
|
- const r = Math.min(Math.max(0, ratio), 1);
|
|
|
|
|
- const centerX = pad + r * contentW;
|
|
|
|
|
- let left = centerX - cw / 2;
|
|
|
|
|
- left = Math.max(0, Math.min(left, maxScroll));
|
|
|
|
|
- scrollEl.scrollTo({ left, behavior });
|
|
|
|
|
|
|
+/** 滚动缓动系数,越小滚动越慢(0~1) */
|
|
|
|
|
+const SCROLL_LERP = 0.08;
|
|
|
|
|
+
|
|
|
|
|
+let scrollTargetLeft = null;
|
|
|
|
|
+let scrollAnimRafId = null;
|
|
|
|
|
+
|
|
|
|
|
+function getPeriodRunBounds(runIdx) {
|
|
|
|
|
+ let stageStart = 0;
|
|
|
|
|
+ for (let ri = 0; ri < runIdx; ri++) {
|
|
|
|
|
+ stageStart += periodRuns.value[ri].span;
|
|
|
|
|
+ }
|
|
|
|
|
+ const span = periodRuns.value[runIdx]?.span ?? 1;
|
|
|
|
|
+ const weights = columnWeights.value;
|
|
|
|
|
+ const total = totalColumnWeight.value;
|
|
|
|
|
+ if (!weights.length) {
|
|
|
|
|
+ return { startRatio: 0, endRatio: 1, centerRatio: 0.5 };
|
|
|
|
|
+ }
|
|
|
|
|
+ let weightStart = 0;
|
|
|
|
|
+ for (let i = 0; i < stageStart; i++) {
|
|
|
|
|
+ weightStart += weights[i] ?? 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ let weightSpan = 0;
|
|
|
|
|
+ for (let i = stageStart; i < stageStart + span; i++) {
|
|
|
|
|
+ weightSpan += weights[i] ?? 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ startRatio: weightStart / total,
|
|
|
|
|
+ endRatio: (weightStart + weightSpan) / total,
|
|
|
|
|
+ centerRatio: (weightStart + weightSpan / 2) / total,
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function syncScrollToRatio(ratio) {
|
|
|
|
|
|
|
+function ratioToScrollLeft(ratio, stageIdx = null) {
|
|
|
const m = getScrollLayout();
|
|
const m = getScrollLayout();
|
|
|
- if (!m) return;
|
|
|
|
|
- const { scrollEl, cw, maxScroll, pad, contentW } = m;
|
|
|
|
|
|
|
+ if (!m) return null;
|
|
|
|
|
+ const { cw, maxScroll, pad, contentW } = m;
|
|
|
const r = Math.min(Math.max(0, ratio), 1);
|
|
const r = Math.min(Math.max(0, ratio), 1);
|
|
|
- const centerX = pad + r * contentW;
|
|
|
|
|
- let left = centerX - cw / 2;
|
|
|
|
|
- left = Math.max(0, Math.min(left, maxScroll));
|
|
|
|
|
- scrollEl.scrollLeft = left;
|
|
|
|
|
|
|
+ const handleCenterX = pad + r * contentW;
|
|
|
|
|
+
|
|
|
|
|
+ const idx =
|
|
|
|
|
+ stageIdx != null
|
|
|
|
|
+ ? clampStageIndex(stageIdx)
|
|
|
|
|
+ : ratioToIndex(ratio);
|
|
|
|
|
+ const runIdx = stageIndexToPeriodRunIndex(idx);
|
|
|
|
|
+ const { startRatio, endRatio } = getPeriodRunBounds(runIdx);
|
|
|
|
|
+ const periodLeft = pad + startRatio * contentW;
|
|
|
|
|
+ const periodRight = pad + endRatio * contentW;
|
|
|
|
|
+ const periodWidth = periodRight - periodLeft;
|
|
|
|
|
+
|
|
|
|
|
+ let left = handleCenterX - cw / 2;
|
|
|
|
|
+ if (periodWidth <= cw) {
|
|
|
|
|
+ if (periodLeft < left) left = periodLeft;
|
|
|
|
|
+ if (periodRight > left + cw) left = periodRight - cw;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ left = Math.max(periodLeft, Math.min(left, periodRight - cw));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Math.max(0, Math.min(left, maxScroll));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function cancelScrollAnimation() {
|
|
|
|
|
+ if (scrollAnimRafId != null) {
|
|
|
|
|
+ cancelAnimationFrame(scrollAnimRafId);
|
|
|
|
|
+ scrollAnimRafId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function runScrollAnimation() {
|
|
|
|
|
+ const scrollEl = scrollRef.value;
|
|
|
|
|
+ if (!scrollEl || scrollTargetLeft == null) {
|
|
|
|
|
+ scrollAnimRafId = null;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const current = scrollEl.scrollLeft;
|
|
|
|
|
+ const diff = scrollTargetLeft - current;
|
|
|
|
|
+ if (Math.abs(diff) < 0.5) {
|
|
|
|
|
+ scrollEl.scrollLeft = scrollTargetLeft;
|
|
|
|
|
+ scrollTargetLeft = null;
|
|
|
|
|
+ scrollAnimRafId = null;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ scrollEl.scrollLeft = current + diff * SCROLL_LERP;
|
|
|
|
|
+ scrollAnimRafId = requestAnimationFrame(runScrollAnimation);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function applyScrollToRatio(ratio, immediate = false, stageIdx = null) {
|
|
|
|
|
+ const left = ratioToScrollLeft(ratio, stageIdx);
|
|
|
|
|
+ if (left == null) return;
|
|
|
|
|
+ const scrollEl = scrollRef.value;
|
|
|
|
|
+ if (!scrollEl) return;
|
|
|
|
|
+
|
|
|
|
|
+ scrollTargetLeft = left;
|
|
|
|
|
+ if (immediate) {
|
|
|
|
|
+ cancelScrollAnimation();
|
|
|
|
|
+ scrollEl.scrollLeft = left;
|
|
|
|
|
+ scrollTargetLeft = null;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (scrollAnimRafId == null) {
|
|
|
|
|
+ scrollAnimRafId = requestAnimationFrame(runScrollAnimation);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 将轨道上的比例位置(0~1,与手柄 left 一致)滚到视口水平中央 */
|
|
|
|
|
+function scrollToCenterRatio(ratio, behavior = "smooth", stageIdx = null) {
|
|
|
|
|
+ applyScrollToRatio(ratio, behavior === "auto", stageIdx);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function syncScrollToRatio(ratio, immediate = false) {
|
|
|
|
|
+ applyScrollToRatio(ratio, immediate, ratioToIndex(ratio));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function onTimelineWheel(e) {
|
|
function onTimelineWheel(e) {
|
|
@@ -351,7 +547,11 @@ watch(
|
|
|
() => {
|
|
() => {
|
|
|
handleRatio.value = indexToRatio(activeIndex.value);
|
|
handleRatio.value = indexToRatio(activeIndex.value);
|
|
|
nextTick(() => {
|
|
nextTick(() => {
|
|
|
- scrollToCenterRatio(indexToRatio(activeIndex.value), "auto");
|
|
|
|
|
|
|
+ scrollToCenterRatio(
|
|
|
|
|
+ indexToRatio(activeIndex.value),
|
|
|
|
|
+ "smooth",
|
|
|
|
|
+ activeIndex.value
|
|
|
|
|
+ );
|
|
|
clampTooltipToScrollArea();
|
|
clampTooltipToScrollArea();
|
|
|
});
|
|
});
|
|
|
},
|
|
},
|
|
@@ -462,6 +662,7 @@ function onPointerUp(e) {
|
|
|
const nextIdx = ratioToIndex(handleRatio.value);
|
|
const nextIdx = ratioToIndex(handleRatio.value);
|
|
|
activeIndex.value = nextIdx;
|
|
activeIndex.value = nextIdx;
|
|
|
handleRatio.value = indexToRatio(nextIdx);
|
|
handleRatio.value = indexToRatio(nextIdx);
|
|
|
|
|
+ syncScrollToRatio(handleRatio.value);
|
|
|
if (movedDuringHandleDrag) {
|
|
if (movedDuringHandleDrag) {
|
|
|
showHandleTooltip.value = false;
|
|
showHandleTooltip.value = false;
|
|
|
}
|
|
}
|
|
@@ -548,7 +749,8 @@ onMounted(() => {
|
|
|
nextTick(() => {
|
|
nextTick(() => {
|
|
|
scrollToCenterRatio(
|
|
scrollToCenterRatio(
|
|
|
indexToRatio(activeIndex.value),
|
|
indexToRatio(activeIndex.value),
|
|
|
- "auto"
|
|
|
|
|
|
|
+ "auto",
|
|
|
|
|
+ activeIndex.value
|
|
|
);
|
|
);
|
|
|
clampTooltipToScrollArea();
|
|
clampTooltipToScrollArea();
|
|
|
});
|
|
});
|
|
@@ -572,6 +774,7 @@ onBeforeUnmount(() => {
|
|
|
el?.removeEventListener("touchmove", onScrollAreaTouchMove);
|
|
el?.removeEventListener("touchmove", onScrollAreaTouchMove);
|
|
|
resizeObserver?.disconnect();
|
|
resizeObserver?.disconnect();
|
|
|
resizeObserver = null;
|
|
resizeObserver = null;
|
|
|
|
|
+ cancelScrollAnimation();
|
|
|
clearScrollSettledTimers();
|
|
clearScrollSettledTimers();
|
|
|
pendingScrollSettled = false;
|
|
pendingScrollSettled = false;
|
|
|
});
|
|
});
|
|
@@ -585,7 +788,7 @@ onBeforeUnmount(() => {
|
|
|
|
|
|
|
|
.growth-stage-timeline__scroll {
|
|
.growth-stage-timeline__scroll {
|
|
|
overflow-x: hidden;
|
|
overflow-x: hidden;
|
|
|
- overflow-y: hidden;
|
|
|
|
|
|
|
+ overflow-y: visible;
|
|
|
touch-action: pan-y;
|
|
touch-action: pan-y;
|
|
|
overscroll-behavior-x: none;
|
|
overscroll-behavior-x: none;
|
|
|
-webkit-overflow-scrolling: touch;
|
|
-webkit-overflow-scrolling: touch;
|
|
@@ -595,59 +798,95 @@ onBeforeUnmount(() => {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
gap: 0;
|
|
gap: 0;
|
|
|
- padding-bottom: 4px;
|
|
|
|
|
|
|
+ padding-top: 36px;
|
|
|
|
|
+ padding-bottom: 8px;
|
|
|
|
|
+ width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__bg {
|
|
.growth-stage-timeline__bg {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
align-items: stretch;
|
|
align-items: stretch;
|
|
|
- border-radius: 6px 6px 0 0;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ gap: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__bg-cell {
|
|
.growth-stage-timeline__bg-cell {
|
|
|
- background: #f0f2f5;
|
|
|
|
|
- padding: 8px 10px;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ padding: 12px 8px;
|
|
|
|
|
+ text-align: left;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
- border-right: 1px solid rgba(0, 0, 0, 0.04);
|
|
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-top: 1px solid #d9d9d9;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
|
|
+ margin: 0 2px;
|
|
|
|
|
+ transition: background-color 0.2s, border-color 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &:first-child {
|
|
|
|
|
+ margin-left: 0;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
&:last-child {
|
|
&:last-child {
|
|
|
- border-right: none;
|
|
|
|
|
|
|
+ margin-right: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
&--period {
|
|
&--period {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ justify-content: flex-start;
|
|
|
|
|
+ gap: 3px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &--active {
|
|
|
|
|
+ background: #e6f4ff;
|
|
|
|
|
+ border-top-width: 3px;
|
|
|
|
|
+ border-top-color: #1890ff;
|
|
|
|
|
+
|
|
|
|
|
+ .growth-stage-timeline__period-title {
|
|
|
|
|
+ color: #1d2129;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .growth-stage-timeline__period-sub {
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:not(&--active) {
|
|
|
|
|
+ .growth-stage-timeline__period-title {
|
|
|
|
|
+ color: #8c8c8c;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .growth-stage-timeline__period-sub {
|
|
|
|
|
+ color: #bfbfbf;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__period-title {
|
|
.growth-stage-timeline__period-title {
|
|
|
- font-size: 14px;
|
|
|
|
|
|
|
+ font-size: 15px;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
color: #1d2129;
|
|
color: #1d2129;
|
|
|
- line-height: 1.2;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
|
|
+ text-align: left;
|
|
|
max-width: 100%;
|
|
max-width: 100%;
|
|
|
|
|
+ word-break: break-word;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__period-sub {
|
|
.growth-stage-timeline__period-sub {
|
|
|
- font-size: 11px;
|
|
|
|
|
- color: rgba(60, 60, 60, 0.45);
|
|
|
|
|
- line-height: 1.3;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ text-align: left;
|
|
|
max-width: 100%;
|
|
max-width: 100%;
|
|
|
word-break: break-word;
|
|
word-break: break-word;
|
|
|
|
|
+ white-space: normal;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__track {
|
|
.growth-stage-timeline__track {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
- min-height: 44px;
|
|
|
|
|
- margin-top: 4px;
|
|
|
|
|
|
|
+ min-height: 48px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__track-line {
|
|
.growth-stage-timeline__track-line {
|
|
@@ -655,9 +894,9 @@ onBeforeUnmount(() => {
|
|
|
left: 0;
|
|
left: 0;
|
|
|
right: 0;
|
|
right: 0;
|
|
|
top: 50%;
|
|
top: 50%;
|
|
|
- height: 2px;
|
|
|
|
|
- margin-top: -1px;
|
|
|
|
|
- background: #e5e6eb;
|
|
|
|
|
|
|
+ height: 1px;
|
|
|
|
|
+ margin-top: -0.5px;
|
|
|
|
|
+ background: #e8e8e8;
|
|
|
pointer-events: none;
|
|
pointer-events: none;
|
|
|
z-index: 0;
|
|
z-index: 0;
|
|
|
}
|
|
}
|
|
@@ -667,7 +906,8 @@ onBeforeUnmount(() => {
|
|
|
z-index: 1;
|
|
z-index: 1;
|
|
|
display: grid;
|
|
display: grid;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- min-height: 44px;
|
|
|
|
|
|
|
+ min-height: 48px;
|
|
|
|
|
+ width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__dot-wrap {
|
|
.growth-stage-timeline__dot-wrap {
|
|
@@ -679,19 +919,20 @@ onBeforeUnmount(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__dot {
|
|
.growth-stage-timeline__dot {
|
|
|
- width: 8px;
|
|
|
|
|
- height: 8px;
|
|
|
|
|
|
|
+ width: 10px;
|
|
|
|
|
+ height: 10px;
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
- background: #e5e6eb;
|
|
|
|
|
- border: 2px solid #fff;
|
|
|
|
|
|
|
+ background: #CDCDCD;
|
|
|
|
|
+ border: 1px solid #fff;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
|
|
+ box-shadow: 0px 2px 3px 0px #00000012;
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__handle {
|
|
.growth-stage-timeline__handle {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
top: 50%;
|
|
top: 50%;
|
|
|
z-index: 2;
|
|
z-index: 2;
|
|
|
- /* 旋转中心仅含按钮高度:tooltip 绝对叠在上方,避免参与 translate(-50%,-50%) 的包围盒 */
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
transform: translate(-50%, -50%);
|
|
|
touch-action: none;
|
|
touch-action: none;
|
|
|
cursor: grab;
|
|
cursor: grab;
|
|
@@ -716,22 +957,21 @@ onBeforeUnmount(() => {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- margin-bottom: 6px;
|
|
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
pointer-events: none;
|
|
pointer-events: none;
|
|
|
z-index: 3;
|
|
z-index: 3;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__tooltip {
|
|
.growth-stage-timeline__tooltip {
|
|
|
- max-width: min(220px, calc(100vw - 24px));
|
|
|
|
|
|
|
+ max-width: min(240px, calc(100vw - 24px));
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
- padding: 6px 10px;
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- background: #808080;
|
|
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ background: #8c8c8c;
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
|
line-height: 1.4;
|
|
line-height: 1.4;
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__tooltip-caret {
|
|
.growth-stage-timeline__tooltip-caret {
|
|
@@ -740,34 +980,42 @@ onBeforeUnmount(() => {
|
|
|
margin-top: -1px;
|
|
margin-top: -1px;
|
|
|
border-width: 5px 5px 0 5px;
|
|
border-width: 5px 5px 0 5px;
|
|
|
border-style: solid;
|
|
border-style: solid;
|
|
|
- border-color: #808080 transparent transparent transparent;
|
|
|
|
|
|
|
+ border-color: #8c8c8c transparent transparent transparent;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__handle-body {
|
|
.growth-stage-timeline__handle-body {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
|
|
|
|
+ flex-direction: row;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
- gap: 3px;
|
|
|
|
|
- width: 36px;
|
|
|
|
|
- height: 28px;
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- background: #2199f8;
|
|
|
|
|
- box-shadow: 0 2px 6px rgba(33, 153, 248, 0.35);
|
|
|
|
|
|
|
+ gap: 2px;
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 30px;
|
|
|
|
|
+ border-radius: 7px;
|
|
|
|
|
+ background: #1890ff;
|
|
|
|
|
+ box-shadow: 0 2px 6px rgba(24, 144, 255, 0.35);
|
|
|
|
|
+ transition: width 0.2s;
|
|
|
|
|
+
|
|
|
|
|
+ &--wide {
|
|
|
|
|
+ width: 48px;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__handle-bar {
|
|
.growth-stage-timeline__handle-bar {
|
|
|
display: block;
|
|
display: block;
|
|
|
- width: 14px;
|
|
|
|
|
- height: 2px;
|
|
|
|
|
|
|
+ width: 2px;
|
|
|
|
|
+ height: 10px;
|
|
|
border-radius: 1px;
|
|
border-radius: 1px;
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__labels {
|
|
.growth-stage-timeline__labels {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
- margin-top: 10px;
|
|
|
|
|
|
|
+ margin-top: 2px;
|
|
|
gap: 0;
|
|
gap: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__label-col {
|
|
.growth-stage-timeline__label-col {
|
|
@@ -775,18 +1023,28 @@ onBeforeUnmount(() => {
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
- padding: 0 4px 6px;
|
|
|
|
|
|
|
+ padding: 0 4px 4px;
|
|
|
box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+
|
|
|
|
|
+ &--wide {
|
|
|
|
|
+ .growth-stage-timeline__label-text {
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.growth-stage-timeline__label-text {
|
|
.growth-stage-timeline__label-text {
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #c0c4cc;
|
|
|
|
|
- line-height: 1.3;
|
|
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: #bfbfbf;
|
|
|
|
|
+ width: max-content;
|
|
|
word-break: break-all;
|
|
word-break: break-all;
|
|
|
|
|
+ transition: color 0.2s, font-size 0.2s;
|
|
|
|
|
|
|
|
&--active {
|
|
&--active {
|
|
|
- color: #2199f8;
|
|
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #1890ff;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -804,7 +1062,7 @@ onBeforeUnmount(() => {
|
|
|
display: inline-block;
|
|
display: inline-block;
|
|
|
max-width: 100%;
|
|
max-width: 100%;
|
|
|
padding: 2px 6px;
|
|
padding: 2px 6px;
|
|
|
- border-radius: 3px;
|
|
|
|
|
|
|
+ border-radius: 4px;
|
|
|
background: #00a870;
|
|
background: #00a870;
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
font-size: 10px;
|
|
font-size: 10px;
|