# @pnt-team/pnt-component-frontend

PNT 前端业务组件库，基于 [dumi2](https://d.umijs.org/) + [father4](https://github.com/umijs/father) 构建，收敛 PNT 业务项目中高价值的重复组件与 Hook，实现单一来源维护。

> 当前版本：**0.0.91** · 组件清单：`PntImage` / `PntBusinessSelect` / `PntSecureBox` / `PntGeneralLayout`

## 安装

```bash
npm install @pnt-team/pnt-component-frontend
# 或
yarn add @pnt-team/pnt-component-frontend
```

### 依赖要求（peerDependencies）

| 依赖                  | 版本要求   | 说明                                   |
| --------------------- | ---------- | -------------------------------------- |
| `react`               | `>=16.9.0` | React 核心框架                         |
| `react-dom`           | `>=16.9.0` | React DOM 渲染                         |
| `tdesign-react`       | `>=1.5.0`  | UI 组件库（本组件库基于 TDesign 封装） |
| `tdesign-icons-react` | `>=0.2.0`  | TDesign 图标库                         |

> ⚠️ 请确保项目中已安装以上依赖，否则组件可能无法正常运行。

## 快速开始

使用前需在应用根入口用 `PntProvider` 注入 http 实例（消费方已配置好的 axios）：

```tsx
import { PntProvider } from '@pnt-team/pnt-component-frontend';
import http from '你的项目中的 axios 实例';

<PntProvider value={{ http }}>
  <App />
</PntProvider>;
```

```tsx
import {
  // L1 基础展示组件
  PntImage,
  // L2 业务复合组件
  PntBusinessSelect,
  PntSecureBox,
  // L3 布局组件
  PntGeneralLayout,
  // 配置
  PntProvider,
  usePntConfig,
} from '@pnt-team/pnt-component-frontend';
```

## 组件一览

### L1 基础展示组件

#### PntImage — 业务图片组件

统一的业务图片组件，单一组件通过 `onRemove` 是否传入自适应「上传预览」与「纯预览」两种模式，避免业务侧维护两套组件（原 UploadImageViewer + BasicImageViewer）。

**Props：**

| 属性         | 类型                                 | 必填 | 默认值                                                               | 说明                                                    |
| ------------ | ------------------------------------ | :--: | -------------------------------------------------------------------- | ------------------------------------------------------- |
| `src`        | `string \| undefined`                |  ✅  | -                                                                    | 图片 URL；为空则展示 defaultImg 或内置 uploadImg        |
| `alt`        | `string`                             |  ❌  | `''`                                                                 | alt 文本，纯预览模式建议传入以提升可访问性              |
| `defaultImg` | `string`                             |  ❌  | 内置 uploadImg                                                       | 默认图片（仅上传预览模式生效）                          |
| `size`       | `'small' \| 'medium' \| 'large'`     |  ❌  | `'large'`                                                            | 快捷正方形尺寸：small=36px、medium=60px、large=136px    |
| `style`      | `React.CSSProperties`                |  ❌  | -                                                                    | 自定义样式；width/height 优先级高于 size                |
| `loading`    | `boolean`                            |  ❌  | `false`                                                              | 上传中状态（仅上传预览模式生效）                        |
| `onRemove`   | `(e: any) => void`                   |  ❌  | -                                                                    | 删除回调（传入即启用上传预览模式）                      |
| `container`  | `HTMLElement \| (() => HTMLElement)` |  ❌  | -                                                                    | attach 容器（弹窗场景需要，透传给 tdesign ImageViewer） |
| `texts`      | `PntImageTexts`                      |  ❌  | `{ uploading: '上传中', loading: '加载中', loadFailed: '加载失败' }` | 多语言文案，传入字段覆盖默认中文                        |

**模式判定规则：**

| 模式         | 触发条件        | mask 行为                | loading 占位            | defaultImg | error 占位 |
| ------------ | --------------- | ------------------------ | ----------------------- | :--------: | ---------- |
| 上传预览模式 | 传入 `onRemove` | 预览按钮 + 分隔线 + 删除 | "上传中"（LoadingIcon） |     ✅     | 不显示     |
| 纯预览模式   | 不传 `onRemove` | 半透明遮罩 + 点击预览    | "加载中"（ImageIcon）   |     ❌     | "加载失败" |

**示例：**

```tsx
// 上传预览模式
<PntImage
  src={iconUrl}
  loading={uploading}
  defaultImg="/path/to/default.png"
  onRemove={handleRemove}
  size="large"
/>

// 纯预览模式
<PntImage
  src={url}
  alt="avatar"
  size="medium"
  container={dialogContainer}
/>

// 多语言
<PntImage
  src={url}
  alt="avatar"
  size="medium"
  texts={{ loading: 'Loading', loadFailed: 'Failed' }}
/>
```

---

### L2 业务复合组件

#### PntBusinessSelect — 业务选择器组件

统一业务下拉选择器，内置对接后端通用接口 `/api/business/select/list`，支持远程搜索、滚动分页加载、多选、场景化禁用（合规场景 `creatable` 字段）。

替代此前 5 个分散的业务 select 接口（`game/list`、`game/list_option`、`compliance-age/game/list`、`biz-list-with-creatable-state`、`sdk_customize/game_list`）。

**Props：**

| 属性               | 类型                                                        | 必填 | 默认值      | 说明                                                                                |
| ------------------ | ----------------------------------------------------------- | :--: | ----------- | ----------------------------------------------------------------------------------- |
| `value`            | `string \| string[]`                                        |  ❌  | -           | 当前选中值；单选为 string，多选为 string[]                                          |
| `onChange`         | `(value, context) => void`                                  |  ❌  | -           | 值变化回调；context.selectedOptions 返回纯业务数据对象                              |
| `multiple`         | `boolean`                                                   |  ❌  | `false`     | 是否多选                                                                            |
| `scene`            | `'default' \| 'sdk' \| 'compliance_age' \| 'custom_region'` |  ❌  | `'default'` | 下拉场景；后端按场景返回默认字段集                                                  |
| `statuses`         | `number[]`                                                  |  ❌  | -           | 业务状态筛选（3-线上运营, 4-接入中, 5-中途结项, 6-已下线）                          |
| `businessTypes`    | `number[]`                                                  |  ❌  | -           | 业务类型筛选（1-正式业务, 2-测试业务）                                              |
| `labelFormat`      | `'name' \| 'name_en' \| 'name_id' \| 'name_en_id'`          |  ❌  | `'name'`    | 标签格式                                                                            |
| `labelField`       | `string`                                                    |  ❌  | -           | 自定义 label 字段（消费方接口返回字段非标准 name/name_en 时使用）                   |
| `optionLabel`      | `(item) => ReactNode`                                       |  ❌  | -           | 自定义选项 label 渲染；优先级高于 labelFormat/labelField                            |
| `placeholder`      | `string`                                                    |  ❌  | -           | 占位提示文案                                                                        |
| `disabled`         | `boolean`                                                   |  ❌  | `false`     | 是否禁用整个选择器                                                                  |
| `clearable`        | `boolean`                                                   |  ❌  | `true`      | 是否可清除                                                                          |
| `filterable`       | `boolean`                                                   |  ❌  | `true`      | 是否可远程搜索                                                                      |
| `max`              | `number`                                                    |  ❌  | -           | 多选时最大可选数量                                                                  |
| `pageSize`         | `number`                                                    |  ❌  | `20`        | 每页条数                                                                            |
| `searchDebounce`   | `number`                                                    |  ❌  | `400`       | 搜索防抖延迟（毫秒）                                                                |
| `minCollapsedNum`  | `number`                                                    |  ❌  | -           | 多选时最少折叠数量；透传至 TDesign Select                                           |
| `collapsedItems`   | `TdSelectProps['collapsedItems']`                           |  ❌  | -           | 折叠项自定义渲染；透传至 TDesign Select                                             |
| `popupProps`       | `Partial<TdSelectProps['popupProps']>`                      |  ❌  | -           | 弹层属性透传；与内部 onScrollToBottom 合并；常用 attach、overlayClassName           |
| `fields`           | `string[]`                                                  |  ❌  | -           | 字段白名单，传给后端 fields 参数；未传时后端按 scene 返回默认字段                   |
| `pagination`       | `boolean`                                                   |  ❌  | `true`      | 是否分页；false 时一次性加载全量数据，不远程搜索                                    |
| `onLoaded`         | `(options) => void`                                         |  ❌  | -           | 选项加载完成回调；不分页模式下首屏即全量，可复用此数据避免重复请求                  |
| `optionDisabled`   | `(item) => boolean`                                         |  ❌  | -           | 自定义选项禁用逻辑；与合规场景 creatable 判断叠加（任一为 true 即禁用）             |
| `optionTooltip`    | `(item) => string`                                          |  ❌  | -           | 自定义选项 tooltip 内容；返回非空字符串时该选项悬停显示 tooltip，常用于解释禁用原因 |
| `tooltipPlacement` | `'top' \| 'bottom' \| 'left' \| 'right'`                    |  ❌  | `'top'`     | tooltip 位置                                                                        |
| `className`        | `string`                                                    |  ❌  | -           | 自定义类名                                                                          |
| `style`            | `React.CSSProperties`                                       |  ❌  | -           | 自定义样式                                                                          |

**示例：**

```tsx
import { PntBusinessSelect } from '@pnt-team/pnt-component-frontend';

<PntBusinessSelect
  scene="compliance_age"
  labelFormat="name_id"
  placeholder="请选择业务"
  value={gameId}
  onChange={(v) => setGameId(v as string)}
/>;

// 多选 + 状态过滤
<PntBusinessSelect
  multiple
  statuses={[3, 4]}
  labelFormat="name_id"
  value={gameIds}
  onChange={(v) => setGameIds(v as string[])}
/>;
```

---

#### PntSecureBox — 高敏信息展示组件

统一处理和展示高敏信息，支持脱敏显示和按需获取真实数据。通过 `PntProvider` 注入的 http 实例请求解锁敏感数据。

**Props：**

| 属性        | 类型                                   | 必填 | 默认值              | 说明                                                         |
| ----------- | -------------------------------------- | :--: | ------------------- | ------------------------------------------------------------ |
| `dataKey`   | `string`                               |  ✅  | -                   | 高敏信息的唯一标识 key，用于缓存真实值                       |
| `dataValue` | `string`                               |  ✅  | -                   | 初始显示值，通常为脱敏后的数据（含 `_high_security_:` 前缀） |
| `env`       | `string \| number`                     |  ✅  | -                   | 环境标识（如 dev / test / production）                       |
| `gameId`    | `string \| number`                     |  ✅  | -                   | 游戏 / 业务 ID                                               |
| `renderDom` | `(content: string) => React.ReactNode` |  ✅  | -                   | 自定义渲染函数，参数为当前显示内容（真实值或脱敏值）         |
| `viewText`  | `string`                               |  ❌  | `'View'`            | 查看按钮文案                                                 |
| `pageId`    | `string`                               |  ❌  | `location.pathname` | 页面 ID，用于操作日志和权限验证                              |

**示例：**

```tsx
import { PntSecureBox } from '@pnt-team/pnt-component-frontend';

<PntSecureBox
  dataKey="userPhone"
  dataValue="_high_security_:AAAA...:6542"
  env={2}
  gameId={12345}
  renderDom={(content) => <span>{content}</span>}
  viewText="查看"
/>;
```

**非展示场景（查看 → 复制 / 批量解锁拼装）请用 `useHighSecurity`：**

```tsx
import { useHighSecurity } from '@pnt-team/pnt-component-frontend';

const { securityData, isUnlocked, requestSecurityData, clearSecurityData } =
  useHighSecurity({ gameId, env });

// 第一次点击解锁、第二次点击复制；切换业务时 clearSecurityData()
const res = await requestSecurityData({
  gameId,
  env,
  param: [{ key: 'SDK_KEY', value: cipher }],
});
if (res.length) navigator.clipboard.writeText(securityData.SDK_KEY);
```

- 模块级缓存按 `gameId + env + key` 复合键隔离，同页多实例共享，同一 key 同页只请求一次；在途并发自动去重
- 解锁失败返回 `[]` 且不抛错；`env` 必填
- 另有 `isSecurityData(value)` 导出，用于判断 `_high_security_:` 密文

---

### L3 布局组件

#### PntGeneralLayout — 通用页面布局组件

提供标准的「头部 + 子头部 + 内容区 + 底部」页面布局结构，基于 TDesign Layout（flex column）封装，内容区自动填满剩余高度并独立滚动，适用于后台管理系统的列表页、详情页、表单页等场景。

**Props：**

| 属性                | 类型                  | 必填 | 默认值    | 说明                                                                               |
| ------------------- | --------------------- | :--: | --------- | ---------------------------------------------------------------------------------- |
| `className`         | `string`              |  ❌  | -         | 自定义类名，追加到根元素，常用于页面级样式覆写                                     |
| `headerTitle`       | `ReactNode`           |  ❌  | -         | 头部标题内容，传入后渲染 Header 区域                                               |
| `showBackButton`    | `boolean`             |  ❌  | `false`   | 是否显示返回按钮                                                                   |
| `onBackButtonClick` | `() => void`          |  ❌  | -         | 返回按钮点击回调                                                                   |
| `headerBordered`    | `boolean`             |  ❌  | `true`    | 是否显示头部底部分割线                                                             |
| `subHeaderContent`  | `ReactNode`           |  ❌  | -         | 子头部内容，位于 Header 下方                                                       |
| `operation`         | `ReactNode`           |  ❌  | -         | 头部右侧操作区，常用于放置按钮组                                                   |
| `bodyRef`           | `Ref<HTMLDivElement>` |  ❌  | -         | 内容区 ref 引用                                                                    |
| `bodyStyle`         | `CSSProperties`       |  ❌  | -         | 内容区自定义样式（内边距、圆角建议优先使用 bodyPadding / cardRadius）              |
| `bodyPadding`       | `number \| string`    |  ❌  | `24`      | 内容区内边距；数字按 px 处理，全出血内容可传 `'24px 0'`                            |
| `cardRadius`        | `number`              |  ❌  | `4`       | 内容区卡片圆角（px），联动 t-card 及其 body                                        |
| `footerButton`      | `'fixed' \| 'auto'`   |  ❌  | `'fixed'` | fixed：底部按钮统一 36px 高 / 92px 最小宽 / 加粗；auto：不约束按钮尺寸，仅保留间距 |
| `footer`            | `ReactNode`           |  ❌  | -         | 底部内容，传入后渲染 Footer 区域                                                   |
| `children`          | `ReactNode`           |  ❌  | -         | 页面主体内容                                                                       |

**示例：**

```tsx
// 基础用法：仅内容区
<PntGeneralLayout>
  <div>页面内容</div>
</PntGeneralLayout>

// 带标题和操作按钮
<PntGeneralLayout
  headerTitle="业务配置"
  operation={
    <Space>
      <Button theme="primary">保存</Button>
      <Button variant="outline">取消</Button>
    </Space>
  }
>
  <Form />
</PntGeneralLayout>

// 带返回按钮和底部操作栏
<PntGeneralLayout
  headerTitle="详情页"
  showBackButton
  onBackButtonClick={() => history.back()}
  footer={
    <Space>
      <Button theme="primary">提交</Button>
      <Button variant="outline">重置</Button>
    </Space>
  }
>
  <Detail />
</PntGeneralLayout>

// 适配项目风格（无头部边线 / 全出血内容 / 大圆角卡片 / 底部按钮自适应）
<PntGeneralLayout
  headerBordered={false}
  bodyPadding="24px 0"
  cardRadius={8}
  footerButton="auto"
/>
```

---

## 本地开发

```bash
# 克隆仓库 & 安装依赖
git clone <repo-url>
cd pnt-component-frontend
yarn install

# 启动文档开发服务器
yarn start

# 构建组件库（lib/ 目录）
yarn build

# 运行测试
yarn test
```

## 发布流程

1. 推送代码到 `master` 分支
2. 触发 Coding 自动构建流程
3. 自动发布到 npm 制品库：[点击查看](https://ptc.coding.intlgame.com/p/player-network/artifacts/158595/npm/packages)
4. 自动发布组件文档到测试机
5. 组件文档预览：http://component.pnt-dev.intlgame.com/

## 技术栈

| 工具                                               | 用途                          |
| -------------------------------------------------- | ----------------------------- |
| [father4](https://github.com/umijs/father)         | 组件库构建（ESM / CJS / UMD） |
| [dumi2](https://d.umijs.org/)                      | 文档生成与开发调试            |
| [TDesign React](https://tdesign.tencent.com/react) | UI 组件库（peerDependency）   |
| TypeScript                                         | 类型安全                      |
| Sass                                               | 样式方案                      |
| Jest                                               | 单元测试                      |

## 相关资源

- [Git 仓库](https://git.tencent.com/pnt-web/playernetwork/frontend/pnt-component-frontend/tree/test)
- [dumi2 官方文档](https://d.umijs.org/guide)
- [father 官方文档](https://github.com/umijs/father/blob/master/docs/config.md)
