# showtime-components

一个面向移动端交互体验的 Vue 3 组件包，当前提供：

- `ShowtimeBottomSheet`
- `ShowtimeActionBar`
- `ShowtimeRevealBottomSheet`
- `ShowtimeMaintenancePage`
- `ShowtimeSkeleton`
- `ShowtimeSkeletonLine`
- `ShowtimeSkeletonShape`
- `ShowtimeWaterfall`
- `ShowtimeTwoLineEllipsis`
- `ShowtimeUpload`

## 安装

```bash
pnpm add showtime-components
```

如果你在本地 workspace 中使用，也可以直接通过 workspace 依赖接入。

## 基础使用

```vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  ShowtimeActionBar,
  ShowtimeBottomSheet,
  ShowtimeMaintenancePage,
  ShowtimeRevealBottomSheet,
  ShowtimeSkeleton,
  ShowtimeSkeletonLine,
  ShowtimeSkeletonShape,
  ShowtimeTwoLineEllipsis,
  ShowtimeWaterfall,
  type ShowtimeBottomSheetDragPayload,
  type ShowtimeSheetSnap
} from 'showtime-components'
import 'showtime-components/style.css'

const snap = ref<ShowtimeSheetSnap>('peek')
const followed = ref(false)

function handleDrag(payload: ShowtimeBottomSheetDragPayload) {
  console.log('sheet y:', payload.y)
}
</script>

<template>
  <ShowtimeBottomSheet
    v-model:snap="snap"
    @drag="handleDrag"
  >
    <template #header>
      <ShowtimeActionBar v-model:follow-active="followed" />
    </template>

    <div>这里是内容区域</div>
  </ShowtimeBottomSheet>
</template>
```

## 样式引入

如果你的构建流程需要显式引入样式，可以加上：

```ts
import 'showtime-components/style.css'
```

### 共享样式定制

样式文件提供可覆盖的 `--showtime-*` token，用于统一定制圆角、色彩、阴影和动效：

```css
:root {
  --showtime-color-surface: #fffdf8;
  --showtime-color-text: #263238;
  --showtime-radius-md: 8px;
  --showtime-radius-lg: 24px;
  --showtime-shadow-sheet: 0 -12px 36px rgba(23, 42, 34, 0.16);
}
```

完整 token 列表和局部覆盖示例见 [样式定制](./dist/docs/components/style-customization.md)。组件实例 props（例如 `panelBackground` 和 `panelRadius`）优先于全局 token。

## Skeleton 与 Waterfall

`ShowtimeSkeleton` 默认开启单束清晰高亮扫光，光束完整离开占位面后再进入下一轮；可通过 `:animation="false"` 关闭。根组件支持 `baseColor`、`highlightColor`、`animationDuration`、`animationAngle`、`animationDirection` 和 `borderRadius`；其中 `animationAngle` 控制扫光带的倾斜角度，`animationDirection` 支持 `ltr`、`rtl`、`ttb`、`btt` 四种移动方向。提高 `highlightColor` 与 `baseColor` 的对比度可获得更醒目的效果。这些视觉配置会传给 `skeleton` 插槽内的后代。`ShowtimeSkeletonLine` 与 `ShowtimeSkeletonShape` 还可以单独覆盖颜色和圆角，分别支持 `rows`、`widths`、`lineHeight`、`lineSpacing`、`shape` 与 `size`。自定义骨架插槽可用 `showtime-skeleton__surface` 标记实际占位面，避免包装容器重复扫光。

`ShowtimeWaterfall` 通过 `ResizeObserver` 测量实际卡片高度并分配到最短列，在接近页面底部时发出 `load` 事件。由父组件维护 `loading` 和 `finished`；加载中骨架卡片会均分到各列并默认显示扫光动画。

详细 API 参见 [Skeleton](./dist/docs/components/skeleton.md) 与 [Waterfall](./dist/docs/components/waterfall.md)。

## ShowtimeTwoLineEllipsis

`ShowtimeTwoLineEllipsis` 默认用实际渲染字体把正文限制在两行，也可以通过 `lineCount` 自定义折叠行数。最后一行会为“更多”入口保留真实宽度，点击后展示全部正文；容器尺寸变化时会通过 `ResizeObserver` 重新排版。

```vue
<script setup lang="ts">
import { ShowtimeTwoLineEllipsis } from 'showtime-components'
</script>

<template>
  <ShowtimeTwoLineEllipsis
    text="这是一段需要限制在折叠行数内展示的正文，超出部分会在末行继续省略。"
    :line-count="3"
    :show-ellipsis="false"
    more-text="查看详情"
    more-href="/detail"
    @more="({ href }) => console.log(href)"
  >
    <template #more="{ text, openMore }">
      <button type="button" @click="openMore">{{ text }}</button>
    </template>
  </ShowtimeTwoLineEllipsis>
</template>
```

| 参数 | 说明 | 默认值 |
| --- | --- | --- |
| `text` | 要测量并拆分的正文 | `''` |
| `lineCount` | 折叠态最多显示的行数，小于 1 的值按 1 处理 | `2` |
| `showEllipsis` | 是否在折叠态末行显示省略号；关闭后只裁切，不显示 `...` | `true` |
| `expanded` | 展开状态，支持 `v-model:expanded` | `false` |
| `moreText` | 默认“更多”入口文案，也会传给 `more` 插槽 | `更多>>` |
| `collapseText` | 展开后默认“收起”入口文案 | `收起` |
| `moreHref` | 默认入口的链接地址 | `#` |
| `lineHeight` | 单行高度，数字按 px 处理 | `28` |
| `moreGap` | 末行正文与入口的间距，数字按 px 处理 | `12` |

`more` 事件返回 `{ href, event }`；点击默认入口或调用 `more` 插槽的 `openMore` 后会展示全部正文，并发出 `update:expanded`。展开后默认把“收起”作为正文后的行内入口，空间不足时自然换行；也可以使用 `collapse` 插槽或 `closeMore` 方法自定义位置。`more` 插槽接收 `{ text, href, hasOverflow, expanded, lineCount, openMore, closeMore }`。默认 `#` 链接会阻止页面跳回顶部，传入真实地址时仍保留原生链接行为。完整说明见 [多行省略](./dist/docs/components/two-line-ellipsis.md)。

## ShowtimeBottomSheet

一个支持可配置吸附点列表的移动端底部弹层组件，默认仍提供 `peek / half / full` 三档体验。

### 完整 Props（`ShowtimeBottomSheetProps`）

| 参数 | 说明 | 默认值 |
| --- | --- | --- |
| `snap` | 当前吸附点 | `peek` |
| `snapPoints` | 推荐传数组 `[{ key, position }]`，兼容 `{ peek, half, full }` 对象写法 | 默认三档数组 |
| `minDragDistance` | 触发切换的最小拖动距离 | `46` |
| `snapSettleTolerance` | 松手位置接近 snap 时直接吸附的容差 | `10` |
| `contentClass` | 追加到面板根节点上的类名 | `''` |
| `headerClass` | 追加到 header 插槽容器上的类名 | `''` |
| `bodyClass` | 追加到默认内容容器上的类名 | `''` |
| `entranceAnimation` | 是否启用首次入场动画 | `true` |
| `entranceOffset` | 首次入场上浮偏移量，单位 px | `14` |
| `entranceDuration` | 首次入场动画时长，单位 ms | `260` |
| `snapAnimation` | 是否启用 snap 切换动画 | `true` |
| `snapAnimationStiffness` | snap 弹簧动画刚度，值越大越利落 | `420` |
| `snapAnimationDamping` | snap 弹簧动画阻尼，值越大越稳 | `36` |
| `snapAnimationMass` | snap 弹簧动画质量，值越大越有惯性 | `0.9` |
| `headerDragOnly` | 是否只允许顶部区域触发拖拽；开启时会关闭内容区边界联动 | `true` |
| `handleVisible` | 是否显示默认拖拽把手 | `true` |
| `handleWidth` | 默认拖拽把手宽度，单位 px | `62` |
| `handleHeight` | 默认拖拽把手高度，单位 px | `8` |
| `panelBackground` | 面板背景色 | `#ffffff` |
| `panelRadius` | 面板顶部圆角半径，单位 px | `34` |
| `contentEdgeSnap` | 是否允许内容区边界继续推动 sheet 连续移动（仅在 `headerDragOnly=false` 时生效） | `true` |
| `contentEdgeSnapThreshold` | 内容区边界联动触发阈值，单位 px | `26` |

### 事件

| 事件 | 说明 |
| --- | --- |
| `update:snap` | `v-model:snap` 更新 |
| `snap-change` | 吸附点变化时触发 |
| `drag` | 顶部拖拽过程中持续触发，返回 `{ y, movementY, isDragging }` |
| `content-touchstart` | 内容区 `touchstart` 原生事件透传 |
| `content-touchmove` | 内容区 `touchmove` 原生事件透传 |
| `content-touchend` | 内容区 `touchend` 原生事件透传 |
| `content-touchcancel` | 内容区 `touchcancel` 原生事件透传 |
| `content-wheel` | 内容区 `wheel` 原生事件透传 |

### 特点

- 推荐 API：`snapPoints` 使用 `{ key, position }[]`
- 兼容 API：旧版 `{ peek, half, full }` 对象仍然可用
- 直接拖拽和内容区边界联动都会在松手后吸附到最近的 snap point
- 当 `headerDragOnly` 开启时，内容区只保留原生滚动和事件透传，不再触发边界联动
- 内容区边界联动会从事件目标向上查找真正可由用户滚动的容器
- 只有 `overflow-y: auto/scroll/overlay` 的溢出元素会被当成滚动容器，`overflow-y: hidden/visible/clip` 会被视为已经处在内容边界上
- 内容区滚动和顶部拖拽分离
- 内容区边界可继续推动 sheet 本身移动
- 可通过 `drag` 事件实时获取当前面板的 y 轴坐标
- 可监听内容区原生 `touch` / `wheel` 事件做埋点或联动

## ShowtimeRevealBottomSheet

一个基于 `ShowtimeBottomSheet` 的高层包装组件，用来封装“头部动态 reveal 层”这类交互。

### 三个 Slot

| Slot | 说明 |
| --- | --- |
| `reveal` | 动态头部层，可以放图片、渐变、插画、视频壳或任意元素 |
| `header` | 前景头部内容，例如标题、副标题、操作区 |
| `default` | 内容区主体 |

### 常用 Props

| 参数 | 说明 | 默认值 |
| --- | --- | --- |
| `snap` | 当前吸附点 | 继承底层 sheet 默认行为 |
| `snapPoints` | 透传到底层 sheet 的吸附点配置 | 无 |
| `revealHeight` | reveal 区高度，单位 px | `200` |
| `revealSnapKeys` | 允许 reveal 的 snap key 列表 | `['half', 'full']` |
| `revealClass` | reveal 层附加类名 | `''` |
| `headerClass` | 前景头部附加类名 | `''` |
| `bodyClass` | 内容区附加类名 | `''` |

### 特点

- `reveal` slot 可以承载任意元素，而不是只支持图片
- 拖拽中 reveal 层严格跟随底层 sheet 的真实 `y` 坐标
- 即使拖拽从 reveal 区间外开始，跨过区间下边界后也会按真实 `y` 坐标连续展开，到达上边界时完整显示
- reveal progress 大于 0 时自动隐藏默认把手，完全收起后恢复调用方的 `handleVisible` 设置
- reveal 区间内的相邻 snap 切换共用同一份 progress，不会让头部和面板各自补间
- 松手后 reveal 会回到离散 snap 态，不会停在半开中间态
- placeholder 高度和 reveal 进度共用同一状态源，前景头部不会跳动
- 面板圆角会在非拖拽阶段柔和过渡，拖拽中关闭过渡，避免和 reveal 头部不同步
- 会继续向外透传底层的 `drag`、`update:snap`、`snap-change`

### 默认交互基线

`ShowtimeRevealBottomSheet` 为 reveal 场景内置了一组比底层 sheet 更合适的交互默认值：

- `headerDragOnly=false`
- `contentEdgeSnap=true`

所以在常见的 `quick-full -> full` 头图展开流程里，组件会自行处理 reveal 连续进度和默认把手显隐。业务通常只需要按最终 snap 调整 `panelRadius` 或内容滚动策略，不必再按当前 snap 额外切换 `headerDragOnly` / `contentEdgeSnap`。

### 基础用法

```vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  ShowtimeRevealBottomSheet,
  type ShowtimeSheetSnap
} from 'showtime-components'

const snap = ref<ShowtimeSheetSnap>('quick-full')
</script>

<template>
  <ShowtimeRevealBottomSheet
    v-model:snap="snap"
    :snap-points="[
      { key: 'peek', position: 90 },
      { key: 'half', position: 55 },
      { key: 'quick-full', position: 30 },
      { key: 'full', position: 0 }
    ]"
  >
    <template #reveal>
      <div class="hero-cover" />
    </template>

    <template #header>
      <strong>互动留言</strong>
    </template>

    <div>这里是内容区域</div>
  </ShowtimeRevealBottomSheet>
</template>
```

## ShowtimeActionBar

一个适合内容详情页、弹层顶部、互动区的操作栏组件。

### 常用参数

| 参数 | 说明 | 默认值 |
| --- | --- | --- |
| `items` | 左侧互动按钮列表 | 默认内置三项 |
| `followLabel` | 未关注文案 | `关注` |
| `followedLabel` | 已关注文案 | `已关注` |
| `followActive` | 初始关注状态 | `false` |
| `animated` | 是否启用点击反馈动画 | `true` |

### 事件

| 事件 | 说明 |
| --- | --- |
| `action` | 任一按钮点击时触发，返回按钮 key |
| `update:followActive` | 关注状态更新 |
| `follow-change` | 关注状态变化时触发 |

## ShowtimeUpload

统一处理图片、视频、办公文档、PDF 和常见压缩包的上传场景。组件只负责点击选择、拖拽文件、校验、裁剪、队列、进度、预览和状态管理；实际上传由 `upload` 属性传入的方法完成。当 `limit > 1` 时，会按 `min(limit, maxConcurrent)` 自动调度多文件上传；取消操作通过上传回调上下文中的 `AbortSignal` 传给业务请求。

```vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  ShowtimeUpload,
  type ShowtimeUploadHandler,
  type ShowtimeUploadItem
} from 'showtime-components'

const files = ref<ShowtimeUploadItem[]>([])

const upload: ShowtimeUploadHandler = async (file, { signal, onProgress }) => {
  // 请在这里调用项目自己的上传接口，并在请求进度变化时调用 onProgress。
  return { url: URL.createObjectURL(file) }
}
</script>

<template>
  <ShowtimeUpload
    v-model="files"
    :upload="upload"
    :kinds="['image', 'file']"
    theme-color="#1677ff"
    multiple
  />
</template>
```

组件根节点支持拖入文件，拖拽与点击选择共享校验、裁剪和上传流程。`themeColor` 可统一设置内置界面的强调色；完整的上传回调类型、规则、插槽、自定义操作区和样式变量见 [ShowtimeUpload](./dist/docs/components/upload.md)。

组件库还导出无框架依赖的 `TaskQueue`，可用于其他支持 `AbortSignal` 的浏览器异步任务。它提供 FIFO 调度、并发控制、暂停、取消、清空与销毁能力；上传场景的使用方式见 [ShowtimeUpload](./dist/docs/components/upload.md)。

## ShowtimeMaintenancePage

一个可用于路由页或容器内的维护状态展示组件，默认提供低干扰的亮色科技视觉和 CSS 动画；背景使用 SVG 固定网格与折线电路脉冲，可通过 `theme="dark"` 切换深色模式。组件不绑定业务品牌，也不会自行刷新、倒计时或跳转。

### 常用 Props

| 参数 | 说明 | 默认值 |
| --- | --- | --- |
| `theme` | 视觉主题，可选 `light` 或 `dark` | `light` |
| `status` | 维护状态文案 | `System maintenance` |
| `title` | 主标题 | `Service temporarily unavailable` |
| `message` | 说明文案 | 内置维护提示 |
| `contactLabel` | 联系方式标签 | `''` |
| `contactValue` | 联系方式内容 | `''` |
| `contactHref` | 联系方式链接；传入后输出原生链接 | `''` |
| `logo` / `logoAlt` | 可选 logo URL 与替代文本 | `''` |
| `illustration` / `illustrationAlt` | 可选插画 URL 与替代文本 | `''` |
| `minHeight` | 页面最小高度 | `100dvh` |

### Slots

| Slot | 说明 |
| --- | --- |
| `logo` | 覆盖品牌区 |
| `illustration` | 覆盖默认维护核心 |
| `actions` | 放置业务自己的返回、刷新等操作 |

### 基础用法

```vue
<ShowtimeMaintenancePage
  theme="light"
  status="Scheduled maintenance"
  title="Service temporarily unavailable"
  message="We are upgrading the platform. Please check back shortly."
  contact-label="Support"
  contact-value="support@example.com"
  contact-href="mailto:support@example.com"
/>
```

## 类型导出

包内也导出了常用类型：

```ts
import type {
  ShowtimeActionBarProps,
  ShowtimeActionItem,
  ShowtimeBottomSheetDragPayload,
  ShowtimeBottomSheetProps,
  ShowtimeRevealBottomSheetProps,
  ShowtimeMaintenancePageProps,
  ShowtimeSkeletonAnimationDirection,
  ShowtimeSkeletonProps,
  ShowtimeSkeletonLineProps,
  ShowtimeSkeletonShapeProps,
  ShowtimeSkeletonVisualProps,
  ShowtimeWaterfallProps,
  ShowtimeTwoLineEllipsisCollapseSlotProps,
  ShowtimeTwoLineEllipsisMorePayload,
  ShowtimeTwoLineEllipsisMoreSlotProps,
  ShowtimeTwoLineEllipsisProps,
  ShowtimeSheetSnap
} from 'showtime-components'
```

## License

[MIT](./LICENSE)
