@pnt-team/pnt-component-frontend 0.0.99 → 0.0.103

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.
@@ -45,10 +45,51 @@ 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;
83
+ /**
84
+ * 解密成功回调
85
+ * @description 当用户点击「查看」且接口解密成功后触发,回调参数为解密后的明文值;
86
+ * 消费方可用于同步表单字段(如 setFieldsValue)或其他副作用
87
+ */
88
+ onDecrypt?: (value: string) => void;
48
89
  }
49
90
  /**
50
91
  * 高敏信息展示统一组件
51
- * @description 用于统一处理和展示高敏信息,支持脱敏显示和按需获取真实数据。
92
+ * @description 用于统一处理和展示高敏信息,支持脱敏显示、按需获取真实数据、解锁后复制。
52
93
  * 通过 PntProvider 注入 http 实例,对接 `/api/v3/business/game_info/high-security-decrypt`。
53
94
  *
54
95
  * @param {PntSecureBoxProps} props - 组件属性
@@ -59,6 +100,10 @@ interface PntSecureBoxProps {
59
100
  * @param {(content: string) => React.ReactNode} props.renderDom - 自定义渲染函数
60
101
  * @param {string} [props.viewText] - 自定义查看按钮文案
61
102
  * @param {string} [props.pageId] - 页面 ID
103
+ * @param {boolean} [props.copyable=true] - 解锁后是否显示复制按钮
104
+ * @param {string} [props.copyText] - 复制按钮文案
105
+ * @param {string} [props.copySuccessText] - 复制成功内联提示文案
106
+ * @param {(value: string) => void | Promise<void>} [props.onCopy] - 自定义复制行为
62
107
  *
63
108
  * @example
64
109
  * ```tsx
@@ -71,6 +116,8 @@ interface PntSecureBoxProps {
71
116
  * gameId={12345}
72
117
  * renderDom={(content) => <span>{content}</span>}
73
118
  * viewText="查看"
119
+ * copyText="复制"
120
+ * copySuccessText="已复制"
74
121
  * />
75
122
  * ```
76
123
  *
@@ -28,15 +28,46 @@ 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 { dataKey, dataValue, env, gameId, renderDom, viewText, pageId, maskPlain } = prop;
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
+ onDecrypt
47
+ } = prop;
48
+ const storeKey = cacheKey || dataKey;
32
49
  const { securityData, isUnlocked, requestSecurityData } = (0, import_useHighSecurity.useHighSecurity)({
33
50
  gameId,
34
- env
51
+ env,
52
+ scope
35
53
  });
36
54
  const [plainViewed, setPlainViewed] = (0, import_react.useState)(false);
55
+ const [copied, setCopied] = (0, import_react.useState)(false);
56
+ const copyTimer = (0, import_react.useRef)(null);
57
+ (0, import_react.useEffect)(() => {
58
+ return () => {
59
+ if (copyTimer.current)
60
+ clearTimeout(copyTimer.current);
61
+ };
62
+ }, []);
37
63
  const isSecurity = (0, import_utils.isSecurityData)(dataValue);
38
- const unlocked = isSecurity ? isUnlocked(dataKey) : maskPlain ? plainViewed : true;
39
- const displayValue = unlocked ? isSecurity ? securityData[dataKey] || "******" : dataValue : "******";
64
+ const unlocked = isSecurity ? isUnlocked(storeKey) : maskPlain ? plainViewed : true;
65
+ const displayValue = unlocked ? isSecurity ? securityData[storeKey] || "******" : dataValue : "******";
66
+ (0, import_react.useEffect)(() => {
67
+ if (onDecrypt && unlocked && isSecurity && securityData[storeKey]) {
68
+ onDecrypt(securityData[storeKey]);
69
+ }
70
+ }, [unlocked, isSecurity, securityData, storeKey, onDecrypt]);
40
71
  const handleView = () => {
41
72
  if (isSecurity) {
42
73
  requestSecurityData({
@@ -46,7 +77,8 @@ var PntSecureBox = (prop) => {
46
77
  param: [
47
78
  {
48
79
  key: dataKey,
49
- value: dataValue
80
+ value: dataValue,
81
+ cacheKey: storeKey
50
82
  }
51
83
  ]
52
84
  });
@@ -54,9 +86,42 @@ var PntSecureBox = (prop) => {
54
86
  setPlainViewed(true);
55
87
  }
56
88
  };
89
+ const handleCopy = (0, import_react.useCallback)(async () => {
90
+ const value = unlocked ? isSecurity ? securityData[storeKey] || "" : dataValue : "";
91
+ if (onCopy) {
92
+ await onCopy(value);
93
+ return;
94
+ }
95
+ try {
96
+ await navigator.clipboard.writeText(value);
97
+ if (copySuccessText) {
98
+ if (copyTimer.current)
99
+ clearTimeout(copyTimer.current);
100
+ setCopied(true);
101
+ copyTimer.current = setTimeout(
102
+ () => setCopied(false),
103
+ 2e3
104
+ );
105
+ }
106
+ } catch (err) {
107
+ console.warn("[pnt-component-frontend] 复制失败", err);
108
+ }
109
+ }, [
110
+ unlocked,
111
+ isSecurity,
112
+ securityData,
113
+ storeKey,
114
+ dataValue,
115
+ onCopy,
116
+ copySuccessText
117
+ ]);
118
+ const isSensitive = isSecurity || !!maskPlain;
119
+ const showViewBtn = !unlocked;
120
+ const showCopyBtn = unlocked && isSensitive && copyable;
57
121
  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
122
  renderDom(displayValue),
59
- !unlocked && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "view-icon", onClick: handleView, children: viewText || "View" })
123
+ showViewBtn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "view-icon", onClick: handleView, children: viewText || "View" }),
124
+ showCopyBtn && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "copy-icon", onClick: handleCopy, children: copied && copySuccessText ? copySuccessText : copyText || "Copy" })
60
125
  ] }) });
61
126
  };
62
127
  var PntSecureBox_default = PntSecureBox;
@@ -3,7 +3,8 @@
3
3
  color: #333;
4
4
  max-width: fit-content;
5
5
 
6
- .view-icon {
6
+ .view-icon,
7
+ .copy-icon {
7
8
  position: absolute;
8
9
  top: 1px;
9
10
  right: -30px;
@@ -22,8 +22,16 @@ export interface PntHttpLike {
22
22
  * 高敏信息解锁接口配置(可选,全局覆盖)
23
23
  */
24
24
  export interface PntHighSecurityConfig {
25
- /** 解密接口 URL,覆盖默认 /v3/business/game_info/high-security-decrypt?audience=/laas-pnt */
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) */
@@ -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, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
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
- let url = merged.decryptUrl;
147
- if (!url) {
148
- const audience = merged.audience || "/laas-pnt";
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 _a, _b;
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
- (_a = res[successCodeField]) != null ? _a : res.code
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 _a2;
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((_a2 = item.value) != null ? _a2 : "") };
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);
@@ -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,51 @@ 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;
83
+ /**
84
+ * 解密成功回调
85
+ * @description 当用户点击「查看」且接口解密成功后触发,回调参数为解密后的明文值;
86
+ * 消费方可用于同步表单字段(如 setFieldsValue)或其他副作用
87
+ */
88
+ onDecrypt?: (value: string) => void;
48
89
  }
49
90
  /**
50
91
  * 高敏信息展示统一组件
51
- * @description 用于统一处理和展示高敏信息,支持脱敏显示和按需获取真实数据。
92
+ * @description 用于统一处理和展示高敏信息,支持脱敏显示、按需获取真实数据、解锁后复制。
52
93
  * 通过 PntProvider 注入 http 实例,对接 `/api/v3/business/game_info/high-security-decrypt`。
53
94
  *
54
95
  * @param {PntSecureBoxProps} props - 组件属性
@@ -59,6 +100,10 @@ interface PntSecureBoxProps {
59
100
  * @param {(content: string) => React.ReactNode} props.renderDom - 自定义渲染函数
60
101
  * @param {string} [props.viewText] - 自定义查看按钮文案
61
102
  * @param {string} [props.pageId] - 页面 ID
103
+ * @param {boolean} [props.copyable=true] - 解锁后是否显示复制按钮
104
+ * @param {string} [props.copyText] - 复制按钮文案
105
+ * @param {string} [props.copySuccessText] - 复制成功内联提示文案
106
+ * @param {(value: string) => void | Promise<void>} [props.onCopy] - 自定义复制行为
62
107
  *
63
108
  * @example
64
109
  * ```tsx
@@ -71,6 +116,8 @@ interface PntSecureBoxProps {
71
116
  * gameId={12345}
72
117
  * renderDom={(content) => <span>{content}</span>}
73
118
  * viewText="查看"
119
+ * copyText="复制"
120
+ * copySuccessText="已复制"
74
121
  * />
75
122
  * ```
76
123
  *
@@ -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,19 @@ 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,
60
+ onDecrypt
48
61
  } = prop;
49
62
 
63
+ // 缓存读写用的 key(cacheKey 优先于 dataKey)
64
+ const storeKey = cacheKey || dataKey;
65
+
50
66
  // 通过 PntProvider 注入的 http 实例请求高敏数据,缓存按 gameId+env 隔离
51
67
  const {
52
68
  securityData,
@@ -54,14 +70,32 @@ const PntSecureBox = prop => {
54
70
  requestSecurityData
55
71
  } = useHighSecurity({
56
72
  gameId,
57
- env
73
+ env,
74
+ scope
58
75
  });
59
76
 
60
77
  // 明文揭示状态(maskPlain 模式下使用,本地 state,不调接口)
61
78
  const [plainViewed, setPlainViewed] = useState(false);
79
+ // 复制成功内联反馈状态
80
+ const [copied, setCopied] = useState(false);
81
+ const copyTimer = useRef(null);
82
+
83
+ // 卸载时清理定时器
84
+ useEffect(() => {
85
+ return () => {
86
+ if (copyTimer.current) clearTimeout(copyTimer.current);
87
+ };
88
+ }, []);
62
89
  const isSecurity = isSecurityData(dataValue);
63
- const unlocked = isSecurity ? isUnlocked(dataKey) : maskPlain ? plainViewed : true;
64
- const displayValue = unlocked ? isSecurity ? securityData[dataKey] || '******' : dataValue : '******';
90
+ const unlocked = isSecurity ? isUnlocked(storeKey) : maskPlain ? plainViewed : true;
91
+ const displayValue = unlocked ? isSecurity ? securityData[storeKey] || '******' : dataValue : '******';
92
+
93
+ // 解密成功后回调,把明文值抛给消费方
94
+ useEffect(() => {
95
+ if (onDecrypt && unlocked && isSecurity && securityData[storeKey]) {
96
+ onDecrypt(securityData[storeKey]);
97
+ }
98
+ }, [unlocked, isSecurity, securityData, storeKey, onDecrypt]);
65
99
  const handleView = () => {
66
100
  if (isSecurity) {
67
101
  requestSecurityData({
@@ -70,20 +104,53 @@ const PntSecureBox = prop => {
70
104
  pageId,
71
105
  param: [{
72
106
  key: dataKey,
73
- value: dataValue
107
+ value: dataValue,
108
+ cacheKey: storeKey
74
109
  }]
75
110
  });
76
111
  } else if (maskPlain) {
77
112
  setPlainViewed(true);
78
113
  }
79
114
  };
115
+ const handleCopy = useCallback(async () => {
116
+ // 待复制的明文值
117
+ const value = unlocked ? isSecurity ? securityData[storeKey] || '' : dataValue : '';
118
+ if (onCopy) {
119
+ await onCopy(value);
120
+ return;
121
+ }
122
+ try {
123
+ await navigator.clipboard.writeText(value);
124
+ if (copySuccessText) {
125
+ if (copyTimer.current) clearTimeout(copyTimer.current);
126
+ setCopied(true);
127
+ copyTimer.current = setTimeout(() => setCopied(false), 2000);
128
+ }
129
+ } catch (err) {
130
+ // eslint-disable-next-line no-console
131
+ console.warn('[pnt-component-frontend] 复制失败', err);
132
+ }
133
+ }, [unlocked, isSecurity, securityData, storeKey, dataValue, onCopy, copySuccessText]);
134
+
135
+ // 渲染逻辑:
136
+ // 1. 敏感数据未解锁:显示查看按钮(viewText)
137
+ // 2. 敏感数据已解锁 + copyable:显示复制按钮(copyText),复制成功临时显示 copySuccessText
138
+ // 3. 敏感数据已解锁 + !copyable:不显示按钮(旧行为)
139
+ // 4. 普通明文(非敏感):无任何按钮
140
+ const isSensitive = isSecurity || !!maskPlain;
141
+ const showViewBtn = !unlocked;
142
+ const showCopyBtn = unlocked && isSensitive && copyable;
80
143
  return /*#__PURE__*/_jsx("div", {
81
144
  className: "pnt-secure-box",
82
145
  children: /*#__PURE__*/_jsxs(_Fragment, {
83
- children: [renderDom(displayValue), !unlocked && /*#__PURE__*/_jsx("span", {
146
+ children: [renderDom(displayValue), showViewBtn && /*#__PURE__*/_jsx("span", {
84
147
  className: "view-icon",
85
148
  onClick: handleView,
86
149
  children: viewText || 'View'
150
+ }), showCopyBtn && /*#__PURE__*/_jsx("span", {
151
+ className: "copy-icon",
152
+ onClick: handleCopy,
153
+ children: copied && copySuccessText ? copySuccessText : copyText || 'Copy'
87
154
  })]
88
155
  })
89
156
  });
@@ -3,7 +3,8 @@
3
3
  color: #333;
4
4
  max-width: fit-content;
5
5
 
6
- .view-icon {
6
+ .view-icon,
7
+ .copy-icon {
7
8
  position: absolute;
8
9
  top: 1px;
9
10
  right: -30px;
@@ -22,8 +22,16 @@ export interface PntHttpLike {
22
22
  * 高敏信息解锁接口配置(可选,全局覆盖)
23
23
  */
24
24
  export interface PntHighSecurityConfig {
25
- /** 解密接口 URL,覆盖默认 /v3/business/game_info/high-security-decrypt?audience=/laas-pnt */
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) */
@@ -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 {