mcpField
G
gsap-master

gsap-master

github.com/su69ar设计与 UI2259 次安装40 周前更新
代码仓库

关于 gsap-master

面向 GSAP v3 动画实现的技能,供制作交互界面动效的开发者参考。内容涵盖时间线组织、滚动与手势交互、文本和 SVG 动画,以及性能、无障碍和框架清理实践。

主要功能

  1. 01组织 Tween 与 Timeline
  2. 02实现滚动触发动画
  3. 03处理拖拽与手势交互
  4. 04制作文本和 SVG 动画
  5. 05考虑性能与减少动态效果偏好

适用场景

  1. 01构建页面滚动叙事
  2. 02制作拖拽式轮播或滑块
  3. 03添加分步文本揭示效果
  4. 04在 React 或 Next.js 中管理动画清理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/su69ar/rdnax-gsap-master --skill gsap-master

安装后在 Claude Code 中输入/gsap-master确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
references/
DEBUG_CHECKLIST.md
GSAP_CORE.md
GSAP_RECIPES.md
GSDEVTOOLS.md
NEXTJS_REACT_PATTERNS.md
PHYSICS_PLUGINS.md
TEXT_ANIMATIONS.md
UI_INTERACTIONS.md
scripts/
make-gsap-starter.sh

详细介绍

GSAP Master Skill(Antigravity)

适用范围与基本规则

你是一名符合生产要求的 GSAP v3 工程师。你交付的交互式 UI 动效具备以下特点:

  • 性能(避免布局抖动,保持 60 fps,优化指针交互)
  • 可维护性(时间轴架构、模块化函数、清晰的标签)
  • 无障碍性(prefers-reduced-motion、易于理解的动效、清晰可见的焦点)
  • 框架安全性(React/Next.js 清理,避免重新渲染时重复创建触发器)

GSAP Core 处理 Tween/Timeline 和实用工具/工具。插件用于扩展功能。

许可说明:GSAP 表示,现在整个库都可免费使用(历史上部分插件仅供 Club 成员使用)。 切勿推荐盗版或破解插件。


1) 激活触发条件

用户提及以下内容时自动激活:

GSAP、Tween、Timeline、缓动、错开、关键帧、修饰器、ScrollTrigger、pin/scrub/snap、 ScrollSmoother、ScrollTo、SplitText、ScrambleText、TextPlugin、Flip、Draggable、Inertia、Observer、 MotionPath、MorphSVG、DrawSVG、物理效果、光标跟随、悬停微交互、 Next.js/React 清理、SSR、性能/卡顿、磁吸按钮、3D 倾斜、卡片堆叠、 滑动卡片、加入购物车、彩纸特效、加载骨架屏、工具提示、上下文菜单、 UI 交互、微交互、手势、下拉刷新、轮播图吸附、 文本动画、字符动画、单词动画、逐行显示、打字机效果、 乱码文本、打字效果、文本拆分、错开文本、文本遮罩显示、 gsap core、时间轴控制、实用工具、quickTo、quickSetter、matchMedia、 gsap.context、gsap.effects、ticker、GSDevTools、调试、动画检查器、 Physics2D、PhysicsProps、速度、重力、加速度、摩擦力、粒子。


2) 输出约定(必须交付的内容)

用户要求实现动画时,输出:

  1. 动效方案(目标、触发条件、状态、时长、缓动)
  2. 实现(先提供最精简的可运行代码)
  3. 架构(时间轴映射、标签、可复用函数/模块)
  4. 响应式与无障碍性(matchMedia + 减少动态效果时的备用方案)
  5. 性能说明(quickTo/quickSetter、避免布局抖动、批处理)
  6. 调试清单(标记/刷新/清理/重复创建)

优先提供一个“良好的 MVP”,然后再增强。


3) 插件覆盖范围图(完整)

滚动插件

插件描述使用场景
ScrollTrigger在滚动时触发/擦动/固定/吸附动画大多数滚动动画
ScrollTo以编程方式平滑滚动导航、CTA 按钮
ScrollSmoother基于原生滚动的平滑滚动 + 视差效果丝滑的滚动体验
Observer统一检测滚轮/触摸/指针手势滚动劫持、滑动手势

UI/交互

插件描述使用场景
Flip基于 FLIP 的布局过渡网格重排、模态框扩展、共享元素
Draggable拖拽交互轮播图、滑块、卡片
InertiaPlugin动量/速度滑行拖拽后的惯性物理效果

文本

插件描述使用场景
SplitText拆分字符/单词/行以制作动画错开显示文本
ScrambleText随机文本解码效果科技感标题
TextPlugin输入/替换文本内容计数器、动态标签

SVG

插件描述使用场景
DrawSVG绘制描边动画线稿、签名
MorphSVG在 SVG 路径之间变形形状过渡
MotionPath沿 SVG 路径制作动画沿曲线运动
MotionPathHelper可视化编辑路径(开发工具)开发工作流

物理 / 缓动 / 工具

插件描述使用场景
Physics2D2D 物理模拟弹道运动、粒子
PhysicsProps为任意属性添加物理效果自然的属性动画
CustomEase定义自定义缓动曲线打造独特的动效风格
CustomWiggle振荡 / 摇摆缓动抖动、振动效果
CustomBounce自定义弹跳缓动逼真的弹跳
EasePack额外的内置缓动扩展缓动库
GSDevTools可视化时间轴调试界面开发工作流

渲染库

插件描述使用场景
PixiPlugin为 Pixi.js 对象添加动画Canvas/WebGL 渲染
EaselPlugin为 EaselJS 对象添加动画CreateJS canvas

React 集成

助手描述使用场景
useGSAP()官方 React HookReact/Next.js 应用

4) GSAP 核心精通(始终适用的基础知识)

4.1 基本构件

gsap.to(target, { x: 100, duration: 1 });      // animate TO values
gsap.from(target, { opacity: 0 });              // animate FROM values
gsap.fromTo(target, { x: 0 }, { x: 100 });     // explicit from→to
gsap.set(target, { x: 0, opacity: 1 });        // instant set (no animation)

优先使用变换属性(x、y、scale、rotation),而不是布局属性(top、left、width、height)。

4.2 时间轴(任何不简单的动画都默认使用)

const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 0.6 } });
tl.from(".hero-title", { y: 30, autoAlpha: 0 })
  .from(".hero-subtitle", { y: 20, autoAlpha: 0 }, "<0.1")
  .from(".hero-cta", { scale: 0.9, autoAlpha: 0 }, "<0.15");

规则:

  • 每个“交互单元”(首屏 / 区块 / 组件)使用一个时间轴
  • 使用标签和位置参数,而不是脆弱的延迟
  • 使用 defaults 保持一致,并有意覆盖默认值

4.3 位置参数

tl.to(a, {...})           // appends at end
tl.to(b, {...}, "<")      // starts same time as previous
tl.to(c, {...}, ">")      // starts after previous ends
tl.to(d, {...}, "+=0.3")  // wait 0.3s after last end
tl.to(e, {...}, "label")  // starts at label

4.4 关键工具

  • keyframes:在单个补间动画中实现多步骤动画
  • modifiers:逐帧变换数值(高级)
  • snapping:使用 snap 工具实现网格对齐
  • utils:gsap.utils.mapRange()、clamp()、snap()、toArray()
  • matchMedia:响应式动画编排
  • context:为框架提供作用域清理

5) 性能工具包(不可妥协)

5.1 指针 / 鼠标交互

// ❌ BAD: Creates new tween every event
window.addEventListener("pointermove", (e) => {
  gsap.to(".cursor", { x: e.clientX, y: e.clientY });
});

// ✅ GOOD: Reuses quickTo instances
const xTo = gsap.quickTo(".cursor", "x", { duration: 0.2, ease: "power3" });
const yTo = gsap.quickTo(".cursor", "y", { duration: 0.2, ease: "power3" });
window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY); });

// ✅ GOOD: Ultra-fast direct updates (no tween)
const setX = gsap.quickSetter(".cursor", "x", "px");
const setY = gsap.quickSetter(".cursor", "y", "px");
window.addEventListener("pointermove", (e) => { setX(e.clientX); setY(e.clientY); });

5.2 避免布局抖动

// ❌ BAD: Mixed reads and writes
elements.forEach(el => {
  const rect = el.getBoundingClientRect(); // read
  gsap.set(el, { x: rect.width });         // write
});

// ✅ GOOD: Batch reads, then writes
const rects = elements.map(el => el.getBoundingClientRect()); // all reads
elements.forEach((el, i) => gsap.set(el, { x: rects[i].width })); // all writes

5.3 ScrollTrigger 使用规模规则

  • 减少触发器数量;尽可能批量显示元素
  • 使用由布局驱动的数值时,设置 invalidateOnRefresh
  • 对相似元素列表使用 ScrollTrigger.batch()

6) 精通 ScrollTrigger(核心滚动动画)

ScrollTrigger 支持基于滚动触发、滚动同步、固定、吸附等功能。

6.1 基本模式

// Pattern A: Inline scrollTrigger
gsap.to(".box", {
  x: 500,
  scrollTrigger: {
    trigger: ".box",
    start: "top 80%",
    end: "top 30%",
    scrub: 1,
    markers: true // dev only
  }
});

// Pattern B: Timeline + ScrollTrigger.create
const tl = gsap.timeline();
tl.from(".item", { y: 50, autoAlpha: 0, stagger: 0.1 });

ScrollTrigger.create({
  animation: tl,
  trigger: ".section",
  start: "top 70%",
  toggleActions: "play none none reverse"
});

6.2 固定式叙事(时间轴 + 滚动同步)

const tl = gsap.timeline();
tl.from(".panel1", { autoAlpha: 0, y: 20 })
  .to(".panel1", { autoAlpha: 0 })
  .from(".panel2", { autoAlpha: 0, y: 20 }, "<");

ScrollTrigger.create({
  animation: tl,
  trigger: ".story",
  start: "top top",
  end: "+=2000",
  scrub: 1,
  pin: true,
  invalidateOnRefresh: true
});

6.3 列表批处理

ScrollTrigger.batch(".card", {
  onEnter: (elements) => gsap.from(elements, { y: 30, autoAlpha: 0, stagger: 0.1 }),
  start: "top 85%"
});

6.4 水平滚动区块

const sections = gsap.utils.toArray(".panel");
gsap.to(sections, {
  xPercent: -100 * (sections.length - 1),
  ease: "none",
  scrollTrigger: {
    trigger: ".horizontal-container",
    pin: true,
    scrub: 1,
    end: () => "+=" + document.querySelector(".horizontal-container").offsetWidth
  }
});

6.5 响应式 + 减少动态效果(必需)

gsap.matchMedia().add({
  "(prefers-reduced-motion: no-preference) and (min-width: 768px)": () => {
    // Desktop animations
    gsap.from(".hero", {
      y: 50, autoAlpha: 0,
      scrollTrigger: { trigger: ".hero", start: "top 80%" }
    });
  },
  "(prefers-reduced-motion: reduce)": () => {
    // Reduced motion: instant visibility, no animation
    gsap.set(".hero", { autoAlpha: 1 });
  }
});

7) ScrollTo(程序化滚动)

使用 ScrollTo 平滑导航到锚点 / 区块。

gsap.registerPlugin(ScrollToPlugin);

// Scroll to element
gsap.to(window, { duration: 1, scrollTo: "#pricing", ease: "power2.out" });

// Scroll to position
gsap.to(window, { duration: 1, scrollTo: { y: 500, autoKill: true } });

// With offset
gsap.to(window, { scrollTo: { y: "#section", offsetY: 80 } });

8) ScrollSmoother(平滑滚动 + 特效)

ScrollSmoother 基于原生滚动实现平滑和视差效果。

gsap.registerPlugin(ScrollTrigger, ScrollSmoother);

// Required HTML structure:
// <div id="smooth-wrapper">
//   <div id="smooth-content">...content...</div>
// </div>

const smoother = ScrollSmoother.create({
  wrapper: "#smooth-wrapper",
  content: "#smooth-content",
  smooth: 1.5,        // seconds for smoothing
  effects: true       // enable data-speed/data-lag attributes
});

// HTML: <div data-speed="0.5">Slow parallax</div>
//       <div data-speed="2">Fast parallax</div>
//       <div data-lag="0.5">Trailing effect</div>

注意事项:

  • 必须与 ScrollTrigger 协调,并在布局变化后调用 refresh()
  • 始终提供减少动态效果的备用方案
  • 使用 normalizeScroll: true 确保移动设备上的一致性

9) Observer(手势统一处理)

使用 Observer 将滚轮 / 触摸 / 指针事件统一为一致的“意图”信号。

gsap.registerPlugin(Observer);

Observer.create({
  target: window,
  type: "wheel,touch,pointer",
  onUp: () => goToPrevSection(),
  onDown: () => goToNextSection(),
  tolerance: 50,
  preventDefault: true
});

使用场景:

  • 类似滚动劫持的分步区块(注意无障碍支持)
  • 由触控板手势触发的时间轴
  • 自定义滑动导航

10) Flip(布局过渡)

当元素布局发生变化时(网格重排、模态框展开、共享元素过渡),使用 Flip。

gsap.registerPlugin(Flip);

// 1. Capture state
const state = Flip.getState(".cards");

// 2. Change layout (DOM/CSS)
container.classList.toggle("grid");

// 3. Animate from old state
Flip.from(state, {
  duration: 0.6,
  ease: "power2.inOut",
  stagger: 0.05,
  absolute: true,
  onEnter: elements => gsap.fromTo(elements, { autoAlpha: 0 }, { autoAlpha: 1 }),
  onLeave: elements => gsap.to(elements, { autoAlpha: 0 })
});

11) Draggable + InertiaPlugin(惯性拖动)

gsap.registerPlugin(Draggable, InertiaPlugin);

Draggable.create(".slider", {
  type: "x",
  bounds: ".container",
  inertia: true,           // requires InertiaPlugin
  snap: value => Math.round(value / 200) * 200,  // snap to 200px intervals
  onDrag: function() { console.log(this.x); },
  onThrowComplete: () => console.log("Throw complete")
});

自制惯性(如果 InertiaPlugin 不可用):

let velocity = 0;
let lastX = 0;

Draggable.create(".element", {
  type: "x",
  onDrag: function() {
    velocity = this.x - lastX;
    lastX = this.x;
  },
  onDragEnd: function() {
    // DIY momentum with exponential decay
    gsap.to(this.target, {
      x: `+=${velocity * 10}`,
      duration: Math.abs(velocity) * 0.1,
      ease: "power2.out"
    });
  }
});

12) 文本:SplitText / ScrambleText / TextPlugin

12.1 SplitText

gsap.registerPlugin(SplitText);

const split = new SplitText(".headline", { type: "chars,words,lines" });

gsap.from(split.chars, {
  y: 20,
  autoAlpha: 0,
  stagger: 0.02,
  duration: 0.4,
  ease: "power3.out"
});

// Cleanup (important for responsive)
// split.revert();

自制替代方案(如有需要):

function splitIntoSpans(el) {
  const text = el.textContent;
  el.innerHTML = text.split('').map(char =>
    char === ' ' ? ' ' : `<span class="char">${char}</span>`
  ).join('');
  return el.querySelectorAll('.char');
}

12.2 ScrambleText

gsap.registerPlugin(ScrambleTextPlugin);

gsap.to(".title", {
  duration: 1.5,
  scrambleText: {
    text: "DECODED MESSAGE",
    chars: "XO",
    revealDelay: 0.5
  }
});

12.3 TextPlugin

gsap.registerPlugin(TextPlugin);

gsap.to(".counter", {
  duration: 2,
  text: { value: "100%", delimiter: "" },
  ease: "none"
});

13) SVG:DrawSVG / MorphSVG / MotionPath

13.1 DrawSVG

gsap.registerPlugin(DrawSVGPlugin);

gsap.from(".line-path", {
  drawSVG: 0,          // or "0%" / "50% 50%"
  duration: 2,
  ease: "power2.inOut"
});

13.2 MorphSVG

gsap.registerPlugin(MorphSVGPlugin);

gsap.to("#shape1", {
  morphSVG: "#shape2",
  duration: 1,
  ease: "power2.inOut"
});

13.3 MotionPath

gsap.registerPlugin(MotionPathPlugin);

gsap.to(".element", {
  duration: 5,
  motionPath: {
    path: "#curve-path",
    align: "#curve-path",
    alignOrigin: [0.5, 0.5],
    autoRotate: true
  },
  ease: "none"
});

14) Physics2D / PhysicsProps

gsap.registerPlugin(Physics2DPlugin, PhysicsPropsPlugin);

// 2D physics (velocity, angle, acceleration, gravity)
gsap.to(".ball", {
  duration: 3,
  physics2D: {
    velocity: 500,
    angle: -60,
    gravity: 500
  }
});

// Physics for any property
gsap.to(".element", {
  duration: 2,
  physicsProps: {
    x: { velocity: 100, acceleration: 50 },
    rotation: { velocity: 360 }
  }
});

15) 缓动:CustomEase / CustomWiggle / CustomBounce

gsap.registerPlugin(CustomEase, CustomWiggle, CustomBounce);

// Custom SVG path ease
CustomEase.create("myEase", "M0,0 C0.25,0.1 0.25,1 1,1");
gsap.to(".el", { x: 100, ease: "myEase" });

// Wiggle ease
CustomWiggle.create("myWiggle", { wiggles: 6, type: "easeOut" });
gsap.to(".el", { rotation: 20, ease: "myWiggle" });

// Custom bounce
CustomBounce.create("myBounce", { strength: 0.7, squash: 2 });
gsap.to(".el", { y: 300, ease: "myBounce" });

16) GSDevTools(调试 UI)

gsap.registerPlugin(GSDevTools);

// Attach to a specific timeline
const tl = gsap.timeline();
tl.to(".box", { x: 500 })
  .to(".box", { rotation: 360 });

GSDevTools.create({ animation: tl });

// Or attach to global timeline
GSDevTools.create();

17) Next.js / React(生产环境模式)

17.1 黄金法则:卸载时清理

使用 gsap.context() 限定选择器的作用范围,并还原所有更改。

"use client";
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

export function SectionAnim() {
  const root = useRef(null);

  useLayoutEffect(() => {
    if (!root.current) return;

    const ctx = gsap.context(() => {
      const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 0.8 } });
      tl.from(".title", { y: 18, autoAlpha: 0 })
        .from(".item", { y: 14, autoAlpha: 0, stagger: 0.06 }, "<0.1");

      gsap.from(".reveal", {
        y: 24, autoAlpha: 0,
        scrollTrigger: { trigger: ".reveal", start: "top 85%" }
      });
    }, root);

    return () => ctx.revert();
  }, []);

  return (
    <section ref={root}>
      <h2 className="title">Title</h2>
      <div className="item">A</div>
      <div className="item">B</div>
      <div className="reveal">Reveal</div>
    </section>
  );
}

17.2 官方 useGSAP hook

"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import ScrollTrigger from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

export function AnimatedSection() {
  const container = useRef(null);

  useGSAP(() => {
    gsap.from(".box", {
      y: 50,
      autoAlpha: 0,
      scrollTrigger: { trigger: ".box", start: "top 80%" }
    });
  }, { scope: container });

  return (
    <div ref={container}>
      <div className="box">Content</div>
    </div>
  );
}

17.3 SSR 安全性

  • 将动画组件放在仅客户端文件中
  • 不要在副作用之外访问 window/document
  • 在需要时使用 typeof window !== 'undefined' 保护条件

17.4 响应式编排

useLayoutEffect(() => {
  const mm = gsap.matchMedia();

  mm.add("(min-width: 768px)", () => {
    // Desktop animations
    gsap.from(".hero", { x: -100 });
    return () => { /* cleanup */ };
  });

  mm.add("(max-width: 767px)", () => {
    // Mobile animations
    gsap.from(".hero", { y: 50 });
    return () => { /* cleanup */ };
  });

  return () => mm.revert();
}, []);

18) 调试检查清单(照原文使用)

核心问题

  • 目标是否真实存在? 仔细检查选择器/ref(拼写错误、作用域、SSR)。确认动画运行时元素已存在。
  • DOM 时序:在 React/Next.js 中,确保代码在挂载后运行(客户端组件、useLayoutEffect、refs 不为 null)。
  • CSS 冲突:检查是否有覆盖 transform/opacity/visibility/display/transition 的规则。
  • 重复运行:使用 gsap.context() 和 ctx.revert(),避免叠加补间/ScrollTrigger。

时间轴问题

  • 序列看起来不对? 使用标签和位置参数(<、>、+=0.2),而不是手动延迟。
  • 默认值与补间变量:确保时间轴默认值没有意外覆盖补间。
  • 放慢速度以便检查:gsap.globalTimeline.timeScale(0.5)。

ScrollTrigger 问题

  • 启用调试:markers: true(仅开发环境)。
  • 验证起始/结束位置:确保起始/结束位置与布局和元素高度相符。
  • 布局变化后刷新:图片/字体/动态内容稳定后调用 ScrollTrigger.refresh()。
  • 自定义滚动容器:不使用 window 时,统一设置 scroller。
  • 固定时跳动/抖动:尝试 anticipatePin,检查布局偏移,并考虑 invalidateOnRefresh。

性能问题

  • 指针/鼠标卡顿:不要为每个事件都创建补间。使用 quickTo/quickSetter。
  • 触发器过多:减少数量,使用批处理,并整合时间轴。
  • 避免布局抖动:先批量读取,再批量写入;优先使用变换。
  • 开销较大的属性:尽可能避免使用 top/left/width/height。

19)“插件不可用”策略

如果用户无法使用某个插件(受政策/离线条件限制):

  1. 首先提供官方插件方案
  2. 使用 GSAP 核心功能 + CSS/JS 提供 DIY 替代方案
  3. 明确说明限制:边缘情况、可维护性、性能、跨浏览器一致性

切勿推荐破解/盗版插件。


20) 即用型交互方案

悬停微交互

const el = document.querySelector(".btn");
el.addEventListener("mouseenter", () =>
  gsap.to(el, { scale: 1.03, duration: 0.2, overwrite: "auto" })
);
el.addEventListener("mouseleave", () =>
  gsap.to(el, { scale: 1.00, duration: 0.25, overwrite: "auto" })
);

光标跟随(高性能)

const xTo = gsap.quickTo(".cursor", "x", { duration: 0.2, ease: "power3" });
const yTo = gsap.quickTo(".cursor", "y", { duration: 0.2, ease: "power3" });
window.addEventListener("pointermove", (e) => { xTo(e.clientX); yTo(e.clientY); });

磁性按钮

const btn = document.querySelector(".magnetic");
const strength = 0.3;

btn.addEventListener("mousemove", (e) => {
  const rect = btn.getBoundingClientRect();
  const x = e.clientX - rect.left - rect.width / 2;
  const y = e.clientY - rect.top - rect.height / 2;
  gsap.to(btn, { x: x * strength, y: y * strength, duration: 0.3 });
});

btn.addEventListener("mouseleave", () => {
  gsap.to(btn, { x: 0, y: 0, duration: 0.5, ease: "elastic.out(1, 0.3)" });
});

滚动时显示

gsap.from(".reveal", {
  y: 24, autoAlpha: 0, duration: 0.8, ease: "power3.out",
  scrollTrigger: {
    trigger: ".reveal",
    start: "top 85%",
    toggleActions: "play none none reverse"
  }
});

列表交错显示

gsap.from(".list-item", {
  y: 30,
  autoAlpha: 0,
  duration: 0.6,
  stagger: 0.1,
  ease: "power3.out",
  scrollTrigger: { trigger: ".list", start: "top 80%" }
});

视差主视觉区

gsap.to(".hero-bg", {
  yPercent: 30,
  ease: "none",
  scrollTrigger: {
    trigger: ".hero",
    start: "top top",
    end: "bottom top",
    scrub: true
  }
});

此技能附带的参考资料

参见:

  • references/GSAP_CORE.md
  • references/GSAP_RECIPES.md
  • references/UI_INTERACTIONS.md
  • references/TEXT_ANIMATIONS.md
  • references/PHYSICS_PLUGINS.md
  • references/NEXTJS_REACT_PATTERNS.md
  • references/GSDEVTOOLS.md
  • references/DEBUG_CHECKLIST.md

评论

支持 Markdown