@pnt-team/pnt-component-frontend 0.0.88 → 0.0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cjs/index.js CHANGED
@@ -31,14 +31,14 @@ var src_exports = {};
31
31
  __export(src_exports, {
32
32
  PntBusinessSelect: () => import_PntBusinessSelect.default,
33
33
  PntGeneralLayout: () => import_PntGeneralLayout.default,
34
- PntImagePreview: () => import_PntImagePreview.default,
34
+ PntImage: () => import_PntImage.default,
35
35
  PntProvider: () => import_config.PntProvider,
36
36
  PntSecureBox: () => import_PntSecureBox.default,
37
37
  usePntConfig: () => import_config.usePntConfig
38
38
  });
39
39
  module.exports = __toCommonJS(src_exports);
40
40
  var import_config = require("./config");
41
- var import_PntImagePreview = __toESM(require("./components/PntImagePreview"));
41
+ var import_PntImage = __toESM(require("./components/PntImage"));
42
42
  var import_PntBusinessSelect = __toESM(require("./components/PntBusinessSelect"));
43
43
  var import_PntSecureBox = __toESM(require("./components/PntSecureBox"));
44
44
  var import_PntGeneralLayout = __toESM(require("./components/PntGeneralLayout"));
@@ -46,7 +46,7 @@ var import_PntGeneralLayout = __toESM(require("./components/PntGeneralLayout"));
46
46
  0 && (module.exports = {
47
47
  PntBusinessSelect,
48
48
  PntGeneralLayout,
49
- PntImagePreview,
49
+ PntImage,
50
50
  PntProvider,
51
51
  PntSecureBox,
52
52
  usePntConfig
@@ -153,8 +153,6 @@ const PntBusinessSelect = props => {
153
153
  // append=true 为滚动翻页(追加),append=false 为初始化/搜索/重置(替换)
154
154
  const fetchData = useCallback(async (kw, page, append) => {
155
155
  if (append) {
156
- // 任何请求进行中都不再追加;并校验关键词/页码/总数,
157
- // 防止搜索后残余滚动事件用新关键词 + 旧页码请求错误分页
158
156
  if (appendLoadingRef.current || loadingRef.current) return;
159
157
  const meta = listMetaRef.current;
160
158
  if (meta.keyword !== kw) return;
@@ -162,26 +160,21 @@ const PntBusinessSelect = props => {
162
160
  if (optionsRef.current.length >= meta.total) return;
163
161
  appendLoadingRef.current = true;
164
162
  }
165
- // 替换请求不被在途请求阻塞,靠序号让旧响应失效
166
163
  const seq = ++fetchSeqRef.current;
167
164
  loadingRef.current = true;
168
165
  setLoading(true);
169
166
  try {
170
167
  const res = await requestFn(buildParams(kw, page));
171
- // 已有更新的替换请求发出,本次(可能是旧关键词的翻页)结果丢弃
172
168
  if (seq !== fetchSeqRef.current) return;
173
- // 适配 commonEesType:code === 0 视为成功
174
169
  if (res.code === 0 && res.data) {
175
170
  const newList = res.data.list || [];
176
171
  const resTotal = res.data.pagination?.total || 0;
177
172
  let nextList;
178
173
  if (append) {
179
- // 按 game_id 去重,避免重复页混入
180
174
  const existed = new Set(optionsRef.current.map(o => o.game_id));
181
175
  nextList = [...optionsRef.current, ...newList.filter(o => !existed.has(o.game_id))];
182
176
  } else {
183
177
  nextList = newList;
184
- // 列表整体替换,屏蔽随后浏览器 clamp 滚动位置触发的残余触底
185
178
  lastReplaceAtRef.current = Date.now();
186
179
  }
187
180
  optionsRef.current = nextList;
@@ -193,6 +186,8 @@ const PntBusinessSelect = props => {
193
186
  setOptions(nextList);
194
187
  onLoadedRef.current?.(nextList);
195
188
  }
189
+ } catch {
190
+ // 请求失败静默处理,不阻塞主流程
196
191
  } finally {
197
192
  if (append) appendLoadingRef.current = false;
198
193
  if (seq === fetchSeqRef.current) {
@@ -207,7 +202,6 @@ const PntBusinessSelect = props => {
207
202
  keywordRef.current = val;
208
203
  if (debounceRef.current) clearTimeout(debounceRef.current);
209
204
  debounceRef.current = setTimeout(() => {
210
- // 防抖结束时以最新输入为准(请求序号会使期间的旧翻页响应失效)
211
205
  fetchData(keywordRef.current, 1, false);
212
206
  }, searchDebounce);
213
207
  }, [fetchData, searchDebounce]);
@@ -224,8 +218,14 @@ const PntBusinessSelect = props => {
224
218
  const handlePopupVisibleChange = useCallback(visible => {
225
219
  if (visible) return;
226
220
  if (debounceRef.current) clearTimeout(debounceRef.current);
227
- if (keywordRef.current) {
228
- keywordRef.current = '';
221
+ // 必须以「当前列表实际使用的关键词」为准,不能只看 keywordRef:
222
+ // 1) 单选选中一项后,TDesign 关闭弹窗时会先以 blur 触发 onSearch(''),
223
+ // 把 keywordRef 提前清空并挂一个重置 timer,随后本回调又会清掉该 timer,
224
+ // 若仅按 keywordRef 判断就会漏发重置请求;
225
+ // 2) 用户手动清空输入但未关闭下拉时,keywordRef 已为 '',但列表仍残留
226
+ // 上次搜索结果,关闭时必须重置回默认列表。
227
+ keywordRef.current = '';
228
+ if (listMetaRef.current.keyword) {
229
229
  fetchData('', 1, false);
230
230
  }
231
231
  }, [fetchData]);
@@ -429,11 +429,11 @@ const PntBusinessSelect = props => {
429
429
  // 兜底:从 TDesign 上下文剥离 React 字段(children/content)
430
430
  const fromCtx = ctxOptions.find(o => String(o.value) === id);
431
431
  if (fromCtx) {
432
- const {
433
- children,
434
- content,
435
- ...rest
436
- } = fromCtx;
432
+ const rest = {
433
+ ...fromCtx
434
+ };
435
+ delete rest.children;
436
+ delete rest.content;
437
437
  return rest;
438
438
  }
439
439
  return {
@@ -446,15 +446,23 @@ const PntBusinessSelect = props => {
446
446
  selectedOptions: multiple ? selectedOptions : selectedOptions.slice(0, 1)
447
447
  });
448
448
  }, [onChange, multiple, getLabelText]);
449
+
450
+ // 点击 clear 按钮(X)时重置列表——TDesign 的 onClearValue 只 setInputValue,
451
+ // 不触发 handleInputChange / onSearch,所以需要主动重置。
452
+ const handleClear = useCallback(() => {
453
+ keywordRef.current = '';
454
+ if (debounceRef.current) clearTimeout(debounceRef.current);
455
+ if (listMetaRef.current.keyword) {
456
+ fetchData('', 1, false);
457
+ }
458
+ }, [fetchData]);
449
459
  return /*#__PURE__*/_jsx(Select, {
450
460
  value: selectValue,
451
461
  onChange: handleChange,
452
- filterable: filterable
453
- // 远程搜索模式由服务端过滤(传了 onSearch 时 TDesign 也会跳过本地过滤);
454
- // 仅全量模式使用本地过滤
455
- ,
462
+ filterable: filterable,
456
463
  filter: enableRemoteSearch ? undefined : filterable ? localFilter : undefined,
457
464
  onSearch: enableRemoteSearch ? handleSearch : undefined,
465
+ onClear: enableRemoteSearch ? handleClear : undefined,
458
466
  loading: loading && options.length === 0,
459
467
  multiple: multiple,
460
468
  max: max,
@@ -19,6 +19,11 @@ interface PntGeneralLayoutProps {
19
19
  * 返回按钮点击回调
20
20
  */
21
21
  onBackButtonClick?: (() => void) | undefined;
22
+ /**
23
+ * 是否显示头部底部分割线
24
+ * @default true
25
+ */
26
+ headerBordered?: boolean;
22
27
  /**
23
28
  * 子头部内容
24
29
  * @description 位于 Header 下方,常用于放置面包屑、Tab 等
@@ -41,6 +46,25 @@ interface PntGeneralLayoutProps {
41
46
  * 内容区自定义样式
42
47
  */
43
48
  bodyStyle?: React.CSSProperties;
49
+ /**
50
+ * 内容区内边距
51
+ * @description 数字按 px 处理;需要全出血内容时可传 '24px 0'
52
+ * @default 24
53
+ */
54
+ bodyPadding?: number | string;
55
+ /**
56
+ * 内容区卡片圆角(px)
57
+ * @description 作用于 body 内 t-card / tea-card / 各业务 wrapper、block 容器
58
+ * @default 4
59
+ */
60
+ cardRadius?: number;
61
+ /**
62
+ * 底部按钮样式模式
63
+ * @description fixed-统一下按钮尺寸(高 36px、最小宽 92px、加粗中号字);
64
+ * auto-不约束按钮尺寸,仅保留按钮间距
65
+ * @default 'fixed'
66
+ */
67
+ footerButton?: 'fixed' | 'auto';
44
68
  /**
45
69
  * 底部内容
46
70
  * @description 传入后将渲染 Layout Footer 区域,常用于放置操作按钮
@@ -50,7 +74,8 @@ interface PntGeneralLayoutProps {
50
74
  /**
51
75
  * 通用页面布局组件(L3 布局组件)
52
76
  * @description 提供标准的「头部 + 子头部 + 内容区 + 底部」页面布局结构,
53
- * 基于 TDesign Layout 封装,适用于后台管理系统的详情页、表单页等场景。
77
+ * 基于 TDesign Layout(flex column)封装,内容区自动填满剩余高度,
78
+ * 适用于后台管理系统的列表页、详情页、表单页等场景。
54
79
  *
55
80
  * @example
56
81
  * ```tsx
@@ -76,6 +101,13 @@ interface PntGeneralLayoutProps {
76
101
  * >
77
102
  * <Detail />
78
103
  * </PntGeneralLayout>
104
+ *
105
+ * // 全出血内容区(无头部边线、内容无左右内边距、底部按钮不强制尺寸)
106
+ * <PntGeneralLayout
107
+ * headerBordered={false}
108
+ * bodyPadding="24px 0"
109
+ * footerButton="auto"
110
+ * />
79
111
  * ```
80
112
  */
81
113
  declare const PntGeneralLayout: FunctionComponent<PntGeneralLayoutProps>;
@@ -7,7 +7,8 @@ import { jsxs as _jsxs } from "react/jsx-runtime";
7
7
  /**
8
8
  * 通用页面布局组件(L3 布局组件)
9
9
  * @description 提供标准的「头部 + 子头部 + 内容区 + 底部」页面布局结构,
10
- * 基于 TDesign Layout 封装,适用于后台管理系统的详情页、表单页等场景。
10
+ * 基于 TDesign Layout(flex column)封装,内容区自动填满剩余高度,
11
+ * 适用于后台管理系统的列表页、详情页、表单页等场景。
11
12
  *
12
13
  * @example
13
14
  * ```tsx
@@ -33,6 +34,13 @@ import { jsxs as _jsxs } from "react/jsx-runtime";
33
34
  * >
34
35
  * <Detail />
35
36
  * </PntGeneralLayout>
37
+ *
38
+ * // 全出血内容区(无头部边线、内容无左右内边距、底部按钮不强制尺寸)
39
+ * <PntGeneralLayout
40
+ * headerBordered={false}
41
+ * bodyPadding="24px 0"
42
+ * footerButton="auto"
43
+ * />
36
44
  * ```
37
45
  */
38
46
  const PntGeneralLayout = prop => {
@@ -41,19 +49,29 @@ const PntGeneralLayout = prop => {
41
49
  headerTitle,
42
50
  showBackButton = false,
43
51
  onBackButtonClick,
52
+ headerBordered = true,
44
53
  subHeaderContent,
45
54
  operation,
46
55
  bodyRef,
47
56
  children,
48
57
  bodyStyle,
58
+ bodyPadding = 24,
59
+ cardRadius = 4,
60
+ footerButton = 'fixed',
49
61
  footer
50
62
  } = prop;
51
63
  const {
52
64
  Header,
53
65
  Footer
54
66
  } = Layout;
67
+ const layoutClassName = ['pnt-general-layout', !headerBordered && 'pnt-general-layout--borderless', footerButton === 'auto' && 'pnt-general-layout--footer-auto', className].filter(Boolean).join(' ');
68
+ const cssVars = {
69
+ '--pnt-layout-body-padding': typeof bodyPadding === 'number' ? `${bodyPadding}px` : bodyPadding,
70
+ '--pnt-layout-card-radius': `${cardRadius}px`
71
+ };
55
72
  return /*#__PURE__*/_jsxs(Layout, {
56
- className: `pnt-general-layout${className ? ' ' + className : ''}`,
73
+ className: layoutClassName,
74
+ style: cssVars,
57
75
  children: [headerTitle && /*#__PURE__*/_jsxs(Header, {
58
76
  children: [/*#__PURE__*/_jsxs("span", {
59
77
  className: "header-title",
@@ -75,10 +93,7 @@ const PntGeneralLayout = prop => {
75
93
  }), /*#__PURE__*/_jsx("div", {
76
94
  className: "general-layout-body",
77
95
  ref: bodyRef,
78
- style: {
79
- height: `calc(100%${headerTitle && ' - 72px'}${footer && ' - 72px'})`,
80
- ...bodyStyle
81
- },
96
+ style: bodyStyle,
82
97
  children: children
83
98
  }), footer && /*#__PURE__*/_jsx(Footer, {
84
99
  children: footer
@@ -1,90 +1,121 @@
1
- .pnt-general-layout {
2
- height: 100%;
3
- background-color: rgba(0, 0, 0, 0);
4
-
5
- .t-layout__header {
6
- height: auto;
7
- display: flex;
8
- justify-content: space-between;
9
- border-bottom: 1px solid #f0f3f6;
10
-
11
- .header-title {
12
- display: inline-block;
13
- vertical-align: top;
14
- padding: 24px;
15
-
16
- .goback-icon {
17
- margin-right: 12px;
18
- vertical-align: top;
19
- color: #3f8cff;
20
- cursor: pointer;
21
- }
22
-
23
- .title {
24
- display: inline-block;
25
- vertical-align: top;
26
- font-weight: 600;
27
- font-size: 20px;
28
- height: 24px;
29
- line-height: 24px;
30
- }
31
- }
32
-
33
- .header-operation {
34
- height: 72px;
35
- line-height: 72px;
36
- padding-right: 24px;
37
-
38
- .t-button + .t-button {
39
- margin-left: 12px;
40
- }
41
- }
42
- }
43
-
44
- .general-layout-sub-header {
45
- border-bottom: 1px solid #f0f3f6;
46
- }
47
-
48
- .general-layout-body {
49
- height: 100%;
50
- padding: 24px;
51
- overflow-y: auto;
52
- background: #fafafa;
53
-
54
- > div.tea-card,
55
- > div.t-card,
56
- > div.t-loading__parent > div.t-card,
57
- > div[class*='-wrapper'],
58
- > div[class*='-block'] {
59
- box-shadow: none;
60
- margin-bottom: 24px;
61
- border-radius: 4px;
62
- }
63
-
64
- > div:last-child,
65
- > div:last-child.t-card,
66
- > div:last-child.tea-card,
67
- > div.t-loading__parent > div:last-child.t-card,
68
- > div:last-child[class*='-wrapper'],
69
- > div:last-child[class*='-block'] {
70
- margin-bottom: 0;
71
- }
72
- }
73
-
74
- .t-layout__footer {
75
- padding: 18px 24px;
76
- background: #fff;
77
- border-top: 1px solid #f0f3f6;
78
-
79
- .t-button {
80
- font-size: 14px;
81
- min-width: 92px;
82
- height: 36px;
83
- font-weight: 600;
84
-
85
- & + .t-button {
86
- margin-left: 12px;
87
- }
88
- }
89
- }
90
- }
1
+ .pnt-general-layout {
2
+ // 可通过 props(bodyPadding / cardRadius)覆盖的布局变量
3
+ --pnt-layout-body-padding: 24px;
4
+ --pnt-layout-card-radius: 4px;
5
+
6
+ height: 100%;
7
+ background-color: rgba(0, 0, 0, 0);
8
+
9
+ .t-layout__header {
10
+ height: auto;
11
+ flex-shrink: 0;
12
+ display: flex;
13
+ justify-content: space-between;
14
+ align-items: stretch;
15
+ border-bottom: 1px solid #f0f3f6;
16
+
17
+ .header-title {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ padding: 24px;
21
+
22
+ .goback-icon {
23
+ margin-right: 12px;
24
+ color: #3f8cff;
25
+ cursor: pointer;
26
+ }
27
+
28
+ .title {
29
+ font-weight: 600;
30
+ font-size: 20px;
31
+ line-height: 24px;
32
+ }
33
+ }
34
+
35
+ // 统一 flex 居中:高度由 header-title 的 padding 撑起(72px),
36
+ // 比固定 line-height: 72px 更稳,按钮高度变化也不会错位
37
+ .header-operation {
38
+ display: flex;
39
+ align-items: center;
40
+ padding-right: 24px;
41
+
42
+ .t-button + .t-button {
43
+ margin-left: 12px;
44
+ }
45
+ }
46
+ }
47
+
48
+ // 无头部分割线变体(headerBordered={false})
49
+ &--borderless {
50
+ .t-layout__header {
51
+ border-bottom: none;
52
+ }
53
+ }
54
+
55
+ .general-layout-sub-header {
56
+ flex-shrink: 0;
57
+ border-bottom: 1px solid #f0f3f6;
58
+ }
59
+
60
+ .general-layout-body {
61
+ // flex 自动填满 header/sub-header/footer 之外的剩余空间;
62
+ // min-height: 0 保证内容超长时由 body 自身滚动,而不是撑破布局
63
+ flex: 1 1 auto;
64
+ min-height: 0;
65
+ padding: var(--pnt-layout-body-padding);
66
+ overflow-y: auto;
67
+ background: #fafafa;
68
+
69
+ // 卡片/业务容器取各项目选择器超集,统一去阴影、间距与圆角
70
+ > div.tea-card,
71
+ > div.t-card,
72
+ > div.t-loading__parent > div.t-card,
73
+ > div[class*='-wrapper'],
74
+ > div[class*='-block'] {
75
+ box-shadow: none;
76
+ margin-bottom: 24px;
77
+ border-radius: var(--pnt-layout-card-radius);
78
+
79
+ .t-card__body {
80
+ border-radius: var(--pnt-layout-card-radius);
81
+ }
82
+ }
83
+
84
+ > div:last-child,
85
+ > div:last-child.t-card,
86
+ > div:last-child.tea-card,
87
+ > div.t-loading__parent > div:last-child.t-card,
88
+ > div:last-child[class*='-wrapper'],
89
+ > div:last-child[class*='-block'] {
90
+ margin-bottom: 0;
91
+ }
92
+ }
93
+
94
+ .t-layout__footer {
95
+ flex-shrink: 0;
96
+ padding: 18px 24px;
97
+ background: #fff;
98
+ border-top: 1px solid #f0f3f6;
99
+
100
+ .t-button {
101
+ font-size: 14px;
102
+ min-width: 92px;
103
+ height: 36px;
104
+ font-weight: 600;
105
+
106
+ & + .t-button {
107
+ margin-left: 12px;
108
+ }
109
+ }
110
+ }
111
+
112
+ // 底部按钮不约束尺寸变体(footerButton='auto'),仅保留按钮间距
113
+ &--footer-auto {
114
+ .t-layout__footer .t-button {
115
+ font-size: inherit;
116
+ min-width: 0;
117
+ height: auto;
118
+ font-weight: inherit;
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,87 @@
1
+ import React, { FC } from 'react';
2
+ import './index.scss';
3
+ /**
4
+ * PntImage 文案集合
5
+ * @description 所有内部展示文案均可通过 texts prop 自定义,用于多语言场景
6
+ */
7
+ export interface PntImageTexts {
8
+ /** 上传中占位文案(仅上传预览模式生效) */
9
+ uploading?: string;
10
+ /** 图片加载中占位文案(纯预览模式生效,透传给 tdesign Image loading) */
11
+ loading?: string;
12
+ /** 图片加载失败占位文案(纯预览模式生效,透传给 tdesign Image error) */
13
+ loadFailed?: string;
14
+ }
15
+ export interface PntImageProps {
16
+ /**
17
+ * 图片地址
18
+ * @description 图片的完整 URL 地址;为空则展示 defaultImg 或内置 uploadImg
19
+ */
20
+ src: string | undefined;
21
+ /**
22
+ * alt 文本
23
+ * @description 图片加载失败时的替代文本,纯预览模式必传以提升可访问性
24
+ */
25
+ alt?: string;
26
+ /**
27
+ * 默认图片
28
+ * @description src 为空时展示的占位图;不传则使用内置的 uploadImg
29
+ */
30
+ defaultImg?: string;
31
+ /**
32
+ * 自定义样式
33
+ * @description 一般用于控制图片宽高
34
+ */
35
+ style?: React.CSSProperties;
36
+ /**
37
+ * 上传中状态
38
+ * @description 当为 true 时显示上传中 loading 占位,阻止其他操作;仅在上传预览模式(传入 onRemove)生效
39
+ */
40
+ loading?: boolean;
41
+ /**
42
+ * 删除回调
43
+ * @description 传入即启用「上传预览模式」,mask 含预览 + 删除按钮;不传则为「纯预览模式」
44
+ * @param e - 事件对象
45
+ */
46
+ onRemove?: (e: any) => void;
47
+ /**
48
+ * attach 容器
49
+ * @description 弹窗场景下需要透传给 tdesign ImageViewer 的 attach,避免预览浮层被父容器裁剪
50
+ */
51
+ container?: HTMLElement | (() => HTMLElement);
52
+ /**
53
+ * 多语言文案
54
+ * @description 传入即覆盖默认中文文案;仅对应字段会覆盖,未传字段仍用默认值
55
+ */
56
+ texts?: PntImageTexts;
57
+ }
58
+ /**
59
+ * PntImage 业务图片组件
60
+ * @description 统一覆盖「上传图片预览」和「纯图片预览」两种业务场景,避免业务侧维护两套组件
61
+ *
62
+ * 模式判定:传入 `onRemove` 即为上传预览模式,否则为纯预览模式
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * // 上传预览模式
67
+ * <PntImage
68
+ * src={iconUrl}
69
+ * loading={loading}
70
+ * defaultImg={defaultImg}
71
+ * onRemove={handleRemove}
72
+ * style={{ width: 136, height: 136 }}
73
+ * />
74
+ *
75
+ * // 纯预览模式
76
+ * <PntImage
77
+ * src={url}
78
+ * alt="avatar"
79
+ * style={{ width: 60, height: 60 }}
80
+ * container={container}
81
+ * />
82
+ * ```
83
+ *
84
+ * @returns {ReactElement} 业务图片组件
85
+ */
86
+ declare const PntImage: FC<PntImageProps>;
87
+ export default PntImage;