@pnt-team/pnt-component-frontend 0.0.99 → 0.0.102
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/components/PntSecureBox/index.d.ts +42 -1
- package/lib/cjs/components/PntSecureBox/index.js +65 -6
- package/lib/cjs/components/PntSecureBox/index.scss +2 -1
- package/lib/cjs/config.d.ts +9 -1
- package/lib/cjs/global.d.ts +9 -0
- package/lib/cjs/hooks/useHighSecurity.d.ts +10 -0
- package/lib/cjs/hooks/useHighSecurity.js +14 -12
- package/lib/cjs/index.d.ts +1 -1
- package/lib/esm/components/PntSecureBox/index.d.ts +42 -1
- package/lib/esm/components/PntSecureBox/index.js +67 -8
- package/lib/esm/components/PntSecureBox/index.scss +2 -1
- package/lib/esm/config.d.ts +9 -1
- package/lib/esm/global.d.ts +9 -0
- package/lib/esm/hooks/useHighSecurity.d.ts +10 -0
- package/lib/esm/hooks/useHighSecurity.js +18 -9
- package/lib/esm/index.d.ts +1 -1
- package/lib/umd/pnt-component-frontend.min.css +1 -1
- package/lib/umd/pnt-component-frontend.min.js +1 -1
- package/package.json +9 -3
|
@@ -45,10 +45,45 @@ interface PntSecureBoxProps {
|
|
|
45
45
|
* - true:非加密值也展示 ******,点击后本地揭示(不调接口,防窥屏)
|
|
46
46
|
*/
|
|
47
47
|
maskPlain?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* 接口变体
|
|
50
|
+
* @description
|
|
51
|
+
* - 'business'(默认):业务级,走 /v3/business/game_info/high-security-decrypt
|
|
52
|
+
* - 'platform':平台级(发布记录页等场景),走 /v3/business/game_info/plat-high-security-decrypt
|
|
53
|
+
*/
|
|
54
|
+
scope?: 'business' | 'platform';
|
|
55
|
+
/**
|
|
56
|
+
* 解锁后是否显示复制按钮
|
|
57
|
+
* @description 默认 true,解锁后按钮文案切换为「复制」;传 false 则解锁后按钮消失(旧行为)
|
|
58
|
+
*/
|
|
59
|
+
copyable?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* 复制按钮文案
|
|
62
|
+
* @description 用于适配多语言场景,默认为 'Copy'
|
|
63
|
+
*/
|
|
64
|
+
copyText?: string;
|
|
65
|
+
/**
|
|
66
|
+
* 复制成功后的内联提示文案
|
|
67
|
+
* @description 仅在使用默认复制(未传 onCopy)时生效;
|
|
68
|
+
* 传入非空字符串后,复制成功按钮文案临时切换为该文案 2 秒;默认 'Copied',传 '' 关闭内联提示
|
|
69
|
+
*/
|
|
70
|
+
copySuccessText?: string;
|
|
71
|
+
/**
|
|
72
|
+
* 自定义复制行为
|
|
73
|
+
* @description 传入后替换默认的 navigator.clipboard.writeText;
|
|
74
|
+
* 消费方自行处理复制与反馈(此时 copySuccessText 不再生效)
|
|
75
|
+
*/
|
|
76
|
+
onCopy?: (value: string) => void | Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* 缓存别名
|
|
79
|
+
* @description 当后端 key 相同但密文不同时(如两组 SDK 密钥),用 cacheKey 区分缓存;
|
|
80
|
+
* 请求体仍用 dataKey 作为 key,缓存读写用 cacheKey。默认 cacheKey=dataKey
|
|
81
|
+
*/
|
|
82
|
+
cacheKey?: string;
|
|
48
83
|
}
|
|
49
84
|
/**
|
|
50
85
|
* 高敏信息展示统一组件
|
|
51
|
-
* @description
|
|
86
|
+
* @description 用于统一处理和展示高敏信息,支持脱敏显示、按需获取真实数据、解锁后复制。
|
|
52
87
|
* 通过 PntProvider 注入 http 实例,对接 `/api/v3/business/game_info/high-security-decrypt`。
|
|
53
88
|
*
|
|
54
89
|
* @param {PntSecureBoxProps} props - 组件属性
|
|
@@ -59,6 +94,10 @@ interface PntSecureBoxProps {
|
|
|
59
94
|
* @param {(content: string) => React.ReactNode} props.renderDom - 自定义渲染函数
|
|
60
95
|
* @param {string} [props.viewText] - 自定义查看按钮文案
|
|
61
96
|
* @param {string} [props.pageId] - 页面 ID
|
|
97
|
+
* @param {boolean} [props.copyable=true] - 解锁后是否显示复制按钮
|
|
98
|
+
* @param {string} [props.copyText] - 复制按钮文案
|
|
99
|
+
* @param {string} [props.copySuccessText] - 复制成功内联提示文案
|
|
100
|
+
* @param {(value: string) => void | Promise<void>} [props.onCopy] - 自定义复制行为
|
|
62
101
|
*
|
|
63
102
|
* @example
|
|
64
103
|
* ```tsx
|
|
@@ -71,6 +110,8 @@ interface PntSecureBoxProps {
|
|
|
71
110
|
* gameId={12345}
|
|
72
111
|
* renderDom={(content) => <span>{content}</span>}
|
|
73
112
|
* viewText="查看"
|
|
113
|
+
* copyText="复制"
|
|
114
|
+
* copySuccessText="已复制"
|
|
74
115
|
* />
|
|
75
116
|
* ```
|
|
76
117
|
*
|
|
@@ -28,15 +28,40 @@ var import_react = require("react");
|
|
|
28
28
|
var import_index = require("./index.scss");
|
|
29
29
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
30
30
|
var PntSecureBox = (prop) => {
|
|
31
|
-
const {
|
|
31
|
+
const {
|
|
32
|
+
dataKey,
|
|
33
|
+
dataValue,
|
|
34
|
+
env,
|
|
35
|
+
gameId,
|
|
36
|
+
renderDom,
|
|
37
|
+
viewText,
|
|
38
|
+
pageId,
|
|
39
|
+
maskPlain,
|
|
40
|
+
scope,
|
|
41
|
+
copyable = true,
|
|
42
|
+
copyText,
|
|
43
|
+
copySuccessText = "Copied",
|
|
44
|
+
onCopy,
|
|
45
|
+
cacheKey
|
|
46
|
+
} = prop;
|
|
47
|
+
const storeKey = cacheKey || dataKey;
|
|
32
48
|
const { securityData, isUnlocked, requestSecurityData } = (0, import_useHighSecurity.useHighSecurity)({
|
|
33
49
|
gameId,
|
|
34
|
-
env
|
|
50
|
+
env,
|
|
51
|
+
scope
|
|
35
52
|
});
|
|
36
53
|
const [plainViewed, setPlainViewed] = (0, import_react.useState)(false);
|
|
54
|
+
const [copied, setCopied] = (0, import_react.useState)(false);
|
|
55
|
+
const copyTimer = (0, import_react.useRef)(null);
|
|
56
|
+
(0, import_react.useEffect)(() => {
|
|
57
|
+
return () => {
|
|
58
|
+
if (copyTimer.current)
|
|
59
|
+
clearTimeout(copyTimer.current);
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
37
62
|
const isSecurity = (0, import_utils.isSecurityData)(dataValue);
|
|
38
|
-
const unlocked = isSecurity ? isUnlocked(
|
|
39
|
-
const displayValue = unlocked ? isSecurity ? securityData[
|
|
63
|
+
const unlocked = isSecurity ? isUnlocked(storeKey) : maskPlain ? plainViewed : true;
|
|
64
|
+
const displayValue = unlocked ? isSecurity ? securityData[storeKey] || "******" : dataValue : "******";
|
|
40
65
|
const handleView = () => {
|
|
41
66
|
if (isSecurity) {
|
|
42
67
|
requestSecurityData({
|
|
@@ -46,7 +71,8 @@ var PntSecureBox = (prop) => {
|
|
|
46
71
|
param: [
|
|
47
72
|
{
|
|
48
73
|
key: dataKey,
|
|
49
|
-
value: dataValue
|
|
74
|
+
value: dataValue,
|
|
75
|
+
cacheKey: storeKey
|
|
50
76
|
}
|
|
51
77
|
]
|
|
52
78
|
});
|
|
@@ -54,9 +80,42 @@ var PntSecureBox = (prop) => {
|
|
|
54
80
|
setPlainViewed(true);
|
|
55
81
|
}
|
|
56
82
|
};
|
|
83
|
+
const handleCopy = (0, import_react.useCallback)(async () => {
|
|
84
|
+
const value = unlocked ? isSecurity ? securityData[storeKey] || "" : dataValue : "";
|
|
85
|
+
if (onCopy) {
|
|
86
|
+
await onCopy(value);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await navigator.clipboard.writeText(value);
|
|
91
|
+
if (copySuccessText) {
|
|
92
|
+
if (copyTimer.current)
|
|
93
|
+
clearTimeout(copyTimer.current);
|
|
94
|
+
setCopied(true);
|
|
95
|
+
copyTimer.current = setTimeout(
|
|
96
|
+
() => setCopied(false),
|
|
97
|
+
2e3
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn("[pnt-component-frontend] 复制失败", err);
|
|
102
|
+
}
|
|
103
|
+
}, [
|
|
104
|
+
unlocked,
|
|
105
|
+
isSecurity,
|
|
106
|
+
securityData,
|
|
107
|
+
storeKey,
|
|
108
|
+
dataValue,
|
|
109
|
+
onCopy,
|
|
110
|
+
copySuccessText
|
|
111
|
+
]);
|
|
112
|
+
const isSensitive = isSecurity || !!maskPlain;
|
|
113
|
+
const showViewBtn = !unlocked;
|
|
114
|
+
const showCopyBtn = unlocked && isSensitive && copyable;
|
|
57
115
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pnt-secure-box", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
58
116
|
renderDom(displayValue),
|
|
59
|
-
|
|
117
|
+
showViewBtn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "view-icon", onClick: handleView, children: viewText || "View" }),
|
|
118
|
+
showCopyBtn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "copy-icon", onClick: handleCopy, children: copied && copySuccessText ? copySuccessText : copyText || "Copy" })
|
|
60
119
|
] }) });
|
|
61
120
|
};
|
|
62
121
|
var PntSecureBox_default = PntSecureBox;
|
package/lib/cjs/config.d.ts
CHANGED
|
@@ -22,8 +22,16 @@ export interface PntHttpLike {
|
|
|
22
22
|
* 高敏信息解锁接口配置(可选,全局覆盖)
|
|
23
23
|
*/
|
|
24
24
|
export interface PntHighSecurityConfig {
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* 业务级解密接口 URL,覆盖默认 /api/v3/business/game_info/high-security-decrypt?audience=/laas-pnt
|
|
27
|
+
* @description scope='business'(默认)时使用
|
|
28
|
+
*/
|
|
26
29
|
decryptUrl?: string;
|
|
30
|
+
/**
|
|
31
|
+
* 平台级解密接口 URL,覆盖默认 /api/v3/business/game_info/plat-high-security-decrypt?audience=/laas-pnt
|
|
32
|
+
* @description scope='platform' 时使用(发布记录页等平台级权限校验场景)
|
|
33
|
+
*/
|
|
34
|
+
decryptPlatUrl?: string;
|
|
27
35
|
/** 成功码字段:'code'(默认)或 'ret'(account-business 现状) */
|
|
28
36
|
successCodeField?: 'code' | 'ret';
|
|
29
37
|
/** audience 覆盖(默认 /laas-pnt) */
|
package/lib/cjs/global.d.ts
CHANGED
|
@@ -28,3 +28,12 @@ declare module '*.json' {
|
|
|
28
28
|
const value: any;
|
|
29
29
|
export default value;
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
// use-sync-external-store shim(兼容 React 17,包本身未提供 .d.ts)
|
|
33
|
+
declare module 'use-sync-external-store' {
|
|
34
|
+
export function useSyncExternalStore<A>(
|
|
35
|
+
subscribe: (callback: () => void) => () => void,
|
|
36
|
+
getSnapshot: () => A,
|
|
37
|
+
getServerSnapshot?: () => A,
|
|
38
|
+
): A;
|
|
39
|
+
}
|
|
@@ -14,6 +14,12 @@ export interface HighSecurityParamItem {
|
|
|
14
14
|
}
|
|
15
15
|
/** 单次请求的接口配置覆盖(优先级 > PntProvider 全局配置) */
|
|
16
16
|
export type HighSecurityApiConfig = PntHighSecurityConfig;
|
|
17
|
+
/**
|
|
18
|
+
* 高敏接口变体
|
|
19
|
+
* - 'business'(默认):业务级,走 /api/v3/business/game_info/high-security-decrypt
|
|
20
|
+
* - 'platform':平台级(发布记录页场景),走 /api/v3/business/game_info/plat-high-security-decrypt
|
|
21
|
+
*/
|
|
22
|
+
export type HighSecurityScope = 'business' | 'platform';
|
|
17
23
|
/** 高敏数据解锁请求参数(组件层友好命名,库内部转换为后端 snake_case) */
|
|
18
24
|
export interface HighSecurityRequestParams {
|
|
19
25
|
/** 业务 ID */
|
|
@@ -26,6 +32,8 @@ export interface HighSecurityRequestParams {
|
|
|
26
32
|
param: HighSecurityParamItem[];
|
|
27
33
|
/** 本次请求的接口配置覆盖(可选) */
|
|
28
34
|
apiConfig?: HighSecurityApiConfig;
|
|
35
|
+
/** 本次请求的接口变体,覆盖 hook 初始化时的 scope */
|
|
36
|
+
scope?: HighSecurityScope;
|
|
29
37
|
}
|
|
30
38
|
/** hook 初始化 scope:传入后 securityData/isUnlocked 自动按该业务+环境过滤 */
|
|
31
39
|
export interface UseHighSecurityOptions {
|
|
@@ -33,6 +41,8 @@ export interface UseHighSecurityOptions {
|
|
|
33
41
|
gameId?: string | number;
|
|
34
42
|
/** 当前环境(可缺省,默认 0) */
|
|
35
43
|
env?: string | number;
|
|
44
|
+
/** 接口变体(默认 'business');发布记录页等平台级场景传 'platform' */
|
|
45
|
+
scope?: HighSecurityScope;
|
|
36
46
|
}
|
|
37
47
|
/** 清缓存过滤条件 */
|
|
38
48
|
export interface ClearSecurityDataFilter {
|
|
@@ -25,6 +25,7 @@ module.exports = __toCommonJS(useHighSecurity_exports);
|
|
|
25
25
|
var import_api = require("../api");
|
|
26
26
|
var import_config = require("../config");
|
|
27
27
|
var import_react = require("react");
|
|
28
|
+
var import_use_sync_external_store = require("use-sync-external-store");
|
|
28
29
|
var buildCacheKey = (gameId, env, key) => `${gameId}:${env}:${key}`;
|
|
29
30
|
var securityCache = /* @__PURE__ */ new Map();
|
|
30
31
|
var inflightRequests = /* @__PURE__ */ new Map();
|
|
@@ -78,10 +79,11 @@ var clearEntries = (filter) => {
|
|
|
78
79
|
var getDefaultPageId = () => typeof window !== "undefined" && window.location ? window.location.pathname : "";
|
|
79
80
|
var useHighSecurity = (options) => {
|
|
80
81
|
const { http, highSecurity: globalHighSecurity } = (0, import_config.usePntConfig)();
|
|
81
|
-
const snapshot = (0,
|
|
82
|
+
const snapshot = (0, import_use_sync_external_store.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
82
83
|
const scopeGameId = (options == null ? void 0 : options.gameId) === void 0 ? void 0 : String(options.gameId);
|
|
83
84
|
const scopeEnv = (options == null ? void 0 : options.env) === void 0 ? void 0 : String(options.env);
|
|
84
85
|
const hasScope = scopeGameId !== void 0 && scopeEnv !== void 0;
|
|
86
|
+
const optionsScope = options == null ? void 0 : options.scope;
|
|
85
87
|
const securityData = (0, import_react.useMemo)(() => {
|
|
86
88
|
if (!hasScope)
|
|
87
89
|
return {};
|
|
@@ -108,8 +110,10 @@ var useHighSecurity = (options) => {
|
|
|
108
110
|
env,
|
|
109
111
|
pageId,
|
|
110
112
|
param,
|
|
111
|
-
apiConfig
|
|
113
|
+
apiConfig,
|
|
114
|
+
scope: paramScope
|
|
112
115
|
}) => {
|
|
116
|
+
var _a;
|
|
113
117
|
const gid = String(gameId);
|
|
114
118
|
const eid = String(env != null ? env : 0);
|
|
115
119
|
const merged = {
|
|
@@ -143,29 +147,27 @@ var useHighSecurity = (options) => {
|
|
|
143
147
|
page_id: pageId || getDefaultPageId(),
|
|
144
148
|
param: missing
|
|
145
149
|
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
url = `/v3/business/game_info/high-security-decrypt?audience=${audience}`;
|
|
150
|
-
}
|
|
150
|
+
const scope = (_a = paramScope != null ? paramScope : optionsScope) != null ? _a : "business";
|
|
151
|
+
const audience = merged.audience || "/laas-pnt";
|
|
152
|
+
const url = scope === "platform" ? merged.decryptPlatUrl || `/api/v3/business/game_info/plat-high-security-decrypt?audience=${audience}` : merged.decryptUrl || `/api/v3/business/game_info/high-security-decrypt?audience=${audience}`;
|
|
151
153
|
const requestKey = `${gid}:${eid}:${missing.map((item) => `${item.key}${item.value}`).join("")}`;
|
|
152
154
|
const existing = inflightRequests.get(requestKey);
|
|
153
155
|
if (existing)
|
|
154
156
|
return existing;
|
|
155
157
|
const promise = (async () => {
|
|
156
|
-
var
|
|
158
|
+
var _a2, _b;
|
|
157
159
|
try {
|
|
158
160
|
const res = await (0, import_api.businessGameInfoHighSecurityDecrypt)(http, body, url);
|
|
159
161
|
const successCode = Number(
|
|
160
|
-
(
|
|
162
|
+
(_a2 = res[successCodeField]) != null ? _a2 : res.code
|
|
161
163
|
);
|
|
162
164
|
if (successCode === 0 && Array.isArray(res.data)) {
|
|
163
165
|
const decrypted = res.data.map((raw) => {
|
|
164
|
-
var
|
|
166
|
+
var _a3;
|
|
165
167
|
if (raw && typeof raw === "object" && "key" in raw) {
|
|
166
168
|
const item = raw;
|
|
167
169
|
if (typeof item.key === "string") {
|
|
168
|
-
return { key: item.key, value: String((
|
|
170
|
+
return { key: item.key, value: String((_a3 = item.value) != null ? _a3 : "") };
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
173
|
return null;
|
|
@@ -206,7 +208,7 @@ var useHighSecurity = (options) => {
|
|
|
206
208
|
inflightRequests.set(requestKey, promise);
|
|
207
209
|
return promise;
|
|
208
210
|
},
|
|
209
|
-
[http, snapshot, globalHighSecurity]
|
|
211
|
+
[http, snapshot, globalHighSecurity, optionsScope]
|
|
210
212
|
);
|
|
211
213
|
const clearSecurityData = (0, import_react.useCallback)((filter) => {
|
|
212
214
|
clearEntries(filter);
|
package/lib/cjs/index.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export { default as PntUpload } from './components/PntUpload';
|
|
|
6
6
|
export type { PntUploadErrorPhase, PntUploadProps, PntUploadTexts, } from './components/PntUpload';
|
|
7
7
|
export { createPntCosUploader, normalizeSnakeCredential, default as usePntCosUploader, type CosScene, type FileNameStrategy, type GetCosCredentialParams, type NormalizeCredential, type NormalizedCosCredential, type PntCosRequestOverrides, type PntCosUploadOptions, type PntCosUploadResult, type PntCosUploader as PntCosUploaderInstance, } from './components/PntCosUploader';
|
|
8
8
|
export { useHighSecurity } from './hooks/useHighSecurity';
|
|
9
|
-
export type { ClearSecurityDataFilter, HighSecurityApiConfig, HighSecurityParamItem, HighSecurityRequestParams, UseHighSecurityOptions, UseHighSecurityResult, } from './hooks/useHighSecurity';
|
|
9
|
+
export type { ClearSecurityDataFilter, HighSecurityApiConfig, HighSecurityParamItem, HighSecurityRequestParams, HighSecurityScope, UseHighSecurityOptions, UseHighSecurityResult, } from './hooks/useHighSecurity';
|
|
10
10
|
export { isSecurityData } from './utils';
|
|
11
11
|
export { default as PntGeneralLayout } from './components/PntGeneralLayout';
|
|
@@ -45,10 +45,45 @@ interface PntSecureBoxProps {
|
|
|
45
45
|
* - true:非加密值也展示 ******,点击后本地揭示(不调接口,防窥屏)
|
|
46
46
|
*/
|
|
47
47
|
maskPlain?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* 接口变体
|
|
50
|
+
* @description
|
|
51
|
+
* - 'business'(默认):业务级,走 /v3/business/game_info/high-security-decrypt
|
|
52
|
+
* - 'platform':平台级(发布记录页等场景),走 /v3/business/game_info/plat-high-security-decrypt
|
|
53
|
+
*/
|
|
54
|
+
scope?: 'business' | 'platform';
|
|
55
|
+
/**
|
|
56
|
+
* 解锁后是否显示复制按钮
|
|
57
|
+
* @description 默认 true,解锁后按钮文案切换为「复制」;传 false 则解锁后按钮消失(旧行为)
|
|
58
|
+
*/
|
|
59
|
+
copyable?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* 复制按钮文案
|
|
62
|
+
* @description 用于适配多语言场景,默认为 'Copy'
|
|
63
|
+
*/
|
|
64
|
+
copyText?: string;
|
|
65
|
+
/**
|
|
66
|
+
* 复制成功后的内联提示文案
|
|
67
|
+
* @description 仅在使用默认复制(未传 onCopy)时生效;
|
|
68
|
+
* 传入非空字符串后,复制成功按钮文案临时切换为该文案 2 秒;默认 'Copied',传 '' 关闭内联提示
|
|
69
|
+
*/
|
|
70
|
+
copySuccessText?: string;
|
|
71
|
+
/**
|
|
72
|
+
* 自定义复制行为
|
|
73
|
+
* @description 传入后替换默认的 navigator.clipboard.writeText;
|
|
74
|
+
* 消费方自行处理复制与反馈(此时 copySuccessText 不再生效)
|
|
75
|
+
*/
|
|
76
|
+
onCopy?: (value: string) => void | Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* 缓存别名
|
|
79
|
+
* @description 当后端 key 相同但密文不同时(如两组 SDK 密钥),用 cacheKey 区分缓存;
|
|
80
|
+
* 请求体仍用 dataKey 作为 key,缓存读写用 cacheKey。默认 cacheKey=dataKey
|
|
81
|
+
*/
|
|
82
|
+
cacheKey?: string;
|
|
48
83
|
}
|
|
49
84
|
/**
|
|
50
85
|
* 高敏信息展示统一组件
|
|
51
|
-
* @description
|
|
86
|
+
* @description 用于统一处理和展示高敏信息,支持脱敏显示、按需获取真实数据、解锁后复制。
|
|
52
87
|
* 通过 PntProvider 注入 http 实例,对接 `/api/v3/business/game_info/high-security-decrypt`。
|
|
53
88
|
*
|
|
54
89
|
* @param {PntSecureBoxProps} props - 组件属性
|
|
@@ -59,6 +94,10 @@ interface PntSecureBoxProps {
|
|
|
59
94
|
* @param {(content: string) => React.ReactNode} props.renderDom - 自定义渲染函数
|
|
60
95
|
* @param {string} [props.viewText] - 自定义查看按钮文案
|
|
61
96
|
* @param {string} [props.pageId] - 页面 ID
|
|
97
|
+
* @param {boolean} [props.copyable=true] - 解锁后是否显示复制按钮
|
|
98
|
+
* @param {string} [props.copyText] - 复制按钮文案
|
|
99
|
+
* @param {string} [props.copySuccessText] - 复制成功内联提示文案
|
|
100
|
+
* @param {(value: string) => void | Promise<void>} [props.onCopy] - 自定义复制行为
|
|
62
101
|
*
|
|
63
102
|
* @example
|
|
64
103
|
* ```tsx
|
|
@@ -71,6 +110,8 @@ interface PntSecureBoxProps {
|
|
|
71
110
|
* gameId={12345}
|
|
72
111
|
* renderDom={(content) => <span>{content}</span>}
|
|
73
112
|
* viewText="查看"
|
|
113
|
+
* copyText="复制"
|
|
114
|
+
* copySuccessText="已复制"
|
|
74
115
|
* />
|
|
75
116
|
* ```
|
|
76
117
|
*
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useHighSecurity } from "../../hooks/useHighSecurity";
|
|
2
2
|
import { isSecurityData } from "../../utils";
|
|
3
|
-
import React, { useState } from 'react';
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import "./index.scss";
|
|
5
5
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
6
6
|
import { Fragment as _Fragment } from "react/jsx-runtime";
|
|
7
7
|
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
8
|
/**
|
|
9
9
|
* 高敏信息展示统一组件
|
|
10
|
-
* @description
|
|
10
|
+
* @description 用于统一处理和展示高敏信息,支持脱敏显示、按需获取真实数据、解锁后复制。
|
|
11
11
|
* 通过 PntProvider 注入 http 实例,对接 `/api/v3/business/game_info/high-security-decrypt`。
|
|
12
12
|
*
|
|
13
13
|
* @param {PntSecureBoxProps} props - 组件属性
|
|
@@ -18,6 +18,10 @@ import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
18
18
|
* @param {(content: string) => React.ReactNode} props.renderDom - 自定义渲染函数
|
|
19
19
|
* @param {string} [props.viewText] - 自定义查看按钮文案
|
|
20
20
|
* @param {string} [props.pageId] - 页面 ID
|
|
21
|
+
* @param {boolean} [props.copyable=true] - 解锁后是否显示复制按钮
|
|
22
|
+
* @param {string} [props.copyText] - 复制按钮文案
|
|
23
|
+
* @param {string} [props.copySuccessText] - 复制成功内联提示文案
|
|
24
|
+
* @param {(value: string) => void | Promise<void>} [props.onCopy] - 自定义复制行为
|
|
21
25
|
*
|
|
22
26
|
* @example
|
|
23
27
|
* ```tsx
|
|
@@ -30,6 +34,8 @@ import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
30
34
|
* gameId={12345}
|
|
31
35
|
* renderDom={(content) => <span>{content}</span>}
|
|
32
36
|
* viewText="查看"
|
|
37
|
+
* copyText="复制"
|
|
38
|
+
* copySuccessText="已复制"
|
|
33
39
|
* />
|
|
34
40
|
* ```
|
|
35
41
|
*
|
|
@@ -44,9 +50,18 @@ const PntSecureBox = prop => {
|
|
|
44
50
|
renderDom,
|
|
45
51
|
viewText,
|
|
46
52
|
pageId,
|
|
47
|
-
maskPlain
|
|
53
|
+
maskPlain,
|
|
54
|
+
scope,
|
|
55
|
+
copyable = true,
|
|
56
|
+
copyText,
|
|
57
|
+
copySuccessText = 'Copied',
|
|
58
|
+
onCopy,
|
|
59
|
+
cacheKey
|
|
48
60
|
} = prop;
|
|
49
61
|
|
|
62
|
+
// 缓存读写用的 key(cacheKey 优先于 dataKey)
|
|
63
|
+
const storeKey = cacheKey || dataKey;
|
|
64
|
+
|
|
50
65
|
// 通过 PntProvider 注入的 http 实例请求高敏数据,缓存按 gameId+env 隔离
|
|
51
66
|
const {
|
|
52
67
|
securityData,
|
|
@@ -54,14 +69,25 @@ const PntSecureBox = prop => {
|
|
|
54
69
|
requestSecurityData
|
|
55
70
|
} = useHighSecurity({
|
|
56
71
|
gameId,
|
|
57
|
-
env
|
|
72
|
+
env,
|
|
73
|
+
scope
|
|
58
74
|
});
|
|
59
75
|
|
|
60
76
|
// 明文揭示状态(maskPlain 模式下使用,本地 state,不调接口)
|
|
61
77
|
const [plainViewed, setPlainViewed] = useState(false);
|
|
78
|
+
// 复制成功内联反馈状态
|
|
79
|
+
const [copied, setCopied] = useState(false);
|
|
80
|
+
const copyTimer = useRef(null);
|
|
81
|
+
|
|
82
|
+
// 卸载时清理定时器
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
return () => {
|
|
85
|
+
if (copyTimer.current) clearTimeout(copyTimer.current);
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
62
88
|
const isSecurity = isSecurityData(dataValue);
|
|
63
|
-
const unlocked = isSecurity ? isUnlocked(
|
|
64
|
-
const displayValue = unlocked ? isSecurity ? securityData[
|
|
89
|
+
const unlocked = isSecurity ? isUnlocked(storeKey) : maskPlain ? plainViewed : true;
|
|
90
|
+
const displayValue = unlocked ? isSecurity ? securityData[storeKey] || '******' : dataValue : '******';
|
|
65
91
|
const handleView = () => {
|
|
66
92
|
if (isSecurity) {
|
|
67
93
|
requestSecurityData({
|
|
@@ -70,20 +96,53 @@ const PntSecureBox = prop => {
|
|
|
70
96
|
pageId,
|
|
71
97
|
param: [{
|
|
72
98
|
key: dataKey,
|
|
73
|
-
value: dataValue
|
|
99
|
+
value: dataValue,
|
|
100
|
+
cacheKey: storeKey
|
|
74
101
|
}]
|
|
75
102
|
});
|
|
76
103
|
} else if (maskPlain) {
|
|
77
104
|
setPlainViewed(true);
|
|
78
105
|
}
|
|
79
106
|
};
|
|
107
|
+
const handleCopy = useCallback(async () => {
|
|
108
|
+
// 待复制的明文值
|
|
109
|
+
const value = unlocked ? isSecurity ? securityData[storeKey] || '' : dataValue : '';
|
|
110
|
+
if (onCopy) {
|
|
111
|
+
await onCopy(value);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await navigator.clipboard.writeText(value);
|
|
116
|
+
if (copySuccessText) {
|
|
117
|
+
if (copyTimer.current) clearTimeout(copyTimer.current);
|
|
118
|
+
setCopied(true);
|
|
119
|
+
copyTimer.current = setTimeout(() => setCopied(false), 2000);
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.warn('[pnt-component-frontend] 复制失败', err);
|
|
124
|
+
}
|
|
125
|
+
}, [unlocked, isSecurity, securityData, storeKey, dataValue, onCopy, copySuccessText]);
|
|
126
|
+
|
|
127
|
+
// 渲染逻辑:
|
|
128
|
+
// 1. 敏感数据未解锁:显示查看按钮(viewText)
|
|
129
|
+
// 2. 敏感数据已解锁 + copyable:显示复制按钮(copyText),复制成功临时显示 copySuccessText
|
|
130
|
+
// 3. 敏感数据已解锁 + !copyable:不显示按钮(旧行为)
|
|
131
|
+
// 4. 普通明文(非敏感):无任何按钮
|
|
132
|
+
const isSensitive = isSecurity || !!maskPlain;
|
|
133
|
+
const showViewBtn = !unlocked;
|
|
134
|
+
const showCopyBtn = unlocked && isSensitive && copyable;
|
|
80
135
|
return /*#__PURE__*/_jsx("div", {
|
|
81
136
|
className: "pnt-secure-box",
|
|
82
137
|
children: /*#__PURE__*/_jsxs(_Fragment, {
|
|
83
|
-
children: [renderDom(displayValue),
|
|
138
|
+
children: [renderDom(displayValue), showViewBtn && /*#__PURE__*/_jsx("span", {
|
|
84
139
|
className: "view-icon",
|
|
85
140
|
onClick: handleView,
|
|
86
141
|
children: viewText || 'View'
|
|
142
|
+
}), showCopyBtn && /*#__PURE__*/_jsx("span", {
|
|
143
|
+
className: "copy-icon",
|
|
144
|
+
onClick: handleCopy,
|
|
145
|
+
children: copied && copySuccessText ? copySuccessText : copyText || 'Copy'
|
|
87
146
|
})]
|
|
88
147
|
})
|
|
89
148
|
});
|
package/lib/esm/config.d.ts
CHANGED
|
@@ -22,8 +22,16 @@ export interface PntHttpLike {
|
|
|
22
22
|
* 高敏信息解锁接口配置(可选,全局覆盖)
|
|
23
23
|
*/
|
|
24
24
|
export interface PntHighSecurityConfig {
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* 业务级解密接口 URL,覆盖默认 /api/v3/business/game_info/high-security-decrypt?audience=/laas-pnt
|
|
27
|
+
* @description scope='business'(默认)时使用
|
|
28
|
+
*/
|
|
26
29
|
decryptUrl?: string;
|
|
30
|
+
/**
|
|
31
|
+
* 平台级解密接口 URL,覆盖默认 /api/v3/business/game_info/plat-high-security-decrypt?audience=/laas-pnt
|
|
32
|
+
* @description scope='platform' 时使用(发布记录页等平台级权限校验场景)
|
|
33
|
+
*/
|
|
34
|
+
decryptPlatUrl?: string;
|
|
27
35
|
/** 成功码字段:'code'(默认)或 'ret'(account-business 现状) */
|
|
28
36
|
successCodeField?: 'code' | 'ret';
|
|
29
37
|
/** audience 覆盖(默认 /laas-pnt) */
|
package/lib/esm/global.d.ts
CHANGED
|
@@ -28,3 +28,12 @@ declare module '*.json' {
|
|
|
28
28
|
const value: any;
|
|
29
29
|
export default value;
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
// use-sync-external-store shim(兼容 React 17,包本身未提供 .d.ts)
|
|
33
|
+
declare module 'use-sync-external-store' {
|
|
34
|
+
export function useSyncExternalStore<A>(
|
|
35
|
+
subscribe: (callback: () => void) => () => void,
|
|
36
|
+
getSnapshot: () => A,
|
|
37
|
+
getServerSnapshot?: () => A,
|
|
38
|
+
): A;
|
|
39
|
+
}
|
|
@@ -14,6 +14,12 @@ export interface HighSecurityParamItem {
|
|
|
14
14
|
}
|
|
15
15
|
/** 单次请求的接口配置覆盖(优先级 > PntProvider 全局配置) */
|
|
16
16
|
export type HighSecurityApiConfig = PntHighSecurityConfig;
|
|
17
|
+
/**
|
|
18
|
+
* 高敏接口变体
|
|
19
|
+
* - 'business'(默认):业务级,走 /api/v3/business/game_info/high-security-decrypt
|
|
20
|
+
* - 'platform':平台级(发布记录页场景),走 /api/v3/business/game_info/plat-high-security-decrypt
|
|
21
|
+
*/
|
|
22
|
+
export type HighSecurityScope = 'business' | 'platform';
|
|
17
23
|
/** 高敏数据解锁请求参数(组件层友好命名,库内部转换为后端 snake_case) */
|
|
18
24
|
export interface HighSecurityRequestParams {
|
|
19
25
|
/** 业务 ID */
|
|
@@ -26,6 +32,8 @@ export interface HighSecurityRequestParams {
|
|
|
26
32
|
param: HighSecurityParamItem[];
|
|
27
33
|
/** 本次请求的接口配置覆盖(可选) */
|
|
28
34
|
apiConfig?: HighSecurityApiConfig;
|
|
35
|
+
/** 本次请求的接口变体,覆盖 hook 初始化时的 scope */
|
|
36
|
+
scope?: HighSecurityScope;
|
|
29
37
|
}
|
|
30
38
|
/** hook 初始化 scope:传入后 securityData/isUnlocked 自动按该业务+环境过滤 */
|
|
31
39
|
export interface UseHighSecurityOptions {
|
|
@@ -33,6 +41,8 @@ export interface UseHighSecurityOptions {
|
|
|
33
41
|
gameId?: string | number;
|
|
34
42
|
/** 当前环境(可缺省,默认 0) */
|
|
35
43
|
env?: string | number;
|
|
44
|
+
/** 接口变体(默认 'business');发布记录页等平台级场景传 'platform' */
|
|
45
|
+
scope?: HighSecurityScope;
|
|
36
46
|
}
|
|
37
47
|
/** 清缓存过滤条件 */
|
|
38
48
|
export interface ClearSecurityDataFilter {
|