@pnt-team/pnt-component-frontend 0.0.98 → 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/api/cos.d.ts +2 -2
- package/lib/cjs/api/index.d.ts +4 -4
- package/lib/cjs/api/index.js +2 -2
- package/lib/cjs/components/PntCosUploader/index.d.ts +2 -3
- package/lib/cjs/components/PntSecureBox/index.d.ts +51 -3
- package/lib/cjs/components/PntSecureBox/index.js +84 -26
- package/lib/cjs/components/PntSecureBox/index.scss +2 -1
- package/lib/cjs/config.d.ts +32 -2
- package/lib/cjs/global.d.ts +9 -0
- package/lib/cjs/hooks/useHighSecurity.d.ts +24 -3
- package/lib/cjs/hooks/useHighSecurity.js +39 -15
- package/lib/cjs/index.d.ts +2 -2
- package/lib/esm/api/cos.d.ts +2 -2
- package/lib/esm/api/index.d.ts +4 -4
- package/lib/esm/api/index.js +2 -1
- package/lib/esm/components/PntCosUploader/index.d.ts +2 -3
- package/lib/esm/components/PntSecureBox/index.d.ts +51 -3
- package/lib/esm/components/PntSecureBox/index.js +86 -17
- package/lib/esm/components/PntSecureBox/index.scss +2 -1
- package/lib/esm/config.d.ts +32 -2
- package/lib/esm/config.js +11 -0
- package/lib/esm/global.d.ts +9 -0
- package/lib/esm/hooks/useHighSecurity.d.ts +24 -3
- package/lib/esm/hooks/useHighSecurity.js +54 -13
- package/lib/esm/index.d.ts +2 -2
- 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
|
@@ -13,9 +13,9 @@ interface PntSecureBoxProps {
|
|
|
13
13
|
dataValue: string;
|
|
14
14
|
/**
|
|
15
15
|
* 环境标识
|
|
16
|
-
* @description 用于区分不同环境(如:dev, test, production
|
|
16
|
+
* @description 用于区分不同环境(如:dev, test, production);可缺省,默认 0
|
|
17
17
|
*/
|
|
18
|
-
env
|
|
18
|
+
env?: string | number;
|
|
19
19
|
/**
|
|
20
20
|
* 游戏 ID
|
|
21
21
|
* @description 标识具体的游戏项目
|
|
@@ -38,10 +38,52 @@ interface PntSecureBoxProps {
|
|
|
38
38
|
* @description 用于记录操作日志和权限验证
|
|
39
39
|
*/
|
|
40
40
|
pageId?: string;
|
|
41
|
+
/**
|
|
42
|
+
* 明文是否也掩码显示
|
|
43
|
+
* @description
|
|
44
|
+
* - false(默认):非加密值直接展示原值
|
|
45
|
+
* - true:非加密值也展示 ******,点击后本地揭示(不调接口,防窥屏)
|
|
46
|
+
*/
|
|
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;
|
|
41
83
|
}
|
|
42
84
|
/**
|
|
43
85
|
* 高敏信息展示统一组件
|
|
44
|
-
* @description
|
|
86
|
+
* @description 用于统一处理和展示高敏信息,支持脱敏显示、按需获取真实数据、解锁后复制。
|
|
45
87
|
* 通过 PntProvider 注入 http 实例,对接 `/api/v3/business/game_info/high-security-decrypt`。
|
|
46
88
|
*
|
|
47
89
|
* @param {PntSecureBoxProps} props - 组件属性
|
|
@@ -52,6 +94,10 @@ interface PntSecureBoxProps {
|
|
|
52
94
|
* @param {(content: string) => React.ReactNode} props.renderDom - 自定义渲染函数
|
|
53
95
|
* @param {string} [props.viewText] - 自定义查看按钮文案
|
|
54
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] - 自定义复制行为
|
|
55
101
|
*
|
|
56
102
|
* @example
|
|
57
103
|
* ```tsx
|
|
@@ -64,6 +110,8 @@ interface PntSecureBoxProps {
|
|
|
64
110
|
* gameId={12345}
|
|
65
111
|
* renderDom={(content) => <span>{content}</span>}
|
|
66
112
|
* viewText="查看"
|
|
113
|
+
* copyText="复制"
|
|
114
|
+
* copySuccessText="已复制"
|
|
67
115
|
* />
|
|
68
116
|
* ```
|
|
69
117
|
*
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useHighSecurity } from "../../hooks/useHighSecurity";
|
|
2
2
|
import { isSecurityData } from "../../utils";
|
|
3
|
-
import React 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
|
*
|
|
@@ -43,9 +49,19 @@ const PntSecureBox = prop => {
|
|
|
43
49
|
gameId,
|
|
44
50
|
renderDom,
|
|
45
51
|
viewText,
|
|
46
|
-
pageId
|
|
52
|
+
pageId,
|
|
53
|
+
maskPlain,
|
|
54
|
+
scope,
|
|
55
|
+
copyable = true,
|
|
56
|
+
copyText,
|
|
57
|
+
copySuccessText = 'Copied',
|
|
58
|
+
onCopy,
|
|
59
|
+
cacheKey
|
|
47
60
|
} = prop;
|
|
48
61
|
|
|
62
|
+
// 缓存读写用的 key(cacheKey 优先于 dataKey)
|
|
63
|
+
const storeKey = cacheKey || dataKey;
|
|
64
|
+
|
|
49
65
|
// 通过 PntProvider 注入的 http 实例请求高敏数据,缓存按 gameId+env 隔离
|
|
50
66
|
const {
|
|
51
67
|
securityData,
|
|
@@ -53,27 +69,80 @@ const PntSecureBox = prop => {
|
|
|
53
69
|
requestSecurityData
|
|
54
70
|
} = useHighSecurity({
|
|
55
71
|
gameId,
|
|
56
|
-
env
|
|
72
|
+
env,
|
|
73
|
+
scope
|
|
57
74
|
});
|
|
75
|
+
|
|
76
|
+
// 明文揭示状态(maskPlain 模式下使用,本地 state,不调接口)
|
|
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
|
+
}, []);
|
|
58
88
|
const isSecurity = isSecurityData(dataValue);
|
|
59
|
-
const unlocked = isSecurity
|
|
89
|
+
const unlocked = isSecurity ? isUnlocked(storeKey) : maskPlain ? plainViewed : true;
|
|
90
|
+
const displayValue = unlocked ? isSecurity ? securityData[storeKey] || '******' : dataValue : '******';
|
|
91
|
+
const handleView = () => {
|
|
92
|
+
if (isSecurity) {
|
|
93
|
+
requestSecurityData({
|
|
94
|
+
env,
|
|
95
|
+
gameId,
|
|
96
|
+
pageId,
|
|
97
|
+
param: [{
|
|
98
|
+
key: dataKey,
|
|
99
|
+
value: dataValue,
|
|
100
|
+
cacheKey: storeKey
|
|
101
|
+
}]
|
|
102
|
+
});
|
|
103
|
+
} else if (maskPlain) {
|
|
104
|
+
setPlainViewed(true);
|
|
105
|
+
}
|
|
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;
|
|
60
135
|
return /*#__PURE__*/_jsx("div", {
|
|
61
136
|
className: "pnt-secure-box",
|
|
62
137
|
children: /*#__PURE__*/_jsxs(_Fragment, {
|
|
63
|
-
children: [renderDom(
|
|
138
|
+
children: [renderDom(displayValue), showViewBtn && /*#__PURE__*/_jsx("span", {
|
|
64
139
|
className: "view-icon",
|
|
65
|
-
onClick:
|
|
66
|
-
requestSecurityData({
|
|
67
|
-
env,
|
|
68
|
-
gameId,
|
|
69
|
-
pageId,
|
|
70
|
-
param: [{
|
|
71
|
-
key: dataKey,
|
|
72
|
-
value: dataValue
|
|
73
|
-
}]
|
|
74
|
-
});
|
|
75
|
-
},
|
|
140
|
+
onClick: handleView,
|
|
76
141
|
children: viewText || 'View'
|
|
142
|
+
}), showCopyBtn && /*#__PURE__*/_jsx("span", {
|
|
143
|
+
className: "copy-icon",
|
|
144
|
+
onClick: handleCopy,
|
|
145
|
+
children: copied && copySuccessText ? copySuccessText : copyText || 'Copy'
|
|
77
146
|
})]
|
|
78
147
|
})
|
|
79
148
|
});
|
package/lib/esm/config.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/// <reference types="react" />
|
|
2
|
-
import type { AxiosInstance } from 'axios';
|
|
3
2
|
/**
|
|
4
3
|
* 统一响应结构(与后端 commonEesType 对齐)
|
|
5
4
|
*/
|
|
@@ -9,12 +8,43 @@ export interface commonEesType<T> {
|
|
|
9
8
|
data: T;
|
|
10
9
|
ret?: number;
|
|
11
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* 库所需的最小 HTTP 能力(仅 post)。
|
|
13
|
+
* 用结构化类型而非直接依赖 AxiosInstance,
|
|
14
|
+
* 避免与消费方 axios 版本的类型定义强耦合(如 1.13 vs 1.20 的 query 方法差异)。
|
|
15
|
+
* 任何具备 axios 兼容 post 签名的实例均可注入。
|
|
16
|
+
*/
|
|
17
|
+
export interface PntHttpLike {
|
|
18
|
+
get<T = unknown, R = unknown>(url: string, config?: any): Promise<R>;
|
|
19
|
+
post<T = unknown, R = unknown, D = unknown>(url: string, data?: D, config?: any): Promise<R>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 高敏信息解锁接口配置(可选,全局覆盖)
|
|
23
|
+
*/
|
|
24
|
+
export interface PntHighSecurityConfig {
|
|
25
|
+
/**
|
|
26
|
+
* 业务级解密接口 URL,覆盖默认 /api/v3/business/game_info/high-security-decrypt?audience=/laas-pnt
|
|
27
|
+
* @description scope='business'(默认)时使用
|
|
28
|
+
*/
|
|
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;
|
|
35
|
+
/** 成功码字段:'code'(默认)或 'ret'(account-business 现状) */
|
|
36
|
+
successCodeField?: 'code' | 'ret';
|
|
37
|
+
/** audience 覆盖(默认 /laas-pnt) */
|
|
38
|
+
audience?: string;
|
|
39
|
+
}
|
|
12
40
|
/**
|
|
13
41
|
* 组件库运行时配置:由消费方通过 PntProvider 注入
|
|
14
42
|
*/
|
|
15
43
|
export interface PntConfig {
|
|
16
44
|
/** 消费方已配置好的 axios 实例(含拦截器、鉴权、baseURL 等) */
|
|
17
|
-
http:
|
|
45
|
+
http: PntHttpLike;
|
|
46
|
+
/** 高敏信息解锁接口全局配置(可选) */
|
|
47
|
+
highSecurity?: PntHighSecurityConfig;
|
|
18
48
|
}
|
|
19
49
|
/**
|
|
20
50
|
* 配置 Provider:消费方在应用根入口包裹一次即可
|
package/lib/esm/config.js
CHANGED
|
@@ -4,6 +4,17 @@ import { createContext, useContext } from 'react';
|
|
|
4
4
|
* 统一响应结构(与后端 commonEesType 对齐)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 库所需的最小 HTTP 能力(仅 post)。
|
|
9
|
+
* 用结构化类型而非直接依赖 AxiosInstance,
|
|
10
|
+
* 避免与消费方 axios 版本的类型定义强耦合(如 1.13 vs 1.20 的 query 方法差异)。
|
|
11
|
+
* 任何具备 axios 兼容 post 签名的实例均可注入。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 高敏信息解锁接口配置(可选,全局覆盖)
|
|
16
|
+
*/
|
|
17
|
+
|
|
7
18
|
/**
|
|
8
19
|
* 组件库运行时配置:由消费方通过 PntProvider 注入
|
|
9
20
|
*/
|
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
|
+
}
|
|
@@ -1,27 +1,48 @@
|
|
|
1
|
+
import type { PntHighSecurityConfig } from "../config";
|
|
1
2
|
/** 单个解锁项 */
|
|
2
3
|
export interface HighSecurityParamItem {
|
|
3
4
|
/** 字段标识(与后端约定) */
|
|
4
5
|
key: string;
|
|
5
6
|
/** 参数密文(_high_security_: 前缀) */
|
|
6
7
|
value: string;
|
|
8
|
+
/**
|
|
9
|
+
* 缓存别名(可选)
|
|
10
|
+
* @description 当后端 key 相同但密文不同时(如两组 SDK 密钥),
|
|
11
|
+
* 用 cacheKey 区分缓存,请求体仍用 key。默认 cacheKey=key
|
|
12
|
+
*/
|
|
13
|
+
cacheKey?: string;
|
|
7
14
|
}
|
|
15
|
+
/** 单次请求的接口配置覆盖(优先级 > PntProvider 全局配置) */
|
|
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';
|
|
8
23
|
/** 高敏数据解锁请求参数(组件层友好命名,库内部转换为后端 snake_case) */
|
|
9
24
|
export interface HighSecurityRequestParams {
|
|
10
25
|
/** 业务 ID */
|
|
11
26
|
gameId: string | number;
|
|
12
|
-
/**
|
|
13
|
-
env
|
|
27
|
+
/** 环境标识(可缺省,默认 0;以各业务现有约定为准) */
|
|
28
|
+
env?: string | number;
|
|
14
29
|
/** 页面 ID(不传则默认取 location.pathname),用于操作日志和权限校验 */
|
|
15
30
|
pageId?: string;
|
|
16
31
|
/** 需要解锁的参数列表,支持批量 */
|
|
17
32
|
param: HighSecurityParamItem[];
|
|
33
|
+
/** 本次请求的接口配置覆盖(可选) */
|
|
34
|
+
apiConfig?: HighSecurityApiConfig;
|
|
35
|
+
/** 本次请求的接口变体,覆盖 hook 初始化时的 scope */
|
|
36
|
+
scope?: HighSecurityScope;
|
|
18
37
|
}
|
|
19
38
|
/** hook 初始化 scope:传入后 securityData/isUnlocked 自动按该业务+环境过滤 */
|
|
20
39
|
export interface UseHighSecurityOptions {
|
|
21
40
|
/** 当前业务 ID */
|
|
22
41
|
gameId?: string | number;
|
|
23
|
-
/**
|
|
42
|
+
/** 当前环境(可缺省,默认 0) */
|
|
24
43
|
env?: string | number;
|
|
44
|
+
/** 接口变体(默认 'business');发布记录页等平台级场景传 'platform' */
|
|
45
|
+
scope?: HighSecurityScope;
|
|
25
46
|
}
|
|
26
47
|
/** 清缓存过滤条件 */
|
|
27
48
|
export interface ClearSecurityDataFilter {
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { businessGameInfoHighSecurityDecrypt } from "../api";
|
|
2
2
|
import { usePntConfig } from "../config";
|
|
3
|
-
import { useCallback, useMemo
|
|
3
|
+
import { useCallback, useMemo } from 'react';
|
|
4
|
+
// 使用 use-sync-external-store shim,兼容 React 17(sdk-frontend),
|
|
5
|
+
// React 18+ 亦可直接使用此包(内部会代理到 react 的 useSyncExternalStore)
|
|
6
|
+
import { useSyncExternalStore } from 'use-sync-external-store';
|
|
4
7
|
|
|
5
8
|
/** 单个解锁项 */
|
|
6
9
|
|
|
10
|
+
/** 单次请求的接口配置覆盖(优先级 > PntProvider 全局配置) */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 高敏接口变体
|
|
14
|
+
* - 'business'(默认):业务级,走 /api/v3/business/game_info/high-security-decrypt
|
|
15
|
+
* - 'platform':平台级(发布记录页场景),走 /api/v3/business/game_info/plat-high-security-decrypt
|
|
16
|
+
*/
|
|
17
|
+
|
|
7
18
|
/** 高敏数据解锁请求参数(组件层友好命名,库内部转换为后端 snake_case) */
|
|
8
19
|
|
|
9
20
|
/** hook 初始化 scope:传入后 securityData/isUnlocked 自动按该业务+环境过滤 */
|
|
@@ -83,12 +94,14 @@ const getDefaultPageId = () => typeof window !== 'undefined' && window.location
|
|
|
83
94
|
*/
|
|
84
95
|
export const useHighSecurity = options => {
|
|
85
96
|
const {
|
|
86
|
-
http
|
|
97
|
+
http,
|
|
98
|
+
highSecurity: globalHighSecurity
|
|
87
99
|
} = usePntConfig();
|
|
88
100
|
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
89
101
|
const scopeGameId = options?.gameId === undefined ? undefined : String(options.gameId);
|
|
90
102
|
const scopeEnv = options?.env === undefined ? undefined : String(options.env);
|
|
91
103
|
const hasScope = scopeGameId !== undefined && scopeEnv !== undefined;
|
|
104
|
+
const optionsScope = options?.scope;
|
|
92
105
|
const securityData = useMemo(() => {
|
|
93
106
|
if (!hasScope) return {};
|
|
94
107
|
const prefix = `${scopeGameId}:${scopeEnv}:`;
|
|
@@ -109,23 +122,35 @@ export const useHighSecurity = options => {
|
|
|
109
122
|
gameId,
|
|
110
123
|
env,
|
|
111
124
|
pageId,
|
|
112
|
-
param
|
|
125
|
+
param,
|
|
126
|
+
apiConfig,
|
|
127
|
+
scope: paramScope
|
|
113
128
|
}) => {
|
|
114
129
|
const gid = String(gameId);
|
|
115
|
-
const eid = String(env);
|
|
130
|
+
const eid = String(env ?? 0);
|
|
131
|
+
|
|
132
|
+
// 合并接口配置:单次传入 > PntProvider 全局
|
|
133
|
+
const merged = {
|
|
134
|
+
...globalHighSecurity,
|
|
135
|
+
...apiConfig
|
|
136
|
+
};
|
|
137
|
+
const successCodeField = merged.successCodeField || 'code';
|
|
116
138
|
const result = new Array(param.length);
|
|
117
139
|
const missing = [];
|
|
118
140
|
const missingIndexes = [];
|
|
119
141
|
|
|
120
|
-
// 已缓存的 key
|
|
142
|
+
// 已缓存的 key(cacheKey 优先)不再请求
|
|
121
143
|
param.forEach((item, index) => {
|
|
122
|
-
const
|
|
144
|
+
const storeKey = item.cacheKey || item.key;
|
|
145
|
+
const cached = snapshot.get(buildCacheKey(gid, eid, storeKey));
|
|
123
146
|
if (cached !== undefined) {
|
|
124
147
|
result[index] = {
|
|
125
148
|
key: item.key,
|
|
126
|
-
value: cached
|
|
149
|
+
value: cached,
|
|
150
|
+
cacheKey: storeKey
|
|
127
151
|
};
|
|
128
152
|
} else {
|
|
153
|
+
// 请求体只用 key/value,不含 cacheKey
|
|
129
154
|
missing.push({
|
|
130
155
|
key: item.key,
|
|
131
156
|
value: item.value
|
|
@@ -138,19 +163,25 @@ export const useHighSecurity = options => {
|
|
|
138
163
|
}
|
|
139
164
|
const body = {
|
|
140
165
|
game_id: Number(gameId),
|
|
141
|
-
env: Number(env),
|
|
166
|
+
env: Number(env ?? 0),
|
|
142
167
|
page_id: pageId || getDefaultPageId(),
|
|
143
168
|
param: missing
|
|
144
169
|
};
|
|
145
170
|
|
|
171
|
+
// 拼接 URL:按 scope 选择接口变体;decryptUrl/decryptPlatUrl 优先于默认
|
|
172
|
+
const scope = paramScope ?? optionsScope ?? 'business';
|
|
173
|
+
const audience = merged.audience || '/laas-pnt';
|
|
174
|
+
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}`;
|
|
175
|
+
|
|
146
176
|
// 请求级并发去重:同一业务+环境+参数列表指纹在途时复用
|
|
147
177
|
const requestKey = `${gid}:${eid}:${missing.map(item => `${item.key}\u0001${item.value}`).join('\u0002')}`;
|
|
148
178
|
const existing = inflightRequests.get(requestKey);
|
|
149
179
|
if (existing) return existing;
|
|
150
180
|
const promise = (async () => {
|
|
151
181
|
try {
|
|
152
|
-
const res = await businessGameInfoHighSecurityDecrypt(http, body);
|
|
153
|
-
|
|
182
|
+
const res = await businessGameInfoHighSecurityDecrypt(http, body, url);
|
|
183
|
+
const successCode = Number(res[successCodeField] ?? res.code);
|
|
184
|
+
if (successCode === 0 && Array.isArray(res.data)) {
|
|
154
185
|
// yapi 生成类型为 Record<string, unknown>[],按运行时结构收窄
|
|
155
186
|
const decrypted = res.data.map(raw => {
|
|
156
187
|
if (raw && typeof raw === 'object' && 'key' in raw) {
|
|
@@ -164,14 +195,24 @@ export const useHighSecurity = options => {
|
|
|
164
195
|
}
|
|
165
196
|
return null;
|
|
166
197
|
}).filter(item => item !== null);
|
|
167
|
-
|
|
198
|
+
// 写入缓存时用调用方提供的 cacheKey 映射
|
|
199
|
+
const toStore = decrypted.map(d => {
|
|
200
|
+
const matchIdx = missingIndexes.find(i => param[i].key === d.key);
|
|
201
|
+
const storeKey = matchIdx !== undefined ? param[matchIdx].cacheKey || d.key : d.key;
|
|
202
|
+
return {
|
|
203
|
+
key: d.key,
|
|
204
|
+
value: d.value,
|
|
205
|
+
cacheKey: storeKey
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
setEntries(gid, eid, toStore);
|
|
168
209
|
decrypted.forEach(item => {
|
|
169
210
|
const index = missingIndexes.find(i => param[i].key === item.key);
|
|
170
211
|
if (index !== undefined) result[index] = item;
|
|
171
212
|
});
|
|
172
213
|
} else {
|
|
173
214
|
// eslint-disable-next-line no-console
|
|
174
|
-
console.warn('[pnt-component-frontend] high-security-decrypt 解锁失败', res?.msg ?? res
|
|
215
|
+
console.warn('[pnt-component-frontend] high-security-decrypt 解锁失败', res?.msg ?? res[successCodeField]);
|
|
175
216
|
}
|
|
176
217
|
} catch (error) {
|
|
177
218
|
// eslint-disable-next-line no-console
|
|
@@ -183,7 +224,7 @@ export const useHighSecurity = options => {
|
|
|
183
224
|
})();
|
|
184
225
|
inflightRequests.set(requestKey, promise);
|
|
185
226
|
return promise;
|
|
186
|
-
}, [http, snapshot]);
|
|
227
|
+
}, [http, snapshot, globalHighSecurity, optionsScope]);
|
|
187
228
|
const clearSecurityData = useCallback(filter => {
|
|
188
229
|
clearEntries(filter);
|
|
189
230
|
}, []);
|
package/lib/esm/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { PntProvider, usePntConfig, type PntConfig, type commonEesType, } from './config';
|
|
1
|
+
export { PntProvider, usePntConfig, type PntConfig, type PntHttpLike, type PntHighSecurityConfig, type commonEesType, } from './config';
|
|
2
2
|
export { default as PntImage } from './components/PntImage';
|
|
3
3
|
export { default as PntBusinessSelect } from './components/PntBusinessSelect';
|
|
4
4
|
export { default as PntSecureBox } from './components/PntSecureBox';
|
|
@@ -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, 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';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.pnt-image-wrapper{position:relative}.pnt-image-wrapper .pnt-image-progress,.pnt-image-wrapper .pnt-image-upload-progress{align-items:center;display:flex;height:8.5rem;justify-content:center;width:8.5rem}.pnt-image-wrapper .pnt-image-progress .pnt-image-loading,.pnt-image-wrapper .pnt-image-upload-progress .pnt-image-loading{align-items:center;background-color:#f7f8fa;border-radius:.25rem;display:flex;flex-direction:column;height:100%;justify-content:center;text-align:center;width:100%}.pnt-image-wrapper .pnt-image-progress .pnt-image-loading svg,.pnt-image-wrapper .pnt-image-upload-progress .pnt-image-loading svg{color:var(--td-brand-color);font-size:1.5rem}.pnt-image-wrapper .pnt-image-progress .pnt-image-loading p,.pnt-image-wrapper .pnt-image-upload-progress .pnt-image-loading p{color:#808191;font-size:.75rem;margin-top:.25rem}.pnt-image-wrapper .pnt-image{height:8.5rem;width:8.5rem}.pnt-image-wrapper .pnt-image .pnt-image-mask{align-items:center;background:rgba(0,0,0,.6);color:#fff;display:flex;height:100%;justify-content:center}.pnt-image-wrapper .pnt-image .pnt-image-mask--basic{cursor:pointer}.pnt-image-wrapper .pnt-image .pnt-image-mask .pnt-image-divider{border-left:.0625rem solid var(--td-text-color-anti);height:var(--td-comp-size-xxxs);margin:0 var(--td-comp-margin-l)}.pnt-business-select{width:100%}.pnt-business-select-option-label{display:block;width:100%}.pnt-business-select-popup .t-popup__content .t-loading,.pnt-business-select-popup .t-popup__content .total-wrapper{align-items:center;display:flex;font-weight:400;height:2.25rem;line-height:1.25rem;padding:0 .875rem}.pnt-business-select-popup .t-popup__content .total-wrapper{color:#808191;font-size:.875rem}.pnt-secure-box{color:#333;max-width:-moz-fit-content;max-width:fit-content;position:relative}.pnt-secure-box .view-icon{color:#3f8cff!important;cursor:pointer;font-size:small;position:absolute;right:-1.875rem;top:.0625rem}.pnt-secure-box .security-value{position:relative;top:.1875rem}.pnt-upload{display:inline-block}.pnt-upload .pnt-upload__trigger{cursor:pointer;display:inline-block}.pnt-upload .pnt-upload__error{color:#d54941;color:var(--td-error-color,#d54941);font-size:.75rem;line-height:1.25rem;margin:.25rem 0 0}.pnt-upload--disabled .pnt-upload__trigger{cursor:not-allowed}.pnt-upload .pnt-upload-file{background-color:#fff;background-color:var(--td-bg-color-container,#fff);border:.0625rem dashed #dcdcdc;border:.0625rem dashed var(--td-component-border,#dcdcdc);border-radius:.375rem;border-radius:var(--td-radius-default,6px);box-sizing:border-box;min-width:20rem;padding:.75rem 1rem;transition:border-color .2s}.pnt-upload .pnt-upload-file:hover{border-color:#165dff;border-color:var(--td-brand-color,#165dff)}.pnt-upload .pnt-upload-file__row{align-items:center;color:#1f2329;color:var(--td-text-color-primary,#1f2329);display:flex;font-size:.875rem;gap:.5rem;line-height:1.375rem}.pnt-upload .pnt-upload-file__row--empty{color:#6b7280;color:var(--td-text-color-secondary,#6b7280);justify-content:flex-start}.pnt-upload .pnt-upload-file__row--progress{align-items:center}.pnt-upload .pnt-upload-file__icon{color:#6b7280;color:var(--td-text-color-secondary,#6b7280);flex:none;font-size:1.25rem}.pnt-upload .pnt-upload-file__name{flex:1 1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pnt-upload .pnt-upload-file__delete{color:#6b7280;color:var(--td-text-color-secondary,#6b7280);cursor:pointer;flex:none;font-size:1.125rem}.pnt-upload .pnt-upload-file__delete:hover{color:#d54941;color:var(--td-error-color,#d54941)}.pnt-upload .pnt-upload-file__progress{flex:1 1;min-width:0}.pnt-upload .pnt-upload-file__percent{color:#6b7280;color:var(--td-text-color-secondary,#6b7280);flex:none;font-size:.75rem}.pnt-upload--disabled .pnt-upload-file{background-color:#f3f3f3;background-color:var(--td-bg-color-component-disabled,#f3f3f3)}.pnt-upload--disabled .pnt-upload-file:hover{border-color:#dcdcdc;border-color:var(--td-component-border,#dcdcdc)}.pnt-general-layout{--pnt-layout-body-padding:1.5rem;--pnt-layout-card-radius:0.25rem;background-color:transparent;height:100%}.pnt-general-layout .t-layout__header{align-items:stretch;border-bottom:.0625rem solid #f0f3f6;display:flex;flex-shrink:0;height:auto;justify-content:space-between}.pnt-general-layout .t-layout__header .header-title{align-items:center;display:inline-flex;padding:1.5rem}.pnt-general-layout .t-layout__header .header-title .goback-icon{color:#3f8cff;cursor:pointer;margin-right:.75rem}.pnt-general-layout .t-layout__header .header-title .title{font-size:1.25rem;font-weight:600;line-height:1.5rem}.pnt-general-layout .t-layout__header .header-operation{align-items:center;display:flex;padding-right:1.5rem}.pnt-general-layout .t-layout__header .header-operation .t-button+.t-button{margin-left:.75rem}.pnt-general-layout--borderless .t-layout__header{border-bottom:none}.pnt-general-layout .general-layout-sub-header{border-bottom:.0625rem solid #f0f3f6;flex-shrink:0}.pnt-general-layout .general-layout-body{background:#fafafa;flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--pnt-layout-body-padding)}.pnt-general-layout .general-layout-body>div.t-card,.pnt-general-layout .general-layout-body>div.t-loading__parent>div.t-card,.pnt-general-layout .general-layout-body>div.tea-card,.pnt-general-layout .general-layout-body>div[class*=-block],.pnt-general-layout .general-layout-body>div[class*=-wrapper]{border-radius:var(--pnt-layout-card-radius);box-shadow:none;margin-bottom:1.5rem}.pnt-general-layout .general-layout-body>div.t-card .t-card__body,.pnt-general-layout .general-layout-body>div.t-loading__parent>div.t-card .t-card__body,.pnt-general-layout .general-layout-body>div.tea-card .t-card__body,.pnt-general-layout .general-layout-body>div[class*=-block] .t-card__body,.pnt-general-layout .general-layout-body>div[class*=-wrapper] .t-card__body{border-radius:var(--pnt-layout-card-radius)}.pnt-general-layout .general-layout-body>div.t-loading__parent>div:last-child.t-card,.pnt-general-layout .general-layout-body>div:last-child,.pnt-general-layout .general-layout-body>div:last-child.t-card,.pnt-general-layout .general-layout-body>div:last-child.tea-card,.pnt-general-layout .general-layout-body>div:last-child[class*=-block],.pnt-general-layout .general-layout-body>div:last-child[class*=-wrapper]{margin-bottom:0}.pnt-general-layout .t-layout__footer{background:#fff;border-top:.0625rem solid #f0f3f6;flex-shrink:0;padding:1.125rem 1.5rem}.pnt-general-layout .t-layout__footer .t-button{font-size:.875rem;font-weight:600;height:2.25rem;min-width:5.75rem}.pnt-general-layout .t-layout__footer .t-button+.t-button{margin-left:.75rem}.pnt-general-layout--footer-auto .t-layout__footer .t-button{font-size:inherit;font-weight:inherit;height:auto;min-width:0}
|
|
1
|
+
.pnt-image-wrapper{position:relative}.pnt-image-wrapper .pnt-image-progress,.pnt-image-wrapper .pnt-image-upload-progress{align-items:center;display:flex;height:8.5rem;justify-content:center;width:8.5rem}.pnt-image-wrapper .pnt-image-progress .pnt-image-loading,.pnt-image-wrapper .pnt-image-upload-progress .pnt-image-loading{align-items:center;background-color:#f7f8fa;border-radius:.25rem;display:flex;flex-direction:column;height:100%;justify-content:center;text-align:center;width:100%}.pnt-image-wrapper .pnt-image-progress .pnt-image-loading svg,.pnt-image-wrapper .pnt-image-upload-progress .pnt-image-loading svg{color:var(--td-brand-color);font-size:1.5rem}.pnt-image-wrapper .pnt-image-progress .pnt-image-loading p,.pnt-image-wrapper .pnt-image-upload-progress .pnt-image-loading p{color:#808191;font-size:.75rem;margin-top:.25rem}.pnt-image-wrapper .pnt-image{height:8.5rem;width:8.5rem}.pnt-image-wrapper .pnt-image .pnt-image-mask{align-items:center;background:rgba(0,0,0,.6);color:#fff;display:flex;height:100%;justify-content:center}.pnt-image-wrapper .pnt-image .pnt-image-mask--basic{cursor:pointer}.pnt-image-wrapper .pnt-image .pnt-image-mask .pnt-image-divider{border-left:.0625rem solid var(--td-text-color-anti);height:var(--td-comp-size-xxxs);margin:0 var(--td-comp-margin-l)}.pnt-business-select{width:100%}.pnt-business-select-option-label{display:block;width:100%}.pnt-business-select-popup .t-popup__content .t-loading,.pnt-business-select-popup .t-popup__content .total-wrapper{align-items:center;display:flex;font-weight:400;height:2.25rem;line-height:1.25rem;padding:0 .875rem}.pnt-business-select-popup .t-popup__content .total-wrapper{color:#808191;font-size:.875rem}.pnt-secure-box{color:#333;max-width:-moz-fit-content;max-width:fit-content;position:relative}.pnt-secure-box .copy-icon,.pnt-secure-box .view-icon{color:#3f8cff!important;cursor:pointer;font-size:small;position:absolute;right:-1.875rem;top:.0625rem}.pnt-secure-box .security-value{position:relative;top:.1875rem}.pnt-upload{display:inline-block}.pnt-upload .pnt-upload__trigger{cursor:pointer;display:inline-block}.pnt-upload .pnt-upload__error{color:#d54941;color:var(--td-error-color,#d54941);font-size:.75rem;line-height:1.25rem;margin:.25rem 0 0}.pnt-upload--disabled .pnt-upload__trigger{cursor:not-allowed}.pnt-upload .pnt-upload-file{background-color:#fff;background-color:var(--td-bg-color-container,#fff);border:.0625rem dashed #dcdcdc;border:.0625rem dashed var(--td-component-border,#dcdcdc);border-radius:.375rem;border-radius:var(--td-radius-default,6px);box-sizing:border-box;min-width:20rem;padding:.75rem 1rem;transition:border-color .2s}.pnt-upload .pnt-upload-file:hover{border-color:#165dff;border-color:var(--td-brand-color,#165dff)}.pnt-upload .pnt-upload-file__row{align-items:center;color:#1f2329;color:var(--td-text-color-primary,#1f2329);display:flex;font-size:.875rem;gap:.5rem;line-height:1.375rem}.pnt-upload .pnt-upload-file__row--empty{color:#6b7280;color:var(--td-text-color-secondary,#6b7280);justify-content:flex-start}.pnt-upload .pnt-upload-file__row--progress{align-items:center}.pnt-upload .pnt-upload-file__icon{color:#6b7280;color:var(--td-text-color-secondary,#6b7280);flex:none;font-size:1.25rem}.pnt-upload .pnt-upload-file__name{flex:1 1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pnt-upload .pnt-upload-file__delete{color:#6b7280;color:var(--td-text-color-secondary,#6b7280);cursor:pointer;flex:none;font-size:1.125rem}.pnt-upload .pnt-upload-file__delete:hover{color:#d54941;color:var(--td-error-color,#d54941)}.pnt-upload .pnt-upload-file__progress{flex:1 1;min-width:0}.pnt-upload .pnt-upload-file__percent{color:#6b7280;color:var(--td-text-color-secondary,#6b7280);flex:none;font-size:.75rem}.pnt-upload--disabled .pnt-upload-file{background-color:#f3f3f3;background-color:var(--td-bg-color-component-disabled,#f3f3f3)}.pnt-upload--disabled .pnt-upload-file:hover{border-color:#dcdcdc;border-color:var(--td-component-border,#dcdcdc)}.pnt-general-layout{--pnt-layout-body-padding:1.5rem;--pnt-layout-card-radius:0.25rem;background-color:transparent;height:100%}.pnt-general-layout .t-layout__header{align-items:stretch;border-bottom:.0625rem solid #f0f3f6;display:flex;flex-shrink:0;height:auto;justify-content:space-between}.pnt-general-layout .t-layout__header .header-title{align-items:center;display:inline-flex;padding:1.5rem}.pnt-general-layout .t-layout__header .header-title .goback-icon{color:#3f8cff;cursor:pointer;margin-right:.75rem}.pnt-general-layout .t-layout__header .header-title .title{font-size:1.25rem;font-weight:600;line-height:1.5rem}.pnt-general-layout .t-layout__header .header-operation{align-items:center;display:flex;padding-right:1.5rem}.pnt-general-layout .t-layout__header .header-operation .t-button+.t-button{margin-left:.75rem}.pnt-general-layout--borderless .t-layout__header{border-bottom:none}.pnt-general-layout .general-layout-sub-header{border-bottom:.0625rem solid #f0f3f6;flex-shrink:0}.pnt-general-layout .general-layout-body{background:#fafafa;flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--pnt-layout-body-padding)}.pnt-general-layout .general-layout-body>div.t-card,.pnt-general-layout .general-layout-body>div.t-loading__parent>div.t-card,.pnt-general-layout .general-layout-body>div.tea-card,.pnt-general-layout .general-layout-body>div[class*=-block],.pnt-general-layout .general-layout-body>div[class*=-wrapper]{border-radius:var(--pnt-layout-card-radius);box-shadow:none;margin-bottom:1.5rem}.pnt-general-layout .general-layout-body>div.t-card .t-card__body,.pnt-general-layout .general-layout-body>div.t-loading__parent>div.t-card .t-card__body,.pnt-general-layout .general-layout-body>div.tea-card .t-card__body,.pnt-general-layout .general-layout-body>div[class*=-block] .t-card__body,.pnt-general-layout .general-layout-body>div[class*=-wrapper] .t-card__body{border-radius:var(--pnt-layout-card-radius)}.pnt-general-layout .general-layout-body>div.t-loading__parent>div:last-child.t-card,.pnt-general-layout .general-layout-body>div:last-child,.pnt-general-layout .general-layout-body>div:last-child.t-card,.pnt-general-layout .general-layout-body>div:last-child.tea-card,.pnt-general-layout .general-layout-body>div:last-child[class*=-block],.pnt-general-layout .general-layout-body>div:last-child[class*=-wrapper]{margin-bottom:0}.pnt-general-layout .t-layout__footer{background:#fff;border-top:.0625rem solid #f0f3f6;flex-shrink:0;padding:1.125rem 1.5rem}.pnt-general-layout .t-layout__footer .t-button{font-size:.875rem;font-weight:600;height:2.25rem;min-width:5.75rem}.pnt-general-layout .t-layout__footer .t-button+.t-button{margin-left:.75rem}.pnt-general-layout--footer-auto .t-layout__footer .t-button{font-size:inherit;font-weight:inherit;height:auto;min-width:0}
|