最近发现手机端访问首页时,标题「半知半解,且过且学!」的打字机特效走完之后,文字右侧被截断、显示不全,而桌面端一切正常。本文记录排查过程与修复方案。

问题描述

  • 桌面端:标题完整显示,打字机动画正常。
  • 手机端:动画结束后右侧文字被裁掉,最后一个字不完整。

原因分析

问题出在 assets/css/extended/custom.css 里打字机动画的宽度设计:

1
2
3
4
5
6
.profile .profile_inner h1 {
    max-width: 100%;      /* 手机窄屏下宽度被压到容器宽(约 292~347px) */
    width: 14em;          /* 动画终点目标宽度,14em ≈ 448px,超出容器 */
    overflow: hidden;     /* 超出的部分直接裁剪 */
    white-space: nowrap;  /* 禁止换行 */
}

标题「半知半解,且过且学!」是 10 个全角字符 ≈ 10em,但动画把宽度写死成了 14em(可能是按 14 个字符设计的)。三个属性叠加导致窄屏截断:

  • 桌面容器约 780px > 14em,文字完整;
  • 手机容器约 292~347px < 14em,max-width: 100% 把宽度压到容器值,右侧文字被 overflow: hidden 裁掉,而 nowrap 又禁止换行,于是文字就"丢"了。

注:这里和下面的数字都按出问题时的标题「半知半解,且过且学!」(10 字)记录。后来首页标题换成了「拾光入笔,岁月留痕」(9 字),数字换了但改法一致,见文末「后续改进」。

修复方案(保留手机端打字机效果)

将 custom.css 中 .profile .profile_inner h1 至 @media (prefers-reduced-motion...) 整段替换为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.profile .profile_inner h1 {
    max-width: 100%;
    width: 10em; /* 与标题字符数一致:"半知半解,且过且学!" = 10 字符 */
    overflow: hidden;
    white-space: nowrap;
    border-right: 1px solid currentColor;
    animation: profile-title-typing 2.4s steps(10, end), profile-title-caret 0.75s step-end infinite;
}

@keyframes profile-title-typing {
    from {
        width: 0;
    }

    to {
        width: 10em;
    }
}

@keyframes profile-title-caret {

    from,
    to {
        border-color: transparent;
    }

    50% {
        border-color: currentColor;
    }
}

/* 无障碍:系统开启"减少动态效果"时直接静态显示 */
@media (prefers-reduced-motion: reduce) {
    .profile .profile_inner h1 {
        animation: none;
        width: auto;
        max-width: 100%;
        white-space: normal;
        border-right: 0;
    }
}

/* 手机端:缩小字号,保证 10em 的动画宽度能放下 */
/* 320px 屏容器约 292px,1.75em×10 = 280px,完整放得下 */
@media (max-width: 480px) {
    .profile .profile_inner h1 {
        font-size: 1.75em;
    }
}

改动对照表

项原来现在原因
width14em10em精确等于文字宽度,不再白白占 4em
steps()steps(14, end)steps(10, end)步数 = 字符数,每步显示一个字
动画时长3.2s2.4s保持每字约 0.24s 的节奏
手机端无font-size: 1.75emem 随字号缩放,10em 同步变小,窄屏放得下

原理:width: 10em 与 keyframes 里的 10em 都基于元素自身字号,媒体查询里只改 font-size,动画终点宽度等比缩放,打字机效果在手机上依然保留且完整显示。

备选方案:手机端直接关闭动画

若认为手机端不需要打字机效果,仅追加以下媒体查询即可(标题自动换行、永不被裁):

1
2
3
4
5
6
7
8
@media (max-width: 768px) {
    .profile .profile_inner h1 {
        width: auto;
        white-space: normal;
        border-right: 0;
        animation: none;
    }
}

后续改进:字符数改为自动生成

上面的方案修好了截断,但留了个隐患:width 和 steps() 里的数字必须和标题字数保持一致,标题一改就得回来手动同步,很容易忘。

后来改成由模板按标题实际字数注入 CSS 变量,custom.css 里只留兜底值。

layouts/partials/extend_head.html 在首页注入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{{- /* 首页标题打字机:按标题实际字数生成动画参数,
   避免与 custom.css 里的兜底值不同步(1 个全角字按 1em 计)。 */ -}}
{{- if .IsHome }}
{{- with site.Params.profileMode }}
{{- if .enabled }}
{{- $chars := strings.RuneCount (.title | default site.Title) }}
<style>
	.profile .profile_inner h1 {
		--type-chars: {{ $chars }};
		--type-ease: steps({{ $chars }}, end);
	}
</style>
{{- end }}
{{- end }}
{{- end }}

custom.css 里改成全部走变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 首页标题打字机
   --type-chars / --type-ease 由 extend_head.html 按 profileMode.title 的字数注入,
   下面两行只是兜底值,改标题后不需要再手改这里。 */
.profile .profile_inner h1 {
    --type-chars: 9;
    --type-ease: steps(9, end);
    max-width: 100%;
    width: calc(var(--type-chars) * 1em);
    overflow: hidden;
    white-space: nowrap;
    border-right: 1px solid currentColor;
    animation: profile-title-typing calc(var(--type-chars) * 0.24s) var(--type-ease),
        profile-title-caret 0.75s step-end infinite;
}

@keyframes profile-title-typing {
    from {
        width: 0;
    }

    to {
        width: calc(var(--type-chars) * 1em);
    }
}

几个要点:

  • strings.RuneCount 数的是字符数而不是字节数,中文标题一个字算 1;
  • 全角字宽约等于 1em,所以 --type-chars 可以直接当 em 用:width: calc(var(--type-chars) * 1em);
  • steps() 同样能吃变量(--type-ease: steps({{ $chars }}, end)),动画时长也按字数算(calc(var(--type-chars) * 0.24s)),维持每字约 0.24s 的节奏;
  • 模板只在首页、且 profileMode.enabled 时输出这段 <style>,其它页面不会带上无用样式;
  • custom.css 里那两行兜底值保留的意义是:万一模板没注入,动画仍然能跑,不至于整个特效消失。

于是「改标题要同步改数字」这件事就不存在了。

验证方法

  1. 浏览器 F12 → 设备模拟(手机图标);
  2. 选择 iPhone 尺寸(375px)或更窄的 320px 视口;
  3. 刷新首页,等待打字机动画结束,确认标题完整显示;
  4. 桌面端确认打字机效果无回归。

注意事项

  • 标题文字修改后,需同步更新 10em 和 steps(10, end) 中的数字为新字符数。这一条已废弃:字符数现在由 extend_head.html 自动注入(见「后续改进」),改标题不用再动 CSS;
  • 若自定义了 --main-width 或页面内边距,仍要重新核算窄屏容器是否放得下标题(标题宽度 ≈ 字数 × font-size;本站标题 9 字,480px 以下 font-size: 1.75em)。