@pnt-team/pnt-component-frontend 0.0.110 → 0.0.113

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.
Files changed (30) hide show
  1. package/README.md +29 -30
  2. package/lib/cjs/api/index.d.ts +14 -1
  3. package/lib/cjs/api/index.js +6 -0
  4. package/lib/cjs/api/types/index.d.ts +47 -0
  5. package/lib/cjs/components/PntRegionSelect/ImportDialog.d.ts +25 -0
  6. package/lib/cjs/components/PntRegionSelect/ImportDialog.js +244 -0
  7. package/lib/cjs/components/PntRegionSelect/index.d.ts +43 -0
  8. package/lib/cjs/components/PntRegionSelect/index.js +402 -0
  9. package/lib/cjs/components/PntRegionSelect/index.scss +166 -0
  10. package/lib/cjs/components/PntRegionSelect/types.d.ts +158 -0
  11. package/lib/cjs/components/PntRegionSelect/types.js +51 -0
  12. package/lib/cjs/components/PntSecureBox/index.scss +3 -0
  13. package/lib/cjs/index.d.ts +3 -4
  14. package/lib/cjs/index.js +5 -12
  15. package/lib/esm/api/index.d.ts +14 -1
  16. package/lib/esm/api/index.js +16 -1
  17. package/lib/esm/api/types/index.d.ts +47 -0
  18. package/lib/esm/components/PntRegionSelect/ImportDialog.d.ts +25 -0
  19. package/lib/esm/components/PntRegionSelect/ImportDialog.js +241 -0
  20. package/lib/esm/components/PntRegionSelect/index.d.ts +43 -0
  21. package/lib/esm/components/PntRegionSelect/index.js +388 -0
  22. package/lib/esm/components/PntRegionSelect/index.scss +166 -0
  23. package/lib/esm/components/PntRegionSelect/types.d.ts +158 -0
  24. package/lib/esm/components/PntRegionSelect/types.js +51 -0
  25. package/lib/esm/components/PntSecureBox/index.scss +3 -0
  26. package/lib/esm/index.d.ts +3 -4
  27. package/lib/esm/index.js +7 -4
  28. package/lib/umd/pnt-component-frontend.min.css +1 -1
  29. package/lib/umd/pnt-component-frontend.min.js +1 -1
  30. package/package.json +2 -2
@@ -0,0 +1,43 @@
1
+ import { FC } from 'react';
2
+ import './index.scss';
3
+ import type { PntRegionSelectProps } from './types';
4
+ /**
5
+ * 地区选择器组件
6
+ * @description 统一的国家/地区选择组件,支持单选/多选、批量选择、批量导入、自定义区域、合规区域
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * // 基础多选(内部拉取默认区域列表)
11
+ * <PntRegionSelect
12
+ * value={regions}
13
+ * onChange={(v) => setRegions(v)}
14
+ * />
15
+ *
16
+ * // 自定义合规区域
17
+ * <PntRegionSelect
18
+ * dataSource="custom"
19
+ * gameId={gameId}
20
+ * env={0}
21
+ * value={regions}
22
+ * onChange={(v) => setRegions(v)}
23
+ * />
24
+ *
25
+ * // 业务最终生效区域
26
+ * <PntRegionSelect
27
+ * dataSource="business"
28
+ * gameId={gameId}
29
+ * value={regions}
30
+ * onChange={(v) => setRegions(v)}
31
+ * />
32
+ *
33
+ * // 外部受控选项 + 批量导入
34
+ * <PntRegionSelect
35
+ * options={regionList}
36
+ * importable
37
+ * value={regions}
38
+ * onChange={(v) => setRegions(v)}
39
+ * />
40
+ * ```
41
+ */
42
+ declare const PntRegionSelect: FC<PntRegionSelectProps>;
43
+ export default PntRegionSelect;
@@ -0,0 +1,388 @@
1
+ import { businessGameInfoGetCustomRegion, businessRegionQuery } from "../../api";
2
+ import { usePntConfig } from "../../config";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Button, Checkbox, Select } from 'tdesign-react';
5
+ import ImportDialog from "./ImportDialog";
6
+ import "./index.scss";
7
+ import { DEFAULT_TEXTS } from "./types";
8
+
9
+ /**
10
+ * 将后端区域项归一化为组件内部的 RegionOption
11
+ * @description 后端契约字段均为可选,此处补默认值
12
+ */
13
+ import { jsx as _jsx } from "react/jsx-runtime";
14
+ import { jsxs as _jsxs } from "react/jsx-runtime";
15
+ import { Fragment as _Fragment } from "react/jsx-runtime";
16
+ const toRegionOptions = list => (list || []).map(item => ({
17
+ ...item,
18
+ region_code: item.region_code ?? '',
19
+ region_ch: item.region_ch ?? '',
20
+ region_en: item.region_en ?? ''
21
+ }));
22
+
23
+ /**
24
+ * 构建标签文本
25
+ */
26
+ const getLabelText = (item, format) => {
27
+ const {
28
+ region_ch,
29
+ region_en,
30
+ region_code
31
+ } = item;
32
+ switch (format) {
33
+ case 'name_en':
34
+ return region_en || region_ch || region_code;
35
+ case 'name_en_name':
36
+ return region_en && region_ch ? `${region_en}(${region_ch})` : region_en || region_ch || region_code;
37
+ case 'code':
38
+ return region_code;
39
+ default:
40
+ return region_ch || region_en || region_code;
41
+ }
42
+ };
43
+
44
+ /**
45
+ * 地区选择器组件
46
+ * @description 统一的国家/地区选择组件,支持单选/多选、批量选择、批量导入、自定义区域、合规区域
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * // 基础多选(内部拉取默认区域列表)
51
+ * <PntRegionSelect
52
+ * value={regions}
53
+ * onChange={(v) => setRegions(v)}
54
+ * />
55
+ *
56
+ * // 自定义合规区域
57
+ * <PntRegionSelect
58
+ * dataSource="custom"
59
+ * gameId={gameId}
60
+ * env={0}
61
+ * value={regions}
62
+ * onChange={(v) => setRegions(v)}
63
+ * />
64
+ *
65
+ * // 业务最终生效区域
66
+ * <PntRegionSelect
67
+ * dataSource="business"
68
+ * gameId={gameId}
69
+ * value={regions}
70
+ * onChange={(v) => setRegions(v)}
71
+ * />
72
+ *
73
+ * // 外部受控选项 + 批量导入
74
+ * <PntRegionSelect
75
+ * options={regionList}
76
+ * importable
77
+ * value={regions}
78
+ * onChange={(v) => setRegions(v)}
79
+ * />
80
+ * ```
81
+ */
82
+ const PntRegionSelect = props => {
83
+ const {
84
+ value = [],
85
+ onChange,
86
+ multiple = true,
87
+ dataSource = 'default',
88
+ gameId,
89
+ env = 0,
90
+ options: externalOptions,
91
+ disabled = false,
92
+ placeholder,
93
+ max,
94
+ filterable = true,
95
+ labelFormat = 'name',
96
+ texts: customTexts,
97
+ onLoaded,
98
+ optionDisabled,
99
+ optionTooltip,
100
+ importable = false,
101
+ invertible = false,
102
+ minCollapsedNum,
103
+ collapsedItems,
104
+ onBlur,
105
+ importDialogProps,
106
+ className,
107
+ style,
108
+ popupProps
109
+ } = props;
110
+ const {
111
+ http
112
+ } = usePntConfig();
113
+ const texts = useMemo(() => ({
114
+ ...DEFAULT_TEXTS,
115
+ ...customTexts
116
+ }), [customTexts]);
117
+
118
+ // ---- 状态 ----
119
+ const [innerOptions, setInnerOptions] = useState([]);
120
+ const [loading, setLoading] = useState(false);
121
+ const [searchKeyword, setSearchKeyword] = useState('');
122
+ const [importVisible, setImportVisible] = useState(false);
123
+
124
+ // ---- Refs ----
125
+ const fetchSeqRef = useRef(0);
126
+ const onLoadedRef = useRef(onLoaded);
127
+ onLoadedRef.current = onLoaded;
128
+
129
+ // ---- 外部受控模式:传入 options 时跳过内部请求 ----
130
+ const isExternal = externalOptions !== undefined;
131
+
132
+ // ---- 获取数据 ----
133
+ const fetchData = useCallback(async () => {
134
+ if (isExternal) return;
135
+ const seq = ++fetchSeqRef.current;
136
+ setLoading(true);
137
+ try {
138
+ let list = [];
139
+ if (dataSource === 'custom') {
140
+ if (!gameId) {
141
+ console.warn('[PntRegionSelect] dataSource="custom" 时必须传入 gameId');
142
+ return;
143
+ }
144
+ const res = await businessGameInfoGetCustomRegion(http, {
145
+ game_id: Number(gameId),
146
+ env
147
+ });
148
+ if (seq !== fetchSeqRef.current) return;
149
+ if (res.code === 0 && res.data?.region_list) {
150
+ list = toRegionOptions(res.data.region_list);
151
+ }
152
+ } else if (dataSource === 'business') {
153
+ if (!gameId) {
154
+ console.warn('[PntRegionSelect] dataSource="business" 时必须传入 gameId');
155
+ return;
156
+ }
157
+ const res = await businessRegionQuery(http, {
158
+ game_id: Number(gameId)
159
+ });
160
+ if (seq !== fetchSeqRef.current) return;
161
+ if (res.code === 0 && res.data?.region_list) {
162
+ list = toRegionOptions(res.data.region_list);
163
+ }
164
+ } else {
165
+ const res = await businessRegionQuery(http, {});
166
+ if (seq !== fetchSeqRef.current) return;
167
+ if (res.code === 0 && res.data?.region_list) {
168
+ list = toRegionOptions(res.data.region_list);
169
+ }
170
+ }
171
+ setInnerOptions(list);
172
+ onLoadedRef.current?.(list);
173
+ } catch {
174
+ // 请求失败静默处理
175
+ } finally {
176
+ if (seq === fetchSeqRef.current) {
177
+ setLoading(false);
178
+ }
179
+ }
180
+ }, [http, dataSource, gameId, env, isExternal]);
181
+
182
+ // ---- 初始加载 ----
183
+ useEffect(() => {
184
+ fetchData();
185
+ }, [fetchData]);
186
+
187
+ // ---- 生效的选项列表 ----
188
+ const options = useMemo(() => isExternal ? externalOptions : innerOptions, [isExternal, externalOptions, innerOptions]);
189
+
190
+ // ---- 归一化选中值为数组(兼容单选字符串) ----
191
+ const valueArr = useMemo(() => {
192
+ if (value === undefined || value === null || value === '') return [];
193
+ return Array.isArray(value) ? value.map(String) : [String(value)];
194
+ }, [value]);
195
+
196
+ // ---- 本地搜索过滤 ----
197
+ const filteredOptions = useMemo(() => {
198
+ if (!searchKeyword) return options;
199
+ const kw = searchKeyword.toLowerCase();
200
+ return options.filter(item => item.region_ch?.toLowerCase().includes(kw) || item.region_en?.toLowerCase().includes(kw) || item.region_code?.toLowerCase().includes(kw));
201
+ }, [options, searchKeyword]);
202
+
203
+ // ---- 判断选项是否禁用 ----
204
+ const isOptionDisabled = useCallback(item => {
205
+ if (item.disabled) return true;
206
+ return optionDisabled ? optionDisabled(item) : false;
207
+ }, [optionDisabled]);
208
+
209
+ // ---- 多选 onChange 统一出口 ----
210
+ const emitMultiple = useCallback(nextValue => {
211
+ onChange?.(nextValue, options.filter(o => nextValue.includes(o.region_code)));
212
+ }, [onChange, options]);
213
+
214
+ // ---- 全选/清空 ----
215
+ const handleSelectAll = useCallback(() => {
216
+ const allValues = filteredOptions.filter(item => !isOptionDisabled(item)).map(item => item.region_code);
217
+ emitMultiple(Array.from(new Set([...valueArr, ...allValues])));
218
+ }, [filteredOptions, valueArr, emitMultiple, isOptionDisabled]);
219
+ const handleClearAll = useCallback(() => {
220
+ emitMultiple([]);
221
+ }, [emitMultiple]);
222
+
223
+ // ---- 反选 ----
224
+ const handleInvert = useCallback(() => {
225
+ const selectedSet = new Set(valueArr);
226
+ const nextValue = filteredOptions.filter(item => !isOptionDisabled(item)).map(item => item.region_code).filter(code => !selectedSet.has(code));
227
+ emitMultiple(nextValue);
228
+ }, [filteredOptions, valueArr, emitMultiple, isOptionDisabled]);
229
+
230
+ // ---- 是否全选 ----
231
+ const isAllSelected = useMemo(() => {
232
+ const selectable = filteredOptions.filter(item => !isOptionDisabled(item));
233
+ return selectable.length > 0 && selectable.every(item => valueArr.includes(item.region_code));
234
+ }, [filteredOptions, valueArr, isOptionDisabled]);
235
+
236
+ // ---- 是否半选 ----
237
+ const isIndeterminate = useMemo(() => {
238
+ const selectable = filteredOptions.filter(item => !isOptionDisabled(item));
239
+ const selected = selectable.filter(item => valueArr.includes(item.region_code));
240
+ return selected.length > 0 && selected.length < selectable.length;
241
+ }, [filteredOptions, valueArr, isOptionDisabled]);
242
+
243
+ // ---- 构建 Select options ----
244
+ const selectOptions = useMemo(() => {
245
+ return filteredOptions.map(item => {
246
+ const label = getLabelText(item, labelFormat);
247
+ const isDisabled = isOptionDisabled(item);
248
+ const tooltip = optionTooltip ? optionTooltip(item) : undefined;
249
+ return {
250
+ label,
251
+ value: item.region_code,
252
+ disabled: isDisabled,
253
+ tooltip,
254
+ rawItem: item
255
+ };
256
+ });
257
+ }, [filteredOptions, labelFormat, isOptionDisabled, optionTooltip]);
258
+
259
+ // ---- Select onChange ----
260
+ const handleChange = useCallback(val => {
261
+ const values = Array.isArray(val) ? val : val ? [val] : [];
262
+ const strValues = values.map(String);
263
+ const selectedOptions = options.filter(o => strValues.includes(o.region_code));
264
+ if (multiple) {
265
+ onChange?.(strValues, selectedOptions);
266
+ } else {
267
+ onChange?.(strValues[0] ?? '', selectedOptions[0] ?? undefined);
268
+ }
269
+ }, [onChange, options, multiple]);
270
+
271
+ // ---- 批量导入确认 ----
272
+ const handleImportConfirm = useCallback(nextValue => {
273
+ emitMultiple(nextValue);
274
+ // 兼容外部表单 onBlur 校验触发
275
+ onBlur?.({
276
+ value: nextValue
277
+ });
278
+ }, [emitMultiple, onBlur]);
279
+
280
+ // ---- 面板顶部操作栏 ----
281
+ const panelTopContent = useMemo(() => {
282
+ if (!multiple) return null;
283
+ return /*#__PURE__*/_jsxs("div", {
284
+ className: "pnt-region-select__toolbar",
285
+ children: [/*#__PURE__*/_jsx(Checkbox, {
286
+ checked: isAllSelected,
287
+ indeterminate: isIndeterminate,
288
+ onChange: checked => {
289
+ if (checked) {
290
+ handleSelectAll();
291
+ } else {
292
+ handleClearAll();
293
+ }
294
+ },
295
+ children: texts.selectAll
296
+ }), invertible && /*#__PURE__*/_jsx("span", {
297
+ role: "button",
298
+ tabIndex: 0,
299
+ className: "pnt-region-select__invert",
300
+ onClick: event => {
301
+ event.preventDefault();
302
+ event.stopPropagation();
303
+ handleInvert();
304
+ },
305
+ onKeyDown: event => {
306
+ if (event.key === 'Enter' || event.key === ' ') {
307
+ event.preventDefault();
308
+ event.stopPropagation();
309
+ handleInvert();
310
+ }
311
+ },
312
+ children: texts.invertSelection
313
+ }), /*#__PURE__*/_jsx("span", {
314
+ className: "pnt-region-select__count",
315
+ children: texts.selectedCount.replace('{count}', String(valueArr.length))
316
+ })]
317
+ });
318
+ }, [multiple, isAllSelected, isIndeterminate, handleSelectAll, handleClearAll, invertible, handleInvert, texts, valueArr.length]);
319
+
320
+ // ---- 空状态 ----
321
+ const emptyContent = useMemo(() => {
322
+ if (loading) return null;
323
+ if (filteredOptions.length === 0) {
324
+ return /*#__PURE__*/_jsx("div", {
325
+ className: "pnt-region-select__empty",
326
+ children: texts.noResult
327
+ });
328
+ }
329
+ return null;
330
+ }, [loading, filteredOptions.length, texts.noResult]);
331
+ const selectNode = /*#__PURE__*/_jsx(Select, {
332
+ value: multiple ? valueArr : valueArr[0],
333
+ onChange: handleChange,
334
+ filterable: filterable,
335
+ onSearch: setSearchKeyword,
336
+ loading: loading && options.length === 0,
337
+ multiple: multiple,
338
+ max: max,
339
+ clearable: true,
340
+ disabled: disabled,
341
+ placeholder: placeholder || texts.placeholder,
342
+ panelTopContent: panelTopContent,
343
+ empty: emptyContent,
344
+ minCollapsedNum: minCollapsedNum,
345
+ collapsedItems: collapsedItems,
346
+ onBlur: onBlur,
347
+ popupProps: {
348
+ ...popupProps,
349
+ overlayClassName: ['pnt-region-select-popup', popupProps?.overlayClassName].filter(Boolean).join(' ')
350
+ },
351
+ className: className ? `pnt-region-select ${className}` : 'pnt-region-select',
352
+ style: style,
353
+ children: selectOptions.map(opt => /*#__PURE__*/_jsx(Select.Option, {
354
+ value: opt.value,
355
+ label: opt.label,
356
+ disabled: opt.disabled,
357
+ title: opt.tooltip
358
+ }, opt.value))
359
+ });
360
+
361
+ // ---- 批量导入模式:输入框右侧叠加导入按钮 ----
362
+ if (importable && multiple) {
363
+ return /*#__PURE__*/_jsxs(_Fragment, {
364
+ children: [/*#__PURE__*/_jsxs("div", {
365
+ className: "pnt-region-select__import-wrapper",
366
+ children: [selectNode, /*#__PURE__*/_jsx(Button, {
367
+ theme: "primary",
368
+ size: "small",
369
+ variant: "outline",
370
+ disabled: disabled,
371
+ className: "pnt-region-select__import-btn",
372
+ onClick: () => setImportVisible(true),
373
+ children: texts.importBtn
374
+ })]
375
+ }), /*#__PURE__*/_jsx(ImportDialog, {
376
+ visible: importVisible,
377
+ options: options,
378
+ currentValue: valueArr,
379
+ texts: texts,
380
+ dialogProps: importDialogProps,
381
+ onClose: () => setImportVisible(false),
382
+ onConfirm: handleImportConfirm
383
+ })]
384
+ });
385
+ }
386
+ return selectNode;
387
+ };
388
+ export default PntRegionSelect;
@@ -0,0 +1,166 @@
1
+ .pnt-region-select {
2
+ width: 100%;
3
+
4
+ &__toolbar {
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: space-between;
8
+ padding: 8px 12px;
9
+ border-bottom: 1px solid var(--td-component-border, #e7e7e7);
10
+ background: var(--td-bg-color-container, #fff);
11
+ }
12
+
13
+ &__invert {
14
+ margin-left: 12px;
15
+ margin-right: auto;
16
+ font-size: 12px;
17
+ color: var(--td-brand-color, #0052d9);
18
+ cursor: pointer;
19
+ user-select: none;
20
+
21
+ &:hover {
22
+ color: var(--td-brand-color-hover, #2667d4);
23
+ }
24
+ }
25
+
26
+ &__count {
27
+ margin-left: auto;
28
+ font-size: 12px;
29
+ color: var(--td-text-color-secondary, #999);
30
+ }
31
+
32
+ &__invert + &__count {
33
+ margin-left: 0;
34
+ }
35
+
36
+ &__empty {
37
+ padding: 16px;
38
+ text-align: center;
39
+ color: var(--td-text-color-secondary, #999);
40
+ font-size: 14px;
41
+ }
42
+
43
+ // ---- 批量导入 ----
44
+ &__import-wrapper {
45
+ position: relative;
46
+ display: flex;
47
+ align-items: center;
48
+
49
+ .pnt-region-select {
50
+ flex: 1;
51
+ }
52
+ }
53
+
54
+ &__import-btn {
55
+ position: absolute;
56
+ right: 32px;
57
+ z-index: 1;
58
+ flex-shrink: 0;
59
+ }
60
+ }
61
+
62
+ .pnt-region-select-popup {
63
+ .t-select__list {
64
+ padding-top: 0;
65
+ }
66
+ }
67
+
68
+ // ---- 批量导入弹窗 ----
69
+ .pnt-region-import {
70
+ &__tip {
71
+ margin-bottom: 8px;
72
+ font-size: 12px;
73
+ color: var(--td-text-color-secondary, #999);
74
+ }
75
+
76
+ &__result {
77
+ margin-top: 12px;
78
+ }
79
+
80
+ &__summary {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 4px;
84
+ margin-bottom: 8px;
85
+ font-size: 12px;
86
+ }
87
+
88
+ &__summary-item {
89
+ &--unmatched {
90
+ color: var(--td-error-color, #d54941);
91
+ }
92
+
93
+ &--pending {
94
+ color: var(--td-success-color, #2ba471);
95
+ }
96
+
97
+ &--exists {
98
+ color: var(--td-text-color-secondary, #999);
99
+ }
100
+ }
101
+
102
+ &__summary-dot {
103
+ color: var(--td-text-color-placeholder, #bbb);
104
+ }
105
+
106
+ &__copy-btn {
107
+ padding: 0 4px;
108
+ height: 20px;
109
+ }
110
+
111
+ &__table {
112
+ border: 1px solid var(--td-component-border, #e7e7e7);
113
+ border-radius: 4px;
114
+ overflow: hidden;
115
+ }
116
+
117
+ &__table-head {
118
+ display: flex;
119
+ justify-content: space-between;
120
+ padding: 6px 12px;
121
+ background: var(--td-bg-color-secondarycontainer, #f3f3f3);
122
+ font-size: 12px;
123
+ color: var(--td-text-color-secondary, #999);
124
+ }
125
+
126
+ &__table-body {
127
+ max-height: 200px;
128
+ overflow-y: auto;
129
+ }
130
+
131
+ &__table-row {
132
+ display: flex;
133
+ justify-content: space-between;
134
+ align-items: center;
135
+ padding: 6px 12px;
136
+ font-size: 12px;
137
+
138
+ & + & {
139
+ border-top: 1px solid var(--td-component-border, #e7e7e7);
140
+ }
141
+ }
142
+
143
+ &__status {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ gap: 4px;
147
+
148
+ &--unmatched {
149
+ color: var(--td-error-color, #d54941);
150
+ }
151
+
152
+ &--pending {
153
+ color: var(--td-success-color, #2ba471);
154
+ }
155
+
156
+ &--exists {
157
+ color: var(--td-text-color-secondary, #999);
158
+ }
159
+ }
160
+
161
+ &__empty {
162
+ padding: 16px;
163
+ text-align: center;
164
+ color: var(--td-text-color-secondary, #999);
165
+ }
166
+ }