# ShowtimeUpload

`ShowtimeUpload` 是统一上传组件，覆盖本地文件选择、拖拽文件、规则校验、图片裁剪、有限并发上传、进度、失败重试、取消、媒体预览和自定义功能区。组件不会调用任何业务接口；实际上传必须由 `upload` 属性传入的方法完成。

## 基础使用

```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 }) => {
  // 调用项目自己的上传接口；signal 用于取消请求，onProgress 的值范围为 0 到 100。
  const result = await uploadFileToService(file, { signal, onProgress })

  return {
    url: result.url,
    coverUrl: result.coverUrl,
    name: result.name
  }
}
</script>

<template>
  <ShowtimeUpload
    v-model="files"
    :upload="upload"
    :kinds="['image', 'file']"
    :limit="9"
    multiple
  />
</template>
```

没有传入 `upload` 时，组件仍可用于本地选择和预览，所选条目会以完成状态写入 `v-model`。传入 `upload` 后，组件将按照 `autoUpload` 和 `maxConcurrent` 配置调度业务上传方法。

## 拖拽上传

组件根节点默认是拖拽区域，支持将文件直接拖入上传组件。拖入文件会复用点击选择后的同一条处理链路，因此 MIME 校验、`accept` 限制、数量限制、图片裁剪、上传队列、取消和事件的行为完全一致。拖入有效文件时，组件会显示强调色边框；`disabled`、`readonly` 或已达到 `limit` 时不会创建新条目。

拖拽能力通过 `@vueuse/core` 的 `useDropZone` 实现，不需要额外配置。即使使用 `trigger` 插槽替换默认选择按钮，根节点的拖拽区域仍然有效。

## 上传调度

当 `limit > 1` 时，组件自动启用上传任务队列，有效并发数为 `min(limit, maxConcurrent)`。例如 `limit=6`、`maxConcurrent=2` 时，前两个文件立即开始，后续文件保持 `queued` 状态；任一请求结束后，队列会自动开始下一项。

单文件模式固定使用一个执行位，不会创建无意义的并发等待。取消等待项会立即移出队列；取消执行中的请求会通过 `context.signal` 发出中止信号。业务上传实现应把该信号传入自己的请求层，避免旧请求在取消或替换后继续占用网络资源。

通用 `TaskQueue` 也从组件库根入口导出，可用于其他支持 `AbortSignal` 的浏览器异步任务：

```ts
import { TaskQueue } from 'showtime-components'

const queue = new TaskQueue({ concurrency: 2 })
const result = await queue.add(
  async (signal) => {
    const response = await fetch('/api/task', { signal })
    return response.json()
  },
  { id: 'task-1' }
)
```

`TaskQueue` 提供 `add`、`pause`、`resume`、`setConcurrency`、`cancel`、`clear`、`has`、`getStatus` 和 `dispose`。它只处理 FIFO 调度和取消，不内置重试或业务事件；上传重试仍由 `ShowtimeUpload` 的条目操作负责。

## Playground 演示

playground 的 `/upload` 路由提供了真实上传演示，服务地址为 `http://192.168.10.11:8110`。演示当前将规则限制为 PNG、JPG 图片，并使用 `theme-color="#1677ff"` 展示自定义主题色。演示使用原生 `XMLHttpRequest`，因此能够将 `upload.onprogress` 转换为组件进度，并把上传回调的 `AbortSignal` 绑定到 `request.abort()`。

图片和附件请求 `/fileUpload/file-server/uploadFile/uploadOne`；视频请求 `/fileUpload/file-server/uploadFile/uploadVideoAutoGrabFrame`，并将响应中的视频和封面地址转换为组件条目。项目接入时应替换为自己的服务地址、鉴权头和响应解析逻辑，并确保服务端允许浏览器跨域请求。

## 文件类别与规则

`kinds` 支持 `image`、`video` 和 `file`。未传入有效 `rules` 键时，它决定允许的资源类别；传入 `rules` 后，规则对象中声明的类别键优先，未声明的类别不会补充默认规则。例如 `kinds` 包含全部类别但 `rules` 只传入 `image` 时，组件只允许图片。类别判断优先匹配 `File.type`，不依赖文件名后缀：

| 类别 | MIME 正则 | MIME 缺失时的受限后缀回退 |
| --- | --- | --- |
| `image` | `image/(png|jpeg|bmp)` | `jpg`、`jpeg`、`png`、`bmp` |
| `video` | `video/(mp4|mpeg|quicktime|x-msvideo|x-ms-wmv|x-flv)` | `mp4`、`mpeg`、`mpg`、`mov`、`avi`、`wmv`、`flv` |
| `file` | 办公文档、PDF 与压缩包 MIME 白名单 | `doc`、`docx`、`xls`、`xlsx`、`ppt`、`pptx`、`pdf`、`zip`、`rar`、`7z`、`tar`、`gz` |

默认 `file` 规则同时支持 `application/zip`、`application/x-zip-compressed`、`application/x-rar-compressed`、`application/x-7z-compressed`、`application/x-tar`、`application/x-gzip`、`application/x-compressed`。有些浏览器会将压缩包标记为 `application/octet-stream`，此时组件只会在扩展名为 `zip`、`rar`、`7z`、`tar` 或 `gz` 时允许上传。

`accept` 是额外限制，不参与类别识别：它会写入原生文件选择器，并在类别校验通过后再次按 MIME 或后缀收窄范围。因此，伪造为 `.pdf` 的 `text/plain` 文件不会仅因扩展名通过校验。

```vue
<ShowtimeUpload
  v-model="files"
  :kinds="['file']"
  :rules="{
    file: {
      accept: ['application/pdf', '.pdf', '.zip', '.7z'],
      maxSize: 50 * 1024 * 1024
    }
  }"
/>
```

## 属性

| 属性 | 说明 | 默认值 |
| --- | --- | --- |
| `v-model` | 上传条目数组，类型为 `ShowtimeUploadItem[]` | `[]` |
| `upload` | 业务方传入的上传方法，返回 `{ url, name?, coverUrl? }` | 不传 |
| `kinds` | 启用的资源类别；未传有效 `rules` 键时生效 | `['image']` |
| `rules` | 各类别的 `accept` 和 `maxSize` 规则；声明的类别键优先于 `kinds` | 内置默认规则 |
| `limit` | 最多保留的条目数 | `1` |
| `multiple` | 是否允许一次选择多个文件 | `true` |
| `disabled` | 是否禁用选择和替换 | `false` |
| `readonly` | 是否只读；仍可预览已上传媒体 | `false` |
| `autoUpload` | 选择后是否立即调用 `upload` | `true` |
| `maxConcurrent` | 多文件模式下同时执行的上传请求数；单文件模式固定为 1 | `2` |
| `crop` | 图片裁剪配置，详见下节；默认对新选择的图片启用裁剪 | `{ enabled: true }` |
| `layout` | `auto`、`grid` 或 `list`；自动模式下媒体使用网格，其余使用列表 | `auto` |
| `themeColor` | 强调色，支持任意有效 CSS 颜色值；会传递给内置视图和插槽内容 | 不传，默认红色 |
| `triggerLabel` | 默认选择按钮文案 | `添加文件` |
| `showTips` | 是否显示默认规则提示 | `true` |
| `previewEnabled` | 是否允许打开内置或自定义预览 | `true` |

## 裁剪与预览

图片裁剪默认只作用于新选择的图片。多张图片会顺序进入裁剪弹层，避免同时创建多个裁剪器。裁剪确认后，组件使用生成的 `File` 继续上传；取消时不会修改已有条目。若业务场景不需要裁剪，可传入 `:crop="{ enabled: false }"` 直接上传原文件。

```vue
<ShowtimeUpload
  v-model="avatars"
  :crop="{
    enabled: true,
    aspectRatio: 1,
    fixedBox: true,
    outputType: 'jpeg'
  }"
/>
```

`previewEnabled` 控制预览入口。图片和视频默认使用内置弹层；其他文件可以用 `preview` 插槽交由业务方处理。

## 事件

| 事件 | 参数 | 说明 |
| --- | --- | --- |
| `update:modelValue` | `items` | 更新条目数组 |
| `select` | `items` | 本次成功选择并创建的条目 |
| `change` | `items` | 任意条目状态或内容变化 |
| `progress` | `item` | 上传进度变化，进度更新按动画帧合并 |
| `success` | `item` | 单个条目上传完成 |
| `error` | `error` | 校验、裁剪或上传失败；包含 `code`、`message`、`file?`、`item?` |
| `remove` | `item` | 用户移除了一个条目 |
| `preview` | `item` | 用户请求预览一个条目 |

`error.code` 可能为 `kind`、`accept`、`max-size`、`limit`、`upload` 或 `crop`。

## 插槽

插槽均会收到组件封装好的操作方法，业务方不需要访问内部队列或修改内部状态。

| 插槽 | 插槽属性 | 用途 |
| --- | --- | --- |
| `trigger` | `open`、`items`、`remaining`、`accept`、`disabled` | 替换文件选择入口 |
| `item` | `item`、`index`、`remove`、`replace`、`retry`、`preview`、`cancel` | 完全替换单个条目的布局 |
| `item-actions` | `item`、`remove`、`replace`、`retry`、`preview`、`cancel` | 保留默认条目，仅替换操作区 |
| `tip` | `items`、`limit`、`remaining`、`rules` | 替换默认规则提示 |
| `preview` | `item`、`close` | 替换非媒体或全部条目的预览内容 |
| `cropper-footer` | `confirmCrop`、`cancelCrop`、`cropping` | 替换图片裁剪弹层底部操作区 |

```vue
<ShowtimeUpload v-model="files" :upload="upload">
  <template #trigger="{ open, remaining }">
    <button type="button" :disabled="remaining === 0" @click="open">选择素材</button>
  </template>

  <template #item-actions="{ item, remove, retry }">
    <button v-if="item.status === 'error'" type="button" @click="retry">重试</button>
    <button type="button" @click="remove">删除</button>
  </template>
</ShowtimeUpload>
```

## 实例方法

使用组件引用可调用以下方法：

| 方法 | 说明 |
| --- | --- |
| `open()` | 打开原生文件选择器 |
| `clear()` | 清空全部条目，并取消进行中的上传 |
| `remove(id)` | 移除指定条目 |
| `retry(id)` | 重试失败或已取消的本地条目 |
| `uploadPending()` | 在 `autoUpload=false` 时开始上传全部待上传条目 |
| `cancel(id)` | 取消排队中或上传中的条目；条目保留以便重试 |

## 样式与动效

优先使用 `themeColor` 设置统一强调色。它会驱动默认触发器、拖入状态、文件标记、进度条、操作按钮、预览关闭按钮和裁剪确认按钮；错误状态仍使用危险色，避免弱化错误语义。

```vue
<ShowtimeUpload v-model="files" theme-color="#1677ff" />
```

未传 `themeColor` 时，组件可通过父级容器覆盖以下 CSS 变量：

```css
.asset-uploader {
  --showtime-upload-accent: #1677ff;
  --showtime-upload-border: #d5dce3;
  --showtime-upload-radius: 6px;
  --showtime-upload-trigger-background: #ffffff;
  --showtime-upload-trigger-hover: #fff4f5;
  --showtime-upload-item-background: #ffffff;
  --showtime-upload-dialog-radius: 8px;
}
```

条目进入、移除、进度和弹层动画使用共享的 `--showtime-duration-*`、`--showtime-ease-*` 变量。系统启用“减少动态效果”时，组件会自动将动画和过渡缩短到最小值。
