Skip to content

Popup 工具

Popup 是一个底层工具函数,用于动态创建弹出视图。Loading 和 Toast 组件都是基于 Popup 实现的。

基础使用

tsx
import { Popup } from '@0x30/navigation-vue'
import { animate } from 'animejs'

function showCustomPopup() {
  const [show, close] = Popup({
    onEnter(el, done) {
      animate(el, {
        opacity: [0, 1],
        scale: [0.8, 1],
        duration: 300,
        ease: 'outExpo',
        onComplete: done
      })
    },
    onLeave(el, done) {
      animate(el, {
        opacity: [1, 0],
        scale: [1, 0.8],
        duration: 200,
        onComplete: done
      })
    }
  })

  show(
    <div class="my-popup">
      <h2>自定义弹窗</h2>
      <p>这是使用 Popup 创建的弹窗</p>
      <button onClick={() => close()}>关闭</button>
    </div>
  )
}
tsx
import { Popup } from '@0x30/navigation-react'
import { animate } from 'animejs'

function showCustomPopup() {
  const [show, close] = Popup({
    onEnter(el, done) {
      animate(el, {
        opacity: [0, 1],
        scale: [0.8, 1],
        duration: 300,
        ease: 'outExpo',
        onComplete: done
      })
    },
    onLeave(el, done) {
      animate(el, {
        opacity: [1, 0],
        scale: [1, 0.8],
        duration: 200,
        onComplete: done
      })
    }
  })

  show(
    <div className="my-popup">
      <h2>自定义弹窗</h2>
      <p>这是使用 Popup 创建的弹窗</p>
      <button onClick={() => close()}>关闭</button>
    </div>
  )
}

API

tsx
const [show, close] = Popup(options?)

返回值

方法类型说明
show(content: ReactNode) => Promise<void>显示弹窗内容,进入动画完成后 resolve
close() => Promise<void>关闭弹窗,退出动画完成后 resolve

Options

tsx
interface PopupOptions {
  onEnter?: (el: Element, done: () => void) => void  // 进入动画
  onLeave?: (el: Element, done: () => void) => void  // 退出动画
  root?: Element  // 挂载的根元素,默认 document.body
}

动画钩子

onEnter

进入动画钩子,当内容显示时调用:

tsx
onEnter(el, done) {
  // el: 内容元素
  // done: 动画完成后必须调用此函数
  
  animate(el, {
    opacity: [0, 1],
    onComplete: done  // 重要:动画完成后调用 done
  })
}

onLeave

退出动画钩子,当关闭时调用:

tsx
onLeave(el, done) {
  animate(el, {
    opacity: [1, 0],
    onComplete: done  // 重要:动画完成后调用 done
  })
}

完整示例

确认弹窗

tsx
import { Popup } from '@0x30/navigation-vue'
import { animate } from 'animejs'

function confirm(message: string): Promise<boolean> {
  return new Promise((resolve) => {
    const [show, close] = Popup({
      onEnter(el, done) {
        animate(el, {
          opacity: [0, 1],
          scale: [0.9, 1],
          duration: 200,
          onComplete: done
        })
      },
      onLeave(el, done) {
        animate(el, {
          opacity: [1, 0],
          duration: 150,
          onComplete: done
        })
      }
    })

    const handleConfirm = async () => {
      await close()
      resolve(true)
    }

    const handleCancel = async () => {
      await close()
      resolve(false)
    }

    show(
      <div class="confirm-overlay">
        <div class="confirm-dialog">
          <div class="confirm-message">{message}</div>
          <div class="confirm-actions">
            <button onClick={handleCancel}>取消</button>
            <button onClick={handleConfirm}>确认</button>
          </div>
        </div>
      </div>
    )
  })
}

// 使用
const result = await confirm('确定要删除吗?')
if (result) {
  // 用户点击了确认
}
tsx
import { Popup } from '@0x30/navigation-react'
import { animate } from 'animejs'

function confirm(message: string): Promise<boolean> {
  return new Promise((resolve) => {
    const [show, close] = Popup({
      onEnter(el, done) {
        animate(el, {
          opacity: [0, 1],
          scale: [0.9, 1],
          duration: 200,
          onComplete: done
        })
      },
      onLeave(el, done) {
        animate(el, {
          opacity: [1, 0],
          duration: 150,
          onComplete: done
        })
      }
    })

    const handleConfirm = async () => {
      await close()
      resolve(true)
    }

    const handleCancel = async () => {
      await close()
      resolve(false)
    }

    show(
      <div className="confirm-overlay">
        <div className="confirm-dialog">
          <div className="confirm-message">{message}</div>
          <div className="confirm-actions">
            <button onClick={handleCancel}>取消</button>
            <button onClick={handleConfirm}>确认</button>
          </div>
        </div>
      </div>
    )
  })
}

// 使用
const result = await confirm('确定要删除吗?')
if (result) {
  // 用户点击了确认
}

ActionSheet

tsx
import { Popup } from '@0x30/navigation-vue'
import { animate } from 'animejs'

function showActionSheet(actions: { text: string, onClick: () => void }[]) {
  const [show, close] = Popup({
    onEnter(el, done) {
      const sheet = el.querySelector('.action-sheet')
      const mask = el.querySelector('.mask')
      animate(mask, { opacity: [0, 1], duration: 200 })
      animate(sheet, {
        translateY: ['100%', '0%'],
        duration: 300,
        ease: 'outExpo',
        onComplete: done
      })
    },
    onLeave(el, done) {
      const sheet = el.querySelector('.action-sheet')
      const mask = el.querySelector('.mask')
      animate(mask, { opacity: [1, 0], duration: 200 })
      animate(sheet, {
        translateY: ['0%', '100%'],
        duration: 200,
        onComplete: done
      })
    }
  })

  show(
    <div class="action-sheet-container">
      <div class="mask" onClick={() => close()} />
      <div class="action-sheet">
        {actions.map(action => (
          <button onClick={() => { action.onClick(); close() }}>
            {action.text}
          </button>
        ))}
        <button onClick={() => close()}>取消</button>
      </div>
    </div>
  )
}
tsx
import { Popup } from '@0x30/navigation-react'
import { animate } from 'animejs'

function showActionSheet(actions: { text: string, onClick: () => void }[]) {
  const [show, close] = Popup({
    onEnter(el, done) {
      const sheet = el.querySelector('.action-sheet')
      const mask = el.querySelector('.mask')
      animate(mask, { opacity: [0, 1], duration: 200 })
      animate(sheet, {
        translateY: ['100%', '0%'],
        duration: 300,
        ease: 'outExpo',
        onComplete: done
      })
    },
    onLeave(el, done) {
      const sheet = el.querySelector('.action-sheet')
      const mask = el.querySelector('.mask')
      animate(mask, { opacity: [1, 0], duration: 200 })
      animate(sheet, {
        translateY: ['0%', '100%'],
        duration: 200,
        onComplete: done
      })
    }
  })

  show(
    <div className="action-sheet-container">
      <div className="mask" onClick={() => close()} />
      <div className="action-sheet">
        {actions.map(action => (
          <button onClick={() => { action.onClick(); close() }}>
            {action.text}
          </button>
        ))}
        <button onClick={() => close()}>取消</button>
      </div>
    </div>
  )
}

## 注意事项

1. **必须调用 done**: 动画完成后必须调用 `done()` 函数,否则 Promise 不会 resolve
2. **自动清理**: `close()` 完成后会自动移除 DOM 元素
3. **样式自理**: Popup 只负责 DOM 管理和动画,样式需要自己编写

Released under the MIT License.