下面整段可直接复制进笔记。
```markdown
Swiper 卡片轮播丝滑优化笔记
> 场景:居中大卡 + 两侧倾斜下移(`recommended-area-mclaw.vue`)
> 目标:① 横向位移匀速 ② `translateY` / `rotate` 与横滑同步,不抖
1. 问题现象
-
松手 / 自动切页时,**横向滑动有动画**,但卡片 **上下位移、旋转瞬间到位**,感觉「先跳一下再滑」。
-
Swiper 默认缓动是 **ease-out**(先快后慢),不是匀速。
2. 根因(很关键)
Swiper 翻页时大致是:
-
`setTransition(duration)` → 给 **wrapper** 设 CSS transition 时长
-
`setTranslate(最终位置)` → **立刻把 progress 设成终点值**,wrapper 靠 CSS 在 `speed` ms 内滑过去
如果我们在 `progress` / `setTranslate` 里根据 `slide.progress` 改卡片 `transform`,但卡片自己是 `transition: none` / `0ms`:
| 层 | 行为 |
|----|------|
| `.swiper-wrapper` | 300ms 横移 |
| `.rec-card` transform | **瞬间**到目标 Y / 旋转 |
两条时间线不同步 → 抖动 / 卡顿感。
`transitionEnd` 再 `snap` 吸附,还可能末尾再跳一下。
3. 我们做了什么
① 横向匀速(CSS)
```css
.swiper-wrapper {
transition-timing-function: linear !important;
}
```
-
`speed` 只控制**多久滑完**
-
`timing-function` 控制**速度曲线**(linear = 匀速)
② 卡片 transform 跟 Swiper 同时长(JS)
```js
function syncCardTransition(swiper, duration) {
const ms = `${duration || 0}ms`;
swiper.slides.forEach((slide) => {
const card = slide.querySelector(".rec-card");
if (card) card.style.transitionDuration = ms;
});
}
// Swiper 事件
on: {
setTransition(s, duration) {
syncCardTransition(s, duration); // 翻页:300ms;跟手:0ms
},
transitionEnd(s) {
syncCardTransition(s, 0); // 收尾归零,下次拖动不拖泥带水
applySlideEffect(s, false); // 不再 snap,避免末尾再跳
},
}
```
③ 卡片 CSS 允许 transform 过渡
```css
/* 之前:transition: none; → 改 duration 也开不起过渡 */
.rec-card {
transition: transform 0ms linear;
}
```
跟手时 duration=0 → 贴手;翻页时 duration=speed → 和 wrapper 一起动。
4. 关键配置 / 参数说明
Swiper 初始化
| 参数 | 示例值 | 作用 |
|------|--------|------|
| `slidesPerView` | `"auto"` | 每页可见宽度由 slide CSS 决定 |
| `centeredSlides` | `true` | 当前卡居中 |
| `loop` | `length > 2` | >2 张才循环;=2 关闭避免首尾怪 |
| `loopedSlides` | `max(len, 3)` | loop 时预克隆数量 |
| `watchSlidesProgress` | `true` | **必须**,才有 `slide.progress` |
| `spaceBetween` | 设计稿 24→实际 px | 卡片间距 |
| `speed` | `300` | 翻页动画时长(ms) |
| `grabCursor` | `true` | 鼠标手型 |
| `initialSlide` | 记忆下标 | 切分类回来还原位置 |
动效常量
| 常量 | 值 | 作用 |
|------|-----|------|
| `ROTATE_SIDE` | `5` | 两侧最大旋转角(deg),左正右负 |
| `DESIGN_TRANSLATE_Y` | `14` | 设计稿两侧下移 px |
| `DESIGN_SPACE_BETWEEN` | `24` | 设计稿卡片间距 |
| `ROTATE_EPS` / `TRANSLATE_Y_EPS` | `0.3` / `0.5` | snap 吸附阈值(现主要 init 用) |
`slide.progress`
-
中心 ≈ `0`
-
左侧负、右侧正
-
`|progress|` 越大离中心越远
-
常用:`t = Math.min(Math.abs(p), 1)`,再 `translateY = sideY * t`,`rotate = ±ROTATE_SIDE * t`
事件分工
| 事件 | 做什么 |
|------|--------|
| `init` | duration=0 + 初始 effect(可 snap) |
| `progress` / `setTranslate` | 按 progress 写 transform |
| `setTransition` | **同步卡片 transitionDuration** |
| `transitionEnd` | duration 归零,不 snap |
| `slideChange` | 记当前 realIndex |
5. 可复制 Demo(单文件 HTML,浏览器打开即跑)
把下面存成 `swiper-smooth-demo.html`,用浏览器打开,左右拖或点按钮即可对比。
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Swiper 匀速 + transform 同步 Demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@8/swiper-bundle.min.css" />
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: #f0f2f8;
font-family: system-ui, sans-serif;
padding: 24px 0 48px;
}
h1 { text-align: center; font-size: 16px; color: #122133; }
.hint { text-align: center; font-size: 13px; color: #666; margin-bottom: 16px; }
.btns { text-align: center; margin-bottom: 20px; }
button {
margin: 0 6px; padding: 8px 14px; border: none; border-radius: 8px;
background: #736eff; color: #fff; cursor: pointer;
}
button.ghost { background: #fff; color: #736eff; border: 1px solid #736eff; }
.swiper {
width: 100%;
padding: 24px 0 40px;
overflow: visible;
}
.swiper-wrapper {
/* ① 横向匀速(默认是 ease) */
transition-timing-function: linear !important;
}
.swiper-slide {
width: 180px;
height: 220px;
}
.card {
width: 100%;
height: 100%;
border-radius: 16px;
background: linear-gradient(145deg, #fff, #e8eaff);
box-shadow: 0 4px 16px rgba(18, 33, 51, 0.12);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 700;
color: #534dff;
/* ② 允许 transform 过渡;时长由 JS 写入 */
transition: transform 0ms linear;
will-change: transform;
}
</style>
</head>
<body>
<h1>Swiper:横滑匀速 + Y/旋转同步</h1>
<p class="hint">拖动跟手;点「下一张」看翻页是否丝滑(可点「对比:不同步」还原旧问题)</p>
<div class="btns">
<button id="prev">上一张</button>
<button id="next">下一张</button>
<button class="ghost" id="toggle">对比:不同步</button>
</div>
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide"><div class="card">1</div></div>
<div class="swiper-slide"><div class="card">2</div></div>
<div class="swiper-slide"><div class="card">3</div></div>
<div class="swiper-slide"><div class="card">4</div></div>
<div class="swiper-slide"><div class="card">5</div></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/swiper@8/swiper-bundle.min.js"></script>
<script>
const ROTATE_SIDE = 5;
const TRANSLATE_Y = 14;
const SPEED = 300;
let syncEnabled = true; // true = 我们的优化;false = 旧问题
function syncCardTransition(swiper, duration) {
if (!syncEnabled) duration = 0;
const ms = (duration || 0) + "ms";
swiper.slides.forEach((slide) => {
const card = slide.querySelector(".card");
if (card) card.style.transitionDuration = ms;
});
}
function applySlideEffect(swiper) {
swiper.slides.forEach((slide) => {
const card = slide.querySelector(".card");
if (!card) return;
const p = slide.progress || 0;
const t = Math.min(Math.abs(p), 1);
const translateY = TRANSLATE_Y * t;
const rotate = (p < 0 ? ROTATE_SIDE : -ROTATE_SIDE) * t;
// 旧写法还会强制 0ms:card.style.transitionDuration = "0ms"
if (!syncEnabled) card.style.transitionDuration = "0ms";
card.style.transform = `rotate({rotate}deg) translate3d(0, {translateY}px, 0)`;
});
}
const swiper = new Swiper(".swiper", {
slidesPerView: "auto",
centeredSlides: true,
spaceBetween: 24,
loop: true,
loopedSlides: 5,
watchSlidesProgress: true,
speed: SPEED,
grabCursor: true,
on: {
init(s) {
syncCardTransition(s, 0);
applySlideEffect(s);
},
progress(s) {
applySlideEffect(s);
},
setTranslate(s) {
applySlideEffect(s);
},
setTransition(s, duration) {
syncCardTransition(s, duration);
},
transitionEnd(s) {
syncCardTransition(s, 0);
applySlideEffect(s);
},
},
});
document.getElementById("prev").onclick = () => swiper.slidePrev();
document.getElementById("next").onclick = () => swiper.slideNext();
document.getElementById("toggle").onclick = (e) => {
syncEnabled = !syncEnabled;
e.target.textContent = syncEnabled ? "对比:不同步" : "已开同步(再点关)";
alert(syncEnabled
? "已开启:卡片 Y/旋转与横滑同时长"
: "已关闭:模拟旧问题(transform 瞬间跳)");
};
</script>
</body>
</html>