@liujitcn/kratos-taro-app-core 0.0.1

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 (77) hide show
  1. package/README.md +36 -0
  2. package/dist/build.mjs +38 -0
  3. package/dist/runner.mjs +321 -0
  4. package/package.json +51 -0
  5. package/src/api/base/config.ts +20 -0
  6. package/src/api/base/file.ts +57 -0
  7. package/src/api/base/login.ts +87 -0
  8. package/src/api/base/menu.ts +17 -0
  9. package/src/api/base/oauth.ts +149 -0
  10. package/src/api/system/auth.ts +54 -0
  11. package/src/api/system/base_area.ts +34 -0
  12. package/src/api/system/base_dict.ts +24 -0
  13. package/src/bootstrap.ts +32 -0
  14. package/src/build.ts +24 -0
  15. package/src/components/BootstrapStatus.scss +28 -0
  16. package/src/components/BootstrapStatus.tsx +25 -0
  17. package/src/components/KratosPageFrame.scss +41 -0
  18. package/src/components/KratosPageFrame.tsx +51 -0
  19. package/src/components/KratosTabBar.scss +34 -0
  20. package/src/components/KratosTabBar.tsx +45 -0
  21. package/src/index.ts +36 -0
  22. package/src/module.ts +63 -0
  23. package/src/navigation-pattern.d.mts +2 -0
  24. package/src/navigation-pattern.mjs +29 -0
  25. package/src/navigation-tree.d.mts +12 -0
  26. package/src/navigation-tree.mjs +45 -0
  27. package/src/navigation.ts +336 -0
  28. package/src/pages.ts +21 -0
  29. package/src/rpc/base/v1/config.ts +34 -0
  30. package/src/rpc/base/v1/file.ts +66 -0
  31. package/src/rpc/base/v1/login.ts +128 -0
  32. package/src/rpc/base/v1/mcp.ts +20 -0
  33. package/src/rpc/base/v1/oauth.ts +199 -0
  34. package/src/rpc/base/v1/sse.ts +22 -0
  35. package/src/rpc/common/v1/common.ts +71 -0
  36. package/src/rpc/common/v1/enum.ts +71 -0
  37. package/src/rpc/common/v1/types.ts +77 -0
  38. package/src/rpc/google/protobuf/empty.ts +19 -0
  39. package/src/rpc/google/protobuf/wrappers.ts +124 -0
  40. package/src/rpc/system/app/v1/auth.ts +54 -0
  41. package/src/rpc/system/app/v1/base_area.ts +24 -0
  42. package/src/rpc/system/app/v1/base_dict.ts +37 -0
  43. package/src/rpc/system/app/v1/base_menu.ts +61 -0
  44. package/src/rpc/system/common/v1/enum.ts +80 -0
  45. package/src/runner.ts +390 -0
  46. package/src/static/images/avatar.png +0 -0
  47. package/src/static/images/center_bg.png +0 -0
  48. package/src/static/images/logo_icon.png +0 -0
  49. package/src/static/images/navigator_bg.png +0 -0
  50. package/src/static/tabs/home_default.png +0 -0
  51. package/src/static/tabs/home_selected.png +0 -0
  52. package/src/static/tabs/user_default.png +0 -0
  53. package/src/static/tabs/user_selected.png +0 -0
  54. package/src/stores/index.ts +2 -0
  55. package/src/stores/setting.ts +43 -0
  56. package/src/stores/user.ts +152 -0
  57. package/src/styles/base.scss +45 -0
  58. package/src/types/assets.d.ts +9 -0
  59. package/src/utils/auth.ts +55 -0
  60. package/src/utils/file.ts +48 -0
  61. package/src/utils/http.ts +262 -0
  62. package/src/utils/index.ts +35 -0
  63. package/src/utils/navigation.ts +72 -0
  64. package/src/utils/passwordCrypto.ts +257 -0
  65. package/src/views/pages/index/index.scss +137 -0
  66. package/src/views/pages/index/index.tsx +74 -0
  67. package/src/views/pages/login/components/BehaviorCaptcha.h5.tsx +46 -0
  68. package/src/views/pages/login/components/BehaviorCaptcha.tsx +6 -0
  69. package/src/views/pages/login/components/BehaviorCaptcha.weapp.tsx +6 -0
  70. package/src/views/pages/login/components/types.ts +32 -0
  71. package/src/views/pages/login/login.scss +310 -0
  72. package/src/views/pages/login/login.tsx +493 -0
  73. package/src/views/pages/login/protocal.scss +84 -0
  74. package/src/views/pages/login/protocal.tsx +73 -0
  75. package/src/views/pages/status/index.tsx +42 -0
  76. package/src/views/pages/webview/webview.scss +43 -0
  77. package/src/views/pages/webview/webview.tsx +41 -0
@@ -0,0 +1,257 @@
1
+ import Taro from '@tarojs/taro'
2
+ import * as miniCrypto from 'asmcrypto.js'
3
+ import { defLoginService } from '../api/base/login'
4
+ import { PasswordCryptoScene } from '../rpc/common/v1/enum'
5
+ import type { PasswordCrypto } from '../rpc/common/v1/types'
6
+
7
+ type MiniCrypto = typeof import('asmcrypto.js')
8
+
9
+ export const PASSWORD_CRYPTO_SCENE = PasswordCryptoScene
10
+ export type { PasswordCryptoScene }
11
+
12
+ /** 将 PEM 公钥转换为二进制 DER 数据。 */
13
+ function pemToArrayBuffer(pem: string) {
14
+ const base64 = pem
15
+ .replace(/-----BEGIN PUBLIC KEY-----/g, '')
16
+ .replace(/-----END PUBLIC KEY-----/g, '')
17
+ .replace(/\s/g, '')
18
+ const decode = globalThis.atob
19
+ if (!decode) {
20
+ throw new Error('当前环境不支持 Base64 解码')
21
+ }
22
+ const binary = decode(base64)
23
+ const buffer = new Uint8Array(binary.length)
24
+ for (let i = 0; i < binary.length; i += 1) {
25
+ buffer[i] = binary.charCodeAt(i)
26
+ }
27
+ return buffer.buffer
28
+ }
29
+
30
+ /** 将二进制数据编码为 base64 字符串。 */
31
+ function arrayBufferToBase64(buffer: ArrayBuffer) {
32
+ const bytes = new Uint8Array(buffer)
33
+ let binary = ''
34
+ for (let i = 0; i < bytes.byteLength; i += 1) {
35
+ binary += String.fromCharCode(bytes[i])
36
+ }
37
+ const encode = globalThis.btoa
38
+ if (!encode) {
39
+ throw new Error('当前环境不支持 Base64 编码')
40
+ }
41
+ return encode(binary)
42
+ }
43
+
44
+ /** 生成纯 JavaScript 密码加密所需的随机字节。 */
45
+ function getRandomBytes(length: number) {
46
+ const cryptoApi = globalThis.crypto
47
+ if (cryptoApi?.getRandomValues) {
48
+ return Promise.resolve(cryptoApi.getRandomValues(new Uint8Array(length)).buffer)
49
+ }
50
+ return Taro.getRandomValues({ length }).then(({ randomValues }) => randomValues)
51
+ }
52
+
53
+ type DerNode = {
54
+ tag: number
55
+ value: Uint8Array
56
+ next: number
57
+ }
58
+
59
+ /** 读取 DER 编码节点,用于解析 RSA SubjectPublicKeyInfo。 */
60
+ function readDerNode(data: Uint8Array, offset: number): DerNode {
61
+ if (offset + 2 > data.length) {
62
+ throw new Error('公钥格式无效')
63
+ }
64
+ const tag = data[offset]
65
+ const firstLengthByte = data[offset + 1]
66
+ let length = firstLengthByte
67
+ let valueOffset = offset + 2
68
+ if (firstLengthByte & 0x80) {
69
+ const lengthByteCount = firstLengthByte & 0x7f
70
+ if (lengthByteCount === 0 || valueOffset + lengthByteCount > data.length) {
71
+ throw new Error('公钥格式无效')
72
+ }
73
+ length = 0
74
+ for (let index = 0; index < lengthByteCount; index += 1) {
75
+ length = length * 256 + data[valueOffset + index]
76
+ }
77
+ valueOffset += lengthByteCount
78
+ }
79
+ const end = valueOffset + length
80
+ if (end > data.length) {
81
+ throw new Error('公钥格式无效')
82
+ }
83
+ return {
84
+ tag,
85
+ value: data.subarray(valueOffset, end),
86
+ next: end,
87
+ }
88
+ }
89
+
90
+ /** 去除 DER 正整数前导零字节。 */
91
+ function trimDerInteger(value: Uint8Array) {
92
+ let offset = 0
93
+ while (offset < value.length - 1 && value[offset] === 0) {
94
+ offset += 1
95
+ }
96
+ return value.subarray(offset)
97
+ }
98
+
99
+ /** 从 PEM SubjectPublicKeyInfo 中提取 RSA 模数和指数。 */
100
+ function parseMiniRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
101
+ const base64 = publicKey
102
+ .replace(/-----BEGIN PUBLIC KEY-----/g, '')
103
+ .replace(/-----END PUBLIC KEY-----/g, '')
104
+ .replace(/\s/g, '')
105
+ const der = new Uint8Array(Taro.base64ToArrayBuffer(base64))
106
+ const subjectPublicKeyInfo = readDerNode(der, 0)
107
+ const algorithm = readDerNode(subjectPublicKeyInfo.value, 0)
108
+ const bitString = readDerNode(subjectPublicKeyInfo.value, algorithm.next)
109
+ if (bitString.tag !== 0x03 || bitString.value[0] !== 0) {
110
+ throw new Error('公钥格式无效')
111
+ }
112
+ const rsaPublicKey = readDerNode(bitString.value, 1)
113
+ const modulus = readDerNode(rsaPublicKey.value, 0)
114
+ const exponent = readDerNode(rsaPublicKey.value, modulus.next)
115
+ if (modulus.tag !== 0x02 || exponent.tag !== 0x02) {
116
+ throw new Error('公钥格式无效')
117
+ }
118
+ return [trimDerInteger(modulus.value), trimDerInteger(exponent.value)]
119
+ }
120
+
121
+ /** 计算 RSA-OAEP 所需的 MGF1 掩码。 */
122
+ function generateMgf1Mask(crypto: MiniCrypto, seed: Uint8Array, length: number) {
123
+ const mask = new Uint8Array(length)
124
+ const counter = new Uint8Array(4)
125
+ const hash = new crypto.Sha256()
126
+ const blockCount = Math.ceil(length / hash.HASH_SIZE)
127
+ for (let index = 0; index < blockCount; index += 1) {
128
+ counter[0] = index >>> 24
129
+ counter[1] = index >>> 16
130
+ counter[2] = index >>> 8
131
+ counter[3] = index
132
+ let block = hash.reset().process(seed).process(counter).finish().result
133
+ if (!block) {
134
+ throw new Error('密码加密失败')
135
+ }
136
+ const target = mask.subarray(index * hash.HASH_SIZE)
137
+ if (block.length > target.length) {
138
+ block = block.subarray(0, target.length)
139
+ }
140
+ target.set(block)
141
+ }
142
+ return mask
143
+ }
144
+
145
+ /** 将字节数组编码为当前运行端可提交的 Base64。 */
146
+ function miniBytesToBase64(value: Uint8Array) {
147
+ const buffer = value.slice().buffer
148
+ return process.env.TARO_ENV === 'h5'
149
+ ? arrayBufferToBase64(buffer)
150
+ : Taro.arrayBufferToBase64(buffer)
151
+ }
152
+
153
+ /** 使用纯 JavaScript 实现兼容缺少 WebCrypto 的运行时。 */
154
+ async function encryptPortablePassword(
155
+ crypto: MiniCrypto,
156
+ password: string,
157
+ publicKeyResponse: {
158
+ key_id: string
159
+ public_key: string
160
+ algorithm: string
161
+ nonce: string
162
+ },
163
+ ): Promise<PasswordCrypto> {
164
+ const aesKey = new Uint8Array(await getRandomBytes(32))
165
+ const iv = new Uint8Array(await getRandomBytes(12))
166
+ const plaintext = crypto.string_to_bytes(password, true)
167
+ const [modulus, exponent] = parseMiniRsaPublicKey(publicKeyResponse.public_key)
168
+ const hash = new crypto.Sha256()
169
+ const keySize = Math.ceil(new crypto.BigNumber(modulus).bitLength / 8)
170
+ const seed = new Uint8Array(await getRandomBytes(hash.HASH_SIZE))
171
+ const dataBlockLength = keySize - hash.HASH_SIZE - 1
172
+ const paddingLength = dataBlockLength - aesKey.length - hash.HASH_SIZE - 1
173
+ if (paddingLength < 0) {
174
+ throw new Error('密码长度超出限制')
175
+ }
176
+ const encodedMessage = new Uint8Array(keySize)
177
+ const maskedSeed = encodedMessage.subarray(1, hash.HASH_SIZE + 1)
178
+ const dataBlock = encodedMessage.subarray(hash.HASH_SIZE + 1)
179
+ const labelHash = hash.reset().process(new Uint8Array()).finish().result
180
+ if (!labelHash) {
181
+ throw new Error('密码加密失败')
182
+ }
183
+ dataBlock.set(labelHash, 0)
184
+ dataBlock.set(aesKey, hash.HASH_SIZE + paddingLength + 1)
185
+ dataBlock[hash.HASH_SIZE + paddingLength] = 1
186
+ const dataBlockMask = generateMgf1Mask(crypto, seed, dataBlock.length)
187
+ for (let index = 0; index < dataBlock.length; index += 1) {
188
+ dataBlock[index] ^= dataBlockMask[index]
189
+ }
190
+ const seedMask = generateMgf1Mask(crypto, dataBlock, maskedSeed.length)
191
+ maskedSeed.set(seed)
192
+ for (let index = 0; index < maskedSeed.length; index += 1) {
193
+ maskedSeed[index] ^= seedMask[index]
194
+ }
195
+ const encryptedKey = new crypto.RSA([modulus, exponent]).encrypt(
196
+ new crypto.BigNumber(encodedMessage),
197
+ ).result
198
+ if (!encryptedKey) {
199
+ throw new Error('密码加密失败')
200
+ }
201
+ const ciphertext = crypto.AES_GCM.encrypt(plaintext, aesKey, iv)
202
+ return {
203
+ key_id: publicKeyResponse.key_id,
204
+ nonce: publicKeyResponse.nonce,
205
+ algorithm: publicKeyResponse.algorithm,
206
+ encrypted_key: miniBytesToBase64(encryptedKey),
207
+ iv: miniBytesToBase64(iv),
208
+ ciphertext: miniBytesToBase64(ciphertext),
209
+ }
210
+ }
211
+
212
+ /** 加密单个密码字段,返回后端可解析的密码密文。 */
213
+ export async function encryptPassword(
214
+ password: string,
215
+ scene: PasswordCryptoScene,
216
+ ): Promise<PasswordCrypto> {
217
+ const plainPassword = password.trim()
218
+ if (!plainPassword) {
219
+ throw new Error('密码不能为空')
220
+ }
221
+
222
+ const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene })
223
+
224
+ const cryptoApi = globalThis.crypto
225
+ // 微信小程序和局域网 HTTP 页面没有 WebCrypto,使用纯 JavaScript 实现保持协议一致。
226
+ if (process.env.TARO_ENV === 'weapp' || !cryptoApi?.subtle) {
227
+ return encryptPortablePassword(miniCrypto, plainPassword, publicKeyResponse)
228
+ }
229
+
230
+ const publicKey = await cryptoApi.subtle.importKey(
231
+ 'spki',
232
+ pemToArrayBuffer(publicKeyResponse.public_key),
233
+ { name: 'RSA-OAEP', hash: 'SHA-256' },
234
+ false,
235
+ ['encrypt'],
236
+ )
237
+ const aesKey = await cryptoApi.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
238
+ 'encrypt',
239
+ ])
240
+ const rawAesKey = await cryptoApi.subtle.exportKey('raw', aesKey)
241
+ const iv = cryptoApi.getRandomValues(new Uint8Array(12))
242
+ const ciphertext = await cryptoApi.subtle.encrypt(
243
+ { name: 'AES-GCM', iv },
244
+ aesKey,
245
+ new TextEncoder().encode(plainPassword),
246
+ )
247
+ const encryptedKey = await cryptoApi.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, rawAesKey)
248
+
249
+ return {
250
+ key_id: publicKeyResponse.key_id,
251
+ nonce: publicKeyResponse.nonce,
252
+ algorithm: publicKeyResponse.algorithm,
253
+ encrypted_key: arrayBufferToBase64(encryptedKey),
254
+ iv: arrayBufferToBase64(iv.buffer),
255
+ ciphertext: arrayBufferToBase64(ciphertext),
256
+ }
257
+ }
@@ -0,0 +1,137 @@
1
+ .home-page {
2
+ min-height: 100vh;
3
+ padding: 44px 28px 156px;
4
+ box-sizing: border-box;
5
+ background-color: #f7f7f8;
6
+ }
7
+
8
+ .home-page--weapp {
9
+ padding-top: calc(44px + 44PX);
10
+ }
11
+
12
+ .home-hero {
13
+ display: flex;
14
+ align-items: center;
15
+ padding: 34px 28px;
16
+ border-radius: 12px;
17
+ background-color: #27ba9b;
18
+ box-shadow: 0 8px 18px rgba(39, 186, 155, 0.18);
19
+ }
20
+
21
+ .home-logo {
22
+ flex: 0 0 auto;
23
+ width: 92px;
24
+ height: 92px;
25
+ border-radius: 20px;
26
+ background-color: #fff;
27
+ }
28
+
29
+ .home-hero__copy {
30
+ min-width: 0;
31
+ margin-left: 22px;
32
+ }
33
+
34
+ .home-title,
35
+ .home-subtitle {
36
+ display: block;
37
+ }
38
+
39
+ .home-title {
40
+ color: #fff;
41
+ font-size: 40px;
42
+ font-weight: 600;
43
+ }
44
+
45
+ .home-subtitle {
46
+ margin-top: 10px;
47
+ color: rgba(255, 255, 255, 0.86);
48
+ font-size: 24px;
49
+ }
50
+
51
+ .home-section {
52
+ margin-top: 24px;
53
+ }
54
+
55
+ .home-section__title {
56
+ display: block;
57
+ margin: 0 8px 14px;
58
+ color: #26332f;
59
+ font-size: 28px;
60
+ font-weight: 600;
61
+ }
62
+
63
+ .home-info-list,
64
+ .home-demo-list {
65
+ overflow: hidden;
66
+ border-radius: 10px;
67
+ background-color: #fff;
68
+ }
69
+
70
+ .home-info-row {
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: space-between;
74
+ padding: 26px 24px;
75
+ border-bottom: 1px solid #edf1ef;
76
+
77
+ &:last-child {
78
+ border-bottom: 0;
79
+ }
80
+ }
81
+
82
+ .home-info-label {
83
+ color: #66736f;
84
+ font-size: 26px;
85
+ }
86
+
87
+ .home-info-value {
88
+ color: #26332f;
89
+ font-size: 26px;
90
+ font-weight: 500;
91
+ }
92
+
93
+ .home-demo-item {
94
+ display: flex;
95
+ align-items: flex-start;
96
+ padding: 26px 24px;
97
+ border-bottom: 1px solid #edf1ef;
98
+
99
+ &:last-child {
100
+ border-bottom: 0;
101
+ }
102
+ }
103
+
104
+ .home-demo-dot {
105
+ flex: 0 0 auto;
106
+ width: 42px;
107
+ height: 42px;
108
+ border-radius: 50%;
109
+ background-color: #e8f8f4;
110
+ color: #16806d;
111
+ font-size: 24px;
112
+ line-height: 42px;
113
+ text-align: center;
114
+ }
115
+
116
+ .home-demo-copy {
117
+ min-width: 0;
118
+ margin-left: 18px;
119
+ }
120
+
121
+ .home-demo-title,
122
+ .home-demo-desc {
123
+ display: block;
124
+ }
125
+
126
+ .home-demo-title {
127
+ color: #26332f;
128
+ font-size: 28px;
129
+ font-weight: 500;
130
+ }
131
+
132
+ .home-demo-desc {
133
+ margin-top: 8px;
134
+ color: #8a9691;
135
+ font-size: 24px;
136
+ line-height: 1.5;
137
+ }
@@ -0,0 +1,74 @@
1
+ import { Image, Text, View } from '@tarojs/components'
2
+ import { useLoad } from '@tarojs/taro'
3
+ import { resolveBundledAsset } from '../../../module'
4
+ import { useSettingStore } from '../../../stores'
5
+ import './index.scss'
6
+
7
+ const defaultLogo = resolveBundledAsset('static/images/logo_icon.png')
8
+
9
+ /** 应用首页。 */
10
+ export default function HomePage() {
11
+ const settings = useSettingStore((state) => state.data)
12
+ const loadData = useSettingStore((state) => state.loadData)
13
+ const mainTitle = settings?.get('mainTitle') || '应用框架示例'
14
+ const subTitle = settings?.get('subTitle') || '保留通用导航与个人中心体验'
15
+ const appLogo = settings?.get('appLogo') || defaultLogo
16
+ useLoad(() => {
17
+ void loadData().catch(() => undefined)
18
+ })
19
+
20
+ const stack = [
21
+ ['应用框架', 'Taro + React 18'],
22
+ ['开发语言', 'TypeScript'],
23
+ ['状态管理', 'Zustand'],
24
+ ['样式方案', 'Sass + px'],
25
+ ]
26
+ const demos = [
27
+ ['跨端页面', '同一套页面代码适配 H5 和微信小程序'],
28
+ ['账户能力', '个人资料、应用设置与智能助手入口已保留'],
29
+ ['静态首页', '当前首页用于展示框架信息,不依赖业务接口'],
30
+ ]
31
+
32
+ return (
33
+ <View className={`home-page${process.env.TARO_ENV === 'weapp' ? ' home-page--weapp' : ''}`}>
34
+ <View className='home-hero'>
35
+ <Image
36
+ className='home-logo'
37
+ src={appLogo}
38
+ mode='aspectFit'
39
+ />
40
+ <View className='home-hero__copy'>
41
+ <Text className='home-title'>{mainTitle}</Text>
42
+ <Text className='home-subtitle'>{subTitle}</Text>
43
+ </View>
44
+ </View>
45
+
46
+ <View className='home-section'>
47
+ <Text className='home-section__title'>当前技术栈</Text>
48
+ <View className='home-info-list'>
49
+ {stack.map(([label, value]) => (
50
+ <View className='home-info-row' key={label}>
51
+ <Text className='home-info-label'>{label}</Text>
52
+ <Text className='home-info-value'>{value}</Text>
53
+ </View>
54
+ ))}
55
+ </View>
56
+ </View>
57
+
58
+ <View className='home-section'>
59
+ <Text className='home-section__title'>演示范围</Text>
60
+ <View className='home-demo-list'>
61
+ {demos.map(([title, description], index) => (
62
+ <View className='home-demo-item' key={title}>
63
+ <View className='home-demo-dot'>{index + 1}</View>
64
+ <View className='home-demo-copy'>
65
+ <Text className='home-demo-title'>{title}</Text>
66
+ <Text className='home-demo-desc'>{description}</Text>
67
+ </View>
68
+ </View>
69
+ ))}
70
+ </View>
71
+ </View>
72
+ </View>
73
+ )
74
+ }
@@ -0,0 +1,46 @@
1
+ import GoCaptcha from 'go-captcha-react'
2
+ import 'go-captcha-react/dist/go-captcha-react.cjs.development.css'
3
+ import type { BehaviorCaptchaProps } from './types'
4
+
5
+ /** H5 行为验证码适配器。 */
6
+ export default function BehaviorCaptcha({
7
+ type,
8
+ data,
9
+ config,
10
+ onConfirm,
11
+ onRefresh,
12
+ onClose,
13
+ }: BehaviorCaptchaProps) {
14
+ const events = { confirm: onConfirm, refresh: onRefresh, close: onClose }
15
+ if (type === 'click') {
16
+ return <GoCaptcha.Click data={{ image: data.image, thumb: data.thumb }} config={config} events={events} />
17
+ }
18
+ if (type === 'rotate') {
19
+ return (
20
+ <GoCaptcha.Rotate
21
+ data={{
22
+ image: data.image,
23
+ thumb: data.thumb,
24
+ thumbSize: data.thumbSize ?? 150,
25
+ angle: data.angle,
26
+ }}
27
+ config={config}
28
+ events={events}
29
+ />
30
+ )
31
+ }
32
+ return (
33
+ <GoCaptcha.Slide
34
+ data={{
35
+ image: data.image,
36
+ thumb: data.thumb,
37
+ thumbX: data.thumbX ?? 0,
38
+ thumbY: data.thumbY ?? 0,
39
+ thumbWidth: data.thumbWidth ?? 60,
40
+ thumbHeight: data.thumbHeight ?? 60,
41
+ }}
42
+ config={config}
43
+ events={events}
44
+ />
45
+ )
46
+ }
@@ -0,0 +1,6 @@
1
+ import type { BehaviorCaptchaProps } from './types'
2
+
3
+ /** 非 H5 平台不渲染浏览器行为验证码。 */
4
+ export default function BehaviorCaptcha(_props: BehaviorCaptchaProps) {
5
+ return null
6
+ }
@@ -0,0 +1,6 @@
1
+ import type { BehaviorCaptchaProps } from './types'
2
+
3
+ /** 微信端不渲染 H5 行为验证码。 */
4
+ export default function BehaviorCaptcha(_props: BehaviorCaptchaProps) {
5
+ return null
6
+ }
@@ -0,0 +1,32 @@
1
+ /** 行为验证码展示数据。 */
2
+ export interface BehaviorCaptchaData {
3
+ image: string
4
+ thumb: string
5
+ thumbX?: number
6
+ thumbY?: number
7
+ thumbWidth?: number
8
+ thumbHeight?: number
9
+ thumbSize?: number
10
+ angle?: number
11
+ }
12
+
13
+ /** 行为验证码点位。 */
14
+ export interface BehaviorCaptchaPoint {
15
+ x: number
16
+ y: number
17
+ key?: number
18
+ index?: number
19
+ }
20
+
21
+ /** 行为验证码组件参数。 */
22
+ export interface BehaviorCaptchaProps {
23
+ type: string
24
+ data: BehaviorCaptchaData
25
+ config: Record<string, string | number | boolean>
26
+ onConfirm: (
27
+ value: BehaviorCaptchaPoint | BehaviorCaptchaPoint[] | number,
28
+ reset: () => void,
29
+ ) => void
30
+ onRefresh: () => void
31
+ onClose: () => void
32
+ }