# 文本与媒体组件

## `<text>`

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| className | string | - | 样式类名 |
| style | CSSProperties | - | 内联样式 |
| textMaxline | string | - | 最大行数，超出省略号 |
| tailColorConvert | boolean | false | 省略号颜色跟随外层 text 样式 |
| textSingleLineVerticalAlign | `'normal' \| 'bottom' \| 'center' \| 'top'` | `'normal'` | 单行垂直对齐，inline 文本不支持 |
| textSelection | boolean | false | 是否可选择文本 |
| customContextMenu | boolean | false | 开启文本选择后使用自定义菜单 |
| onClick | (e: TouchEvent) => void | - | 点击 |
| onLayout | (e: LayoutEvent) => void | - | 布局完成 |
| onSelectionChange | (e: SelectionChangeEvent) => void | - | 文本选区变化（需开启 textSelection） |

```tsx
<text textMaxline="2">超出两行显示省略号的长文本...</text>
<text textSelection>可以被用户选择复制的文本</text>
```

---

## `<image>`

**必须设置非空 src，且满足以下任意一条，否则图片不显示：**
- 设置 width + height > 0
- 设置 `autoSize`

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| src | string | - | **必填**，图片地址（http/https/base64） |
| mode | `'scaleToFill' \| 'aspectFit' \| 'aspectFill' \| 'center'` | `'scaleToFill'` | 缩放模式 |
| placeholder | string | - | 占位图，用法同 src |
| autoSize | boolean | false | 加载成功后自动调整为原图尺寸 |
| flatten | boolean | true | false 时走 Lynx 自定义解码链路，可缓解 Android 大图降采样 |
| subsample | boolean | true | 是否启用降采样，仅在 `flatten=false` 时可设为 false |
| onLoad | (e: LoadEvent) => void | - | 加载成功，事件包含宽高 |
| onError | (e: ErrorEvent) => void | - | 加载失败 |
| onClick | (e: TouchEvent) => void | - | 点击 |

**mode 说明：**

| 值 | 说明 |
|----|------|
| scaleToFill | 拉伸填满，不保持比例 |
| aspectFit | 保持比例，长边完全显示 |
| aspectFill | 保持比例，短边完全显示（常用于头像/封面） |
| center | 不缩放，只显示中间区域 |

**Android 大图说明：** `flatten={false}` 走 Lynx 解码链路，可缓解 Fresco 默认纹理限制导致的超大图片降采样。`subsample={false}` 关闭降采样，超大图存在 OOM 风险，谨慎使用。

```tsx
<image
  src="https://example.com/photo.jpg"
  mode="aspectFill"
  style={{ width: '200px', height: '200px', borderRadius: '50%' }}
  onLoad={(e) => console.log('尺寸:', e.detail.width, e.detail.height)}
/>
```

---

## `<svg>`

| 属性 | 类型 | 说明 |
|------|------|------|
| className | string | 样式类名 |
| style | CSSProperties | 内联样式 |

SVG 内容作为子节点传入，使用标准 SVG 元素。

---

## `<video>`

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| ref | ForwardedRef\<VideoRef\> | - | 实例 ref，可调用播放控制方法 |
| playUrl | string | - | 播放地址 |
| poster | string | - | 海报图 |
| autoPlay | boolean | false | 资源就绪后自动播放 |
| mute | boolean | true | 静音 |
| loop | boolean | false | 循环 |
| objectfit | `'aspectFit' \| 'aspectFill' \| 'scaleToFill'` | `'aspectFit'` | 缩放模式 |
| style | CSSProperties | - | 设置宽高和圆角 |

### VideoRef 方法

| 方法 | 说明 |
|------|------|
| play(callback?) | 播放 |
| pause(callback?) | 暂停 |
| stop(callback?) | 停止 |
| seek(position, play, callback?) | 跳转到指定时间 |
| replay(play, callback?) | 重新播放 |

```tsx
import type { VideoRef } from '@doubao-dev/framework/components';
import { useRef } from '@doubao-dev/framework';

const videoRef = useRef<VideoRef>(null);

<video
  ref={videoRef}
  playUrl="https://example.com/video.mp4"
  poster="https://example.com/poster.jpg"
  objectfit="aspectFill"
  style={{ width: '100%', height: '220px', borderRadius: '12px' }}
/>
<button onClick={() => videoRef.current?.play()}>播放</button>
<button onClick={() => videoRef.current?.pause()}>暂停</button>
```

---

## `<long-image>`

长图分段解码，提供两种用法。

### 简单用法（内置滚动容器）

```tsx
<long-image
  src="https://example.com/long.jpg"
  style={{ height: '70vh' }}
  onStateChange={(state) => {
    // state: 'measuring' | 'loading-meta' | 'ready' | 'error'
    console.log(state);
  }}
/>
```

| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| src | string | - | **必填** |
| id | string | 自动生成 | 根节点 id |
| reloadToken | number | 0 | 变化时重新初始化 |
| segmentDisplayHeight | number | 600 | 单段目标高度 (px) |
| decodeOverscanScreens | number | 1 | 预加载屏数 |
| maxActiveSegments | number | 8 | 同时激活解码上限 |
| placeholderColor | string | 'transparent' | 非激活段占位背景色 |
| style | CSSProperties | - | 未设 height 时默认 100vh |
| onStateChange | (state: LongImageState) => void | - | 状态回调 |
| onMetricsChange | (metrics: LongImageMetrics) => void | - | 指标回调 |

### Hook 用法（接入外部滚动容器）

```tsx
import { useLongImage, renderLongImageSegments } from '@doubao-dev/framework/components';

const longImageResult = useLongImage({ src });

<view id={longImageResult.containerId} style={{ height: '70vh' }} onLayoutChange={longImageResult.handleContainerLayoutChange}>
  <list style={{ width: '100%', height: '100%' }} onScroll={longImageResult.handleScroll}>
    {renderLongImageSegments(longImageResult, { mode: 'list' })}
  </list>
</view>
```

**⚠️ 长图前有 header 时必须修正 scrollTop：**

```tsx
const HEADER_HEIGHT = 120;
const handlePageScroll = (event) => {
  longImageResult.handleScroll({
    ...event,
    detail: { ...event.detail, scrollTop: Math.max(0, event.detail.scrollTop - HEADER_HEIGHT) }
  });
};
```

`handleScroll` 期望接收的 scrollTop 语义是"可视区域顶部相对于长图内容顶部的偏移"，如果长图前有其他内容，外层 scrollTop 需减去该内容高度。

---

## `<web-view>`

Web 模拟器中的 `<web-view>` 通过本地代理承载 H5 页面，页面脚本读取到的
`location.origin` / `location.host` 可能是调试器本地域名，而不是真机 WebView 中的业务域名。
如果 H5 依赖 `location.origin` 或 `location.host` 判断测试环境、线上环境、白名单或鉴权逻辑，
Web 模拟器结果可能与真机不一致，请使用真机调试确认。

| 属性 | 类型 | 说明 |
|------|------|------|
| src | string | 网页地址 |
| onLoad | (event: { src: string }) => void | 加载成功 |
| onError | (event: { errorMsg: string; errorCode: number; url: string }) => void | 加载失败 |
| onMessage | (event: { data: unknown }) => void | 网页向应用 postMessage |
| ref | Ref&lt;WebViewRef&gt; | 获取 WebView 实例，可调用 `postMessage(data)` 向网页发送消息 |

```tsx
import { useRef } from '@doubao-dev/framework';
import type { WebViewRef } from '@doubao-dev/framework/components';

const webViewRef = useRef<WebViewRef>(null);

<>
  <web-view
    ref={webViewRef}
    src="https://example.com"
    onLoad={(event) => console.log('网页加载成功:', event.src)}
    onError={(event) => console.log('加载错误:', event.errorMsg, event.url)}
    onMessage={(event) => console.log('收到消息:', event.data)}
  />
  <button text="向网页发送消息" onClick={() => webViewRef.current?.postMessage({ type: 'ping' })} />
</>
```
