For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt, and this page is available as Markdown at /zh/guide/start/tutorial-payment-details.md.
Lynx
  • 简体中文
  • 教程:Payment Details

    在完成 Gallery 教程后,相信你已经掌握了 Lynx 的基础用法。接下来,让我们通过一个支付详情页面来学习一些更进阶的功能,包括:

    • 搭建可交互的滚动列表
    • 如何制作 3D 交互动画效果
    • 如何在不同组件之间传递数据

    我们要构建什么?

    让我们先看看这个应用的最终效果。要体验实际效果,请先下载安装 Lynx Explorer App,然后用它扫描下面的二维码。

    让我们开始吧

    我们先来看看这个页面的组成,如果你想搭建一个这样的页面,可以将它拆成这样的三个组成部分,然后一步步实现它:

    card
    1. 卡片详情
      • 卡片可以实现翻转动画
      • 这里我们会学习如何使用 CSS 动画来实现平滑的翻转效果
    card
    1. 卡片列表,这个组件我们用 scroll-view 元件来包裹
      • 可以上下滚动浏览所有卡片
      • 点击某张卡片后,顶部的卡片详情会更新对应的卡号信息
      • 这里我们会学习如何搭建可交互的滚动列表,以及如何在组件之间传递数据
    cardcard
    1. 顶部的金额以及底部的按钮,这些组件比较简单,我们用 view 元件来实现即可。

    让我们重点关注三个技术要点:搭建可交互的滚动列表,3D 翻转动画效果实现和组件间的数据传递。

    搭建一个可交互的卡片列表

    首先,我们来创建一个银行卡列表。这个列表需要展示每张卡片的基本信息,包括:

    • 银行类型(比如 Bac, Boc 等等)
    • 卡号(显示前后四位)
    • 持卡人姓名
    • 是否为主卡

    我们先把这些信息整理成一个数据结构:

    BankCardScrollView.tsx
    export interface BankCard {
      type: string; // 银行类型(比如 Bac, Boc等等)
      number: string; // 卡号
      name: string; // 持卡人姓名
    }

    然后,准备一些卡片数据用于展示:

    BankCardScrollView.tsx
    
    const cards = [
      { type: "bac", number: "4558 **** **** 6767", name: "Alex Quentin" },
      { type: "boc", number: "6222 **** **** 8058", name: "Alex Quentin" },
      ...
    ];
    

    接下来,我们用 <scroll-view> 元件来创建一个可以上下滑动的列表,把所有卡片信息展示出来:

    BankCardScrollView.tsx {6}
    export default function BankCardScrollView() {
      return (
        <view className="payment-wrapper">
          <text className="title">Payment method</text>
          <view className="payment-container">
            <scroll-view scroll-y className="payment-sv">
              {cards.map((card, idx) => (
                <view
                  key={idx}
                  className="card"
                  bindtap={() => handleCardSelect(card)}
                >
                  <view className="card-info">
                    <image className="card-icon" src={getUrlByType(card.type)} />
                    <view className="card-details">
                      <text className="card-name">
                        {card.type.charAt(0).toUpperCase() + card.type.slice(1)}
                      </text>
                    </view>
                  </view>
                </view>
              ))}
            </scroll-view>
          </view>
        </view>
      );
    }

    为了让用户知道自己选中了哪张卡片,我们需要添加一个图标来表示已经被选中:

    BankCardScrollView.tsx
    <image className="check-icon" src={checkIcon} />

    然后,我们需要定义一个 selectedCard 状态,用于记录当前选中的卡片。

    BankCardScrollView.tsx
    const [selectedCard, setSelectedCard] = useState(cards[0]);

    添加完成后,我们需要在 <BankCardScrollView> 组件中添加一个 handleCardSelect 函数,用于处理选中卡片的事件。

    BankCardScrollView.tsx
    const handleCardSelect = (card: BankCard) => {
      setSelectedCard(card);
    };

    当用户点击某个卡片时,就会触发 handleCardSelect 函数,这个函数会更新 selectedCard 的状态。

    BankCardScrollView.tsx
    <view
      className={`card ${selectedCard === card ? 'selected' : ''}`}
      bindtap={() => handleCardSelect(card)}
    >
      ...
    </view>

    我们把上面逻辑组合一下,当用户选中某张卡片时,右侧会出现一个小小的勾选标记,以此来标识当前选中状态:

    BankCardScrollView.tsx {5,17}
    export default function BankCardScrollView() {
      const [selectedCard, setSelectedCard] = useState(cards[0]);
    
      const handleCardSelect = (card: BankCard) => {
        setSelectedCard(card);
      };
    
      return (
        <view className="payment-wrapper">
          <text className="title">Payment method</text>
          <view className="payment-container">
            <scroll-view scroll-y className="payment-sv">
              {cards.map((card, idx) => (
                <view
                  key={idx}
                  className="card"
                  bindtap={() => handleCardSelect(card)}
                >
                  <view className="card-info">
                    <image className="card-icon" src={getUrlByType(card.type)} />
                    <view className="card-details">
                      <text className="card-name">
                        {card.type.charAt(0).toUpperCase() + card.type.slice(1)}
                      </text>
                    </view>
                  </view>
                  {selectedCard === card && (
                    <image className="check-icon" src={checkIcon} />
                  )}
                </view>
              ))}
            </scroll-view>
          </view>
        </view>
      );
    }

    现在,我们已经完成了这个可交互的卡片列表的搭建,让我们一起来看看效果吧!

    3D 翻转特效

    现在让我们来重现这个有趣的 3D 翻转效果,首先需要了解实现这个效果的关键步骤 —— CSS 动画。

    在 Lynx 中支持的 CSS 动画集合

    Lynx 支持多种 CSS 动画集合,想了解更多动画玩法,可以看看 CSS Animation

    为了实现这个翻转效果,我们需要两个关键步骤:

    我们先创建一个 <Card/> 组件

    1. 定义翻转动画:
      • 使用 keyframes 来描述卡片从正面翻到背面(以及反向)的过程,其中包含了旋转的关键帧。
      • transform 属性则定义了元件的旋转角度。
    Cards.scss
    .front {
      animation: backToFront 0.5s both;
    }
    
    .back {
      animation: frontToBack 0.5s both;
    }
    
    @keyframes frontToBack {
      0% {
        transform: rotateY(0deg) translateZ(1);
      }
    
      100% {
        transform: rotateY(180deg) translateZ(0);
      }
    }
    
    @keyframes backToFront {
      0% {
        transform: rotateY(-180deg) translateZ(0);
      }
    
      100% {
        transform: rotateY(0deg) translateZ(1);
      }
    }
    1. 让卡片响应点击:
    • 当用户点击底部按钮时,触发翻转动画
    • 通过切换 className 来控制卡片是显示正面还是背面
    Cards.tsx
    export default function Card({ isFront, isFirstRender }: CardProps) {
      return (
        <view className="card-content">
          <view className={`card-back ${isFront ? 'back' : 'front'}`}>...</view>
          <view
            className={`card-front ${!isFirstRender ? (isFront ? 'front' : 'back') : ''}`}
          >
            ...
          </view>
        </view>
      );
    }

    这样,我们就实现了一个既实用又好玩的卡片翻转效果!用户每次点击底部按钮时,都能看到流畅的翻转动画,让整个交互体验更加生动有趣。

    跨组件数据交互

    相信你也发现了一个问题,就是在点击卡片列表时,卡片详情并不会更新卡号,我们还需要解决卡片详情的同步更新问题。

    在这个应用中,我们有两个主要组件:

    • 卡片列表:展示所有可选的银行卡的 <BankCardScrollView/> 组件
    • 卡片详情:显示当前选中卡片的详细信息的 <Card/> 组件

    当用户在列表中点击某张卡片时,顶部的卡片详情需要同步更新显示该卡片的信息。为了实现这个功能,我们需要让这两个组件能够传递数据。

    首先,我们定义一个回调函数,用来通知其他组件用户选择了哪张卡片:

    BankCardScrollView.tsx
    export interface BankCardScrollViewProps {
      onCardSelect?: (card: BankCard) => void;
    }

    然后,我们在之前定义的 handleCardSelect 函数中调用这个回调函数:

    BankCardScrollView.tsx
    const handleCardSelect = (card: BankCard) => {
      setSelectedCard(card);
      onCardSelect?.(card);
    };

    接着,我们把 onCardSelect 作为属性,定义在 <BankCardScrollView> 组件中:

    BankCardScrollView.tsx
    export default function BankCardScrollView({
      onCardSelect,
    }: BankCardScrollViewProps) {
      const [selectedCard, setSelectedCard] = useState(cards[0]);
    
      const handleCardSelect = (card: BankCard) => {
        setSelectedCard(card);
        onCardSelect?.(card);
      };
    
      return (
        <view className="payment-wrapper">
          <text className="title">Payment method</text>
          <view className="payment-container">
            <scroll-view scroll-y className="payment-sv">
              {cards.map((card, idx) => (
                <view
                  key={idx}
                  className="card"
                  bindtap={() => handleCardSelect(card)}
                >
                  ...
                </view>
              ))}
            </scroll-view>
          </view>
        </view>
      );
    }

    处理完了 <BankCardScrollView> 组件, 我们需要继续处理 <Card> 组件用来在切换列表时更新卡号。

    它接收一个 selectedCard 属性,用于展示用户当前选中的卡片详情,并把卡号前后四位展示出来。

    Card.tsx
    interface CardProps {
      isFront: boolean;
      isFirstRender: boolean;
      selectedCard: BankCard;
    }

    我们接着再定义一个工具函数,用于截取前后四位的卡号

    Card.tsx
    const getCardNumberParts = (number: string) => {
      const parts = number?.split(' ') || [];
      return {
        firstFour: parts[0] || '4558',
        lastFour: parts[3] || '6767',
      };
    };

    通过工具函数,我们把 selectedCard 中卡号的前后四位展示出来。

    Card.tsx
    export default function Card({
      selectedCard,
      isFront,
      isFirstRender,
    }: CardProps) {
      const { firstFour, lastFour } = getCardNumberParts(selectedCard.number);
    
      return (
        <view className="card-content">
          <view
            className={`card-back ${!isFirstRender ? (isFront ? 'back' : 'front') : ''}`}
          >
            ...
          </view>
    
          <view
            className={`card-front ${!isFirstRender ? (isFront ? 'front' : 'back') : ''}`}
          >
            <view className="card-number">
              <text className="first-digits">{firstFour}</text>
              <text className="middle-digits">**** ****</text>
              <text className="last-digits">{lastFour}</text>
            </view>
            <view className="card-info">
              <text>{selectedCard?.name || 'Card holder'}</text>
            </view>
          </view>
        </view>
      );
    }

    最后,在父组件中组合这两个组件:

    1. 使用 selectedCard 状态存储当前选中的卡片
    2. 当卡片列表 <BankCardScrollView>onCardSelect 通知有新卡片被选中时,更新这个状态
    3. 将这个状态传给卡片详情 <Card>,用于展示选中的卡片的信息
    index.tsx
    function BankCards() {
      const [selectedCard, setSelectedCard] = useState<BankCard>({
        type: 'visa',
        number: '4558 **** **** 6767',
        name: 'Alex Quentin',
      });
    
      const [isFront, setIsFront] = useState(true);
      const [isFirstRender, setIsFirstRender] = useState(true);
    
      const handleCardSelect = (card: BankCard) => {
        setSelectedCard(card);
        setIsFront(true);
      };
    
      const handlePayNow = () => {
        if (isFirstRender) {
          setIsFirstRender(false);
        }
        setIsFront(!isFront);
      };
    
      return (
        <view class="page">
          <Card
            selectedCard={selectedCard}
            isFront={isFront}
            isFirstRender={isFirstRender}
          />
          <BankCardScrollView onCardSelect={handleCardSelect} />
          <BottomNode onPayNow={handlePayNow} />
        </view>
      );
    }

    这样,我们就建立了一个高效的协作机制:

    1. 用户在卡片列表中选择卡片
    2. 卡片列表立即通知父组件
    3. 父组件更新状态并通知卡片详情组件
    4. 卡片详情组件立即更新显示内容

    让我们看看这个组件联动的效果:

    我们再添加顶部的展示金额,就大功告成了!

    card

    总结

    通过这个支付详情页面的实现,你已经掌握了以下核心技术点:

    • 可交互列表的构建
    • 复杂 CSS 动画效果的开发
    • 组件间数据传递的实现方法

    现在你已经可以使用 Lynx 开发更复杂的应用了。

    除非另有说明,本项目采用知识共享署名 4.0 国际许可协议进行许可,代码示例采用 Apache License 2.0 许可协议进行许可。