@liujitcn/kratos-uni-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.
- package/README.md +46 -0
- package/dist/module.mjs +84 -0
- package/dist/vite.mjs +405 -0
- package/package.json +58 -0
- package/src/api/base/config.ts +20 -0
- package/src/api/base/file.ts +79 -0
- package/src/api/base/login.ts +87 -0
- package/src/api/base/menu.ts +17 -0
- package/src/api/base/oauth.ts +149 -0
- package/src/api/system/auth.ts +54 -0
- package/src/api/system/base_area.ts +34 -0
- package/src/api/system/base_dict.ts +24 -0
- package/src/bootstrap.ts +23 -0
- package/src/components/BootstrapStatus.vue +39 -0
- package/src/components/KratosTabBar.vue +73 -0
- package/src/index.ts +6 -0
- package/src/module.ts +85 -0
- package/src/navigation-pattern.d.mts +2 -0
- package/src/navigation-pattern.mjs +29 -0
- package/src/navigation-tree.mjs +45 -0
- package/src/navigation.ts +330 -0
- package/src/pages.ts +26 -0
- package/src/rpc/base/v1/config.ts +34 -0
- package/src/rpc/base/v1/file.ts +66 -0
- package/src/rpc/base/v1/login.ts +128 -0
- package/src/rpc/base/v1/mcp.ts +20 -0
- package/src/rpc/base/v1/oauth.ts +199 -0
- package/src/rpc/base/v1/sse.ts +22 -0
- package/src/rpc/common/v1/common.ts +71 -0
- package/src/rpc/common/v1/enum.ts +71 -0
- package/src/rpc/common/v1/types.ts +77 -0
- package/src/rpc/google/protobuf/empty.ts +19 -0
- package/src/rpc/google/protobuf/wrappers.ts +124 -0
- package/src/rpc/system/app/v1/auth.ts +54 -0
- package/src/rpc/system/app/v1/base_area.ts +24 -0
- package/src/rpc/system/app/v1/base_dict.ts +37 -0
- package/src/rpc/system/app/v1/base_menu.ts +61 -0
- package/src/rpc/system/common/v1/enum.ts +80 -0
- package/src/static/images/avatar.png +0 -0
- package/src/static/images/center_bg.png +0 -0
- package/src/static/images/logo_icon.png +0 -0
- package/src/static/images/navigator_bg.png +0 -0
- package/src/static/tabs/home_default.png +0 -0
- package/src/static/tabs/home_selected.png +0 -0
- package/src/static/tabs/user_default.png +0 -0
- package/src/static/tabs/user_selected.png +0 -0
- package/src/stores/index.ts +14 -0
- package/src/stores/modules/setting.ts +56 -0
- package/src/stores/modules/user.ts +266 -0
- package/src/styles/base.scss +42 -0
- package/src/types/env.d.ts +24 -0
- package/src/types/go-captcha-uni.d.ts +7 -0
- package/src/types/uni-helper.d.ts +54 -0
- package/src/uni.scss +76 -0
- package/src/utils/auth.ts +67 -0
- package/src/utils/file.ts +95 -0
- package/src/utils/http.ts +430 -0
- package/src/utils/index.ts +79 -0
- package/src/utils/navigation.ts +81 -0
- package/src/utils/passwordCrypto.ts +275 -0
- package/src/views/pages/index/index.vue +227 -0
- package/src/views/pages/login/login.vue +1133 -0
- package/src/views/pages/login/protocal.vue +176 -0
- package/src/views/pages/status/index.vue +36 -0
- package/src/views/pages/webview/webview.vue +136 -0
- package/src/vite.ts +474 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** 登录前暂存的目标路由。 */
|
|
2
|
+
const LAST_ROUTE_KEY = 'lastRoute'
|
|
3
|
+
/** 登录页路径。 */
|
|
4
|
+
const LOGIN_PAGE = '/pages/login/login'
|
|
5
|
+
/** 首页 tab 页面路径。 */
|
|
6
|
+
export const homeTabPage = '/pages/index/index'
|
|
7
|
+
|
|
8
|
+
/** 路由 query 支持的值类型。 */
|
|
9
|
+
type RouteQueryValue = string | number | boolean | null | undefined
|
|
10
|
+
|
|
11
|
+
/** 构建包含 query 的页面路由。 */
|
|
12
|
+
const buildRouteUrl = (path: string, query: Record<string, RouteQueryValue> = {}) => {
|
|
13
|
+
const queryString = Object.entries(query)
|
|
14
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
15
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
|
16
|
+
.join('&')
|
|
17
|
+
|
|
18
|
+
return queryString ? `${path}?${queryString}` : path
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 获取当前页面的完整路由,包含 query,供登录前回跳使用。 */
|
|
22
|
+
const getCurrentRouteUrl = () => {
|
|
23
|
+
const pages = getCurrentPages()
|
|
24
|
+
const currentPage = pages[pages.length - 1]
|
|
25
|
+
if (!currentPage?.route) {
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let query: Record<string, RouteQueryValue> = {}
|
|
30
|
+
const miniProgramPage = currentPage as { options?: Record<string, string> }
|
|
31
|
+
const routePage = currentPage as {
|
|
32
|
+
$vm?: { $route?: { query?: Record<string, RouteQueryValue> } }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// #ifdef MP-WEIXIN
|
|
36
|
+
query = miniProgramPage.options || {}
|
|
37
|
+
// #endif
|
|
38
|
+
|
|
39
|
+
// #ifdef H5 || APP-PLUS
|
|
40
|
+
query = routePage.$vm?.$route?.query || {}
|
|
41
|
+
// #endif
|
|
42
|
+
|
|
43
|
+
return buildRouteUrl(`/${currentPage.route}`, query)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 保存当前页面路由,登录成功后恢复到用户原来的页面。 */
|
|
47
|
+
export const saveCurrentRoute = () => {
|
|
48
|
+
const currentRoute = getCurrentRouteUrl()
|
|
49
|
+
if (!currentRoute || currentRoute.startsWith(LOGIN_PAGE)) {
|
|
50
|
+
uni.removeStorageSync(LAST_ROUTE_KEY)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
uni.setStorageSync(LAST_ROUTE_KEY, currentRoute)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 保存指定页面路由,供登录成功后继续原访问目标。 */
|
|
57
|
+
export const saveLoginRedirectUrl = (url: string) => {
|
|
58
|
+
const normalizedUrl = url.startsWith('/') ? url : `/${url}`
|
|
59
|
+
if (!normalizedUrl.startsWith(LOGIN_PAGE)) {
|
|
60
|
+
uni.setStorageSync(LAST_ROUTE_KEY, normalizedUrl)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 跳转到登录页,并在跳转前记录当前页面。 */
|
|
65
|
+
export const navigateToLogin = (redirectUrl?: string) => {
|
|
66
|
+
// 模板直接绑定时会传入点击事件对象,只有字符串才是明确的回跳地址。
|
|
67
|
+
if (typeof redirectUrl === 'string' && redirectUrl) {
|
|
68
|
+
saveLoginRedirectUrl(redirectUrl)
|
|
69
|
+
} else {
|
|
70
|
+
saveCurrentRoute()
|
|
71
|
+
}
|
|
72
|
+
uni.navigateTo({
|
|
73
|
+
url: LOGIN_PAGE,
|
|
74
|
+
fail: () => {
|
|
75
|
+
uni.reLaunch({ url: LOGIN_PAGE })
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 返回首页。 */
|
|
81
|
+
export const switchTabToHome = () => uni.reLaunch({ url: homeTabPage })
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { defLoginService } from '../api/base/login'
|
|
2
|
+
import { PasswordCryptoScene } from '../rpc/common/v1/enum'
|
|
3
|
+
import type { PasswordCrypto } from '../rpc/common/v1/types'
|
|
4
|
+
|
|
5
|
+
// #ifdef MP-WEIXIN
|
|
6
|
+
import * as miniCrypto from 'asmcrypto.js'
|
|
7
|
+
// #endif
|
|
8
|
+
|
|
9
|
+
type MiniCrypto = typeof import('asmcrypto.js')
|
|
10
|
+
|
|
11
|
+
export const PASSWORD_CRYPTO_SCENE = PasswordCryptoScene
|
|
12
|
+
export type { PasswordCryptoScene }
|
|
13
|
+
|
|
14
|
+
/** 将 PEM 公钥转换为二进制 DER 数据。 */
|
|
15
|
+
function pemToArrayBuffer(pem: string) {
|
|
16
|
+
const base64 = pem
|
|
17
|
+
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
18
|
+
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
19
|
+
.replace(/\s/g, '')
|
|
20
|
+
const decode = globalThis.atob
|
|
21
|
+
if (!decode) {
|
|
22
|
+
throw new Error('当前环境不支持 Base64 解码')
|
|
23
|
+
}
|
|
24
|
+
const binary = decode(base64)
|
|
25
|
+
const buffer = new Uint8Array(binary.length)
|
|
26
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
27
|
+
buffer[i] = binary.charCodeAt(i)
|
|
28
|
+
}
|
|
29
|
+
return buffer.buffer
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 将二进制数据编码为 base64 字符串。 */
|
|
33
|
+
function arrayBufferToBase64(buffer: ArrayBuffer) {
|
|
34
|
+
const bytes = new Uint8Array(buffer)
|
|
35
|
+
let binary = ''
|
|
36
|
+
for (let i = 0; i < bytes.byteLength; i += 1) {
|
|
37
|
+
binary += String.fromCharCode(bytes[i])
|
|
38
|
+
}
|
|
39
|
+
const encode = globalThis.btoa
|
|
40
|
+
if (!encode) {
|
|
41
|
+
throw new Error('当前环境不支持 Base64 编码')
|
|
42
|
+
}
|
|
43
|
+
return encode(binary)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 获取浏览器 WebCrypto 能力。 */
|
|
47
|
+
function getSubtleCrypto() {
|
|
48
|
+
const cryptoApi = globalThis.crypto
|
|
49
|
+
if (!cryptoApi?.subtle) {
|
|
50
|
+
throw new Error('当前环境不支持密码加密')
|
|
51
|
+
}
|
|
52
|
+
return cryptoApi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 生成小程序端密码加密所需的随机字节。 */
|
|
56
|
+
function getMiniRandomBytes(length: number) {
|
|
57
|
+
return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
58
|
+
if (typeof wx === 'undefined' || typeof wx.getRandomValues !== 'function') {
|
|
59
|
+
reject(new Error('当前环境不支持密码加密'))
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
wx.getRandomValues({
|
|
63
|
+
length,
|
|
64
|
+
success: ({ randomValues }) => {
|
|
65
|
+
resolve(randomValues)
|
|
66
|
+
},
|
|
67
|
+
fail: ({ errMsg }) => {
|
|
68
|
+
reject(new Error(errMsg || '随机数生成失败'))
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type DerNode = {
|
|
75
|
+
tag: number
|
|
76
|
+
value: Uint8Array
|
|
77
|
+
next: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 读取 DER 编码节点,用于解析 RSA SubjectPublicKeyInfo。 */
|
|
81
|
+
function readDerNode(data: Uint8Array, offset: number): DerNode {
|
|
82
|
+
if (offset + 2 > data.length) {
|
|
83
|
+
throw new Error('公钥格式无效')
|
|
84
|
+
}
|
|
85
|
+
const tag = data[offset]
|
|
86
|
+
const firstLengthByte = data[offset + 1]
|
|
87
|
+
let length = firstLengthByte
|
|
88
|
+
let valueOffset = offset + 2
|
|
89
|
+
if (firstLengthByte & 0x80) {
|
|
90
|
+
const lengthByteCount = firstLengthByte & 0x7f
|
|
91
|
+
if (lengthByteCount === 0 || valueOffset + lengthByteCount > data.length) {
|
|
92
|
+
throw new Error('公钥格式无效')
|
|
93
|
+
}
|
|
94
|
+
length = 0
|
|
95
|
+
for (let index = 0; index < lengthByteCount; index += 1) {
|
|
96
|
+
length = length * 256 + data[valueOffset + index]
|
|
97
|
+
}
|
|
98
|
+
valueOffset += lengthByteCount
|
|
99
|
+
}
|
|
100
|
+
const end = valueOffset + length
|
|
101
|
+
if (end > data.length) {
|
|
102
|
+
throw new Error('公钥格式无效')
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
tag,
|
|
106
|
+
value: data.subarray(valueOffset, end),
|
|
107
|
+
next: end,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 去除 DER 正整数前导零字节。 */
|
|
112
|
+
function trimDerInteger(value: Uint8Array) {
|
|
113
|
+
let offset = 0
|
|
114
|
+
while (offset < value.length - 1 && value[offset] === 0) {
|
|
115
|
+
offset += 1
|
|
116
|
+
}
|
|
117
|
+
return value.subarray(offset)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 从 PEM SubjectPublicKeyInfo 中提取 RSA 模数和指数。 */
|
|
121
|
+
function parseMiniRsaPublicKey(publicKey: string): [Uint8Array, Uint8Array] {
|
|
122
|
+
const base64 = publicKey
|
|
123
|
+
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
|
|
124
|
+
.replace(/-----END PUBLIC KEY-----/g, '')
|
|
125
|
+
.replace(/\s/g, '')
|
|
126
|
+
const der = new Uint8Array(wx.base64ToArrayBuffer(base64))
|
|
127
|
+
const subjectPublicKeyInfo = readDerNode(der, 0)
|
|
128
|
+
const algorithm = readDerNode(subjectPublicKeyInfo.value, 0)
|
|
129
|
+
const bitString = readDerNode(subjectPublicKeyInfo.value, algorithm.next)
|
|
130
|
+
if (bitString.tag !== 0x03 || bitString.value[0] !== 0) {
|
|
131
|
+
throw new Error('公钥格式无效')
|
|
132
|
+
}
|
|
133
|
+
const rsaPublicKey = readDerNode(bitString.value, 1)
|
|
134
|
+
const modulus = readDerNode(rsaPublicKey.value, 0)
|
|
135
|
+
const exponent = readDerNode(rsaPublicKey.value, modulus.next)
|
|
136
|
+
if (modulus.tag !== 0x02 || exponent.tag !== 0x02) {
|
|
137
|
+
throw new Error('公钥格式无效')
|
|
138
|
+
}
|
|
139
|
+
return [trimDerInteger(modulus.value), trimDerInteger(exponent.value)]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 计算 RSA-OAEP 所需的 MGF1 掩码。 */
|
|
143
|
+
function generateMgf1Mask(crypto: MiniCrypto, seed: Uint8Array, length: number) {
|
|
144
|
+
const mask = new Uint8Array(length)
|
|
145
|
+
const counter = new Uint8Array(4)
|
|
146
|
+
const hash = new crypto.Sha256()
|
|
147
|
+
const blockCount = Math.ceil(length / hash.HASH_SIZE)
|
|
148
|
+
for (let index = 0; index < blockCount; index += 1) {
|
|
149
|
+
counter[0] = index >>> 24
|
|
150
|
+
counter[1] = index >>> 16
|
|
151
|
+
counter[2] = index >>> 8
|
|
152
|
+
counter[3] = index
|
|
153
|
+
let block = hash.reset().process(seed).process(counter).finish().result
|
|
154
|
+
if (!block) {
|
|
155
|
+
throw new Error('密码加密失败')
|
|
156
|
+
}
|
|
157
|
+
const target = mask.subarray(index * hash.HASH_SIZE)
|
|
158
|
+
if (block.length > target.length) {
|
|
159
|
+
block = block.subarray(0, target.length)
|
|
160
|
+
}
|
|
161
|
+
target.set(block)
|
|
162
|
+
}
|
|
163
|
+
return mask
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 将字节数组编码为小程序可提交的 Base64。 */
|
|
167
|
+
function miniBytesToBase64(value: Uint8Array) {
|
|
168
|
+
return wx.arrayBufferToBase64(value.slice().buffer)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 使用纯 JavaScript 加密实现兼容微信小程序运行时的密码加密协议。 */
|
|
172
|
+
async function encryptMiniProgramPassword(
|
|
173
|
+
crypto: MiniCrypto,
|
|
174
|
+
password: string,
|
|
175
|
+
publicKeyResponse: {
|
|
176
|
+
key_id: string
|
|
177
|
+
public_key: string
|
|
178
|
+
algorithm: string
|
|
179
|
+
nonce: string
|
|
180
|
+
},
|
|
181
|
+
): Promise<PasswordCrypto> {
|
|
182
|
+
const aesKey = new Uint8Array(await getMiniRandomBytes(32))
|
|
183
|
+
const iv = new Uint8Array(await getMiniRandomBytes(12))
|
|
184
|
+
const plaintext = crypto.string_to_bytes(password, true)
|
|
185
|
+
const [modulus, exponent] = parseMiniRsaPublicKey(publicKeyResponse.public_key)
|
|
186
|
+
const hash = new crypto.Sha256()
|
|
187
|
+
const keySize = Math.ceil(new crypto.BigNumber(modulus).bitLength / 8)
|
|
188
|
+
const seed = new Uint8Array(await getMiniRandomBytes(hash.HASH_SIZE))
|
|
189
|
+
const dataBlockLength = keySize - hash.HASH_SIZE - 1
|
|
190
|
+
const paddingLength = dataBlockLength - aesKey.length - hash.HASH_SIZE - 1
|
|
191
|
+
if (paddingLength < 0) {
|
|
192
|
+
throw new Error('密码长度超出限制')
|
|
193
|
+
}
|
|
194
|
+
const encodedMessage = new Uint8Array(keySize)
|
|
195
|
+
const maskedSeed = encodedMessage.subarray(1, hash.HASH_SIZE + 1)
|
|
196
|
+
const dataBlock = encodedMessage.subarray(hash.HASH_SIZE + 1)
|
|
197
|
+
const labelHash = hash.reset().process(new Uint8Array()).finish().result
|
|
198
|
+
if (!labelHash) {
|
|
199
|
+
throw new Error('密码加密失败')
|
|
200
|
+
}
|
|
201
|
+
dataBlock.set(labelHash, 0)
|
|
202
|
+
dataBlock.set(aesKey, hash.HASH_SIZE + paddingLength + 1)
|
|
203
|
+
dataBlock[hash.HASH_SIZE + paddingLength] = 1
|
|
204
|
+
const dataBlockMask = generateMgf1Mask(crypto, seed, dataBlock.length)
|
|
205
|
+
for (let index = 0; index < dataBlock.length; index += 1) {
|
|
206
|
+
dataBlock[index] ^= dataBlockMask[index]
|
|
207
|
+
}
|
|
208
|
+
const seedMask = generateMgf1Mask(crypto, dataBlock, maskedSeed.length)
|
|
209
|
+
maskedSeed.set(seed)
|
|
210
|
+
for (let index = 0; index < maskedSeed.length; index += 1) {
|
|
211
|
+
maskedSeed[index] ^= seedMask[index]
|
|
212
|
+
}
|
|
213
|
+
const encryptedKey = new crypto.RSA([modulus, exponent]).encrypt(
|
|
214
|
+
new crypto.BigNumber(encodedMessage),
|
|
215
|
+
).result
|
|
216
|
+
if (!encryptedKey) {
|
|
217
|
+
throw new Error('密码加密失败')
|
|
218
|
+
}
|
|
219
|
+
const ciphertext = crypto.AES_GCM.encrypt(plaintext, aesKey, iv)
|
|
220
|
+
return {
|
|
221
|
+
key_id: publicKeyResponse.key_id,
|
|
222
|
+
nonce: publicKeyResponse.nonce,
|
|
223
|
+
algorithm: publicKeyResponse.algorithm,
|
|
224
|
+
encrypted_key: miniBytesToBase64(encryptedKey),
|
|
225
|
+
iv: miniBytesToBase64(iv),
|
|
226
|
+
ciphertext: miniBytesToBase64(ciphertext),
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 加密单个密码字段,返回后端可解析的密码密文。 */
|
|
231
|
+
export async function encryptPassword(
|
|
232
|
+
password: string,
|
|
233
|
+
scene: PasswordCryptoScene,
|
|
234
|
+
): Promise<PasswordCrypto> {
|
|
235
|
+
const plainPassword = password.trim()
|
|
236
|
+
if (!plainPassword) {
|
|
237
|
+
throw new Error('密码不能为空')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const publicKeyResponse = await defLoginService.PasswordPublicKey({ scene })
|
|
241
|
+
|
|
242
|
+
// 微信小程序没有 window 和 WebCrypto,使用纯 JavaScript 实现保持与后端协议一致。
|
|
243
|
+
// #ifdef MP-WEIXIN
|
|
244
|
+
return encryptMiniProgramPassword(miniCrypto, plainPassword, publicKeyResponse)
|
|
245
|
+
// #endif
|
|
246
|
+
|
|
247
|
+
const cryptoApi = getSubtleCrypto()
|
|
248
|
+
const publicKey = await cryptoApi.subtle.importKey(
|
|
249
|
+
'spki',
|
|
250
|
+
pemToArrayBuffer(publicKeyResponse.public_key),
|
|
251
|
+
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
|
252
|
+
false,
|
|
253
|
+
['encrypt'],
|
|
254
|
+
)
|
|
255
|
+
const aesKey = await cryptoApi.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [
|
|
256
|
+
'encrypt',
|
|
257
|
+
])
|
|
258
|
+
const rawAesKey = await cryptoApi.subtle.exportKey('raw', aesKey)
|
|
259
|
+
const iv = cryptoApi.getRandomValues(new Uint8Array(12))
|
|
260
|
+
const ciphertext = await cryptoApi.subtle.encrypt(
|
|
261
|
+
{ name: 'AES-GCM', iv },
|
|
262
|
+
aesKey,
|
|
263
|
+
new TextEncoder().encode(plainPassword),
|
|
264
|
+
)
|
|
265
|
+
const encryptedKey = await cryptoApi.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, rawAesKey)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
key_id: publicKeyResponse.key_id,
|
|
269
|
+
nonce: publicKeyResponse.nonce,
|
|
270
|
+
algorithm: publicKeyResponse.algorithm,
|
|
271
|
+
encrypted_key: arrayBufferToBase64(encryptedKey),
|
|
272
|
+
iv: arrayBufferToBase64(iv.buffer),
|
|
273
|
+
ciphertext: arrayBufferToBase64(ciphertext),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onLoad } from '@dcloudio/uni-app'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { useSettingStore } from '../../../stores'
|
|
5
|
+
import defaultLogo from '../../../static/images/logo_icon.png'
|
|
6
|
+
|
|
7
|
+
const settingStore = useSettingStore()
|
|
8
|
+
const mainTitle = computed(() => settingStore.getData('mainTitle') || '应用框架示例')
|
|
9
|
+
const subTitle = computed(() => settingStore.getData('subTitle') || '保留通用导航与个人中心体验')
|
|
10
|
+
const appLogo = computed(() => settingStore.getData('appLogo') || defaultLogo)
|
|
11
|
+
|
|
12
|
+
onLoad(() => {
|
|
13
|
+
void settingStore.loadData().catch(() => undefined)
|
|
14
|
+
})
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<view class="page">
|
|
19
|
+
<view class="hero">
|
|
20
|
+
<image class="logo" :src="appLogo" mode="aspectFit" />
|
|
21
|
+
<view class="hero-copy">
|
|
22
|
+
<text class="title">{{ mainTitle }}</text>
|
|
23
|
+
<text class="subtitle">{{ subTitle }}</text>
|
|
24
|
+
</view>
|
|
25
|
+
</view>
|
|
26
|
+
|
|
27
|
+
<view class="section">
|
|
28
|
+
<text class="section-title">当前技术栈</text>
|
|
29
|
+
<view class="info-list">
|
|
30
|
+
<view class="info-row">
|
|
31
|
+
<text class="info-label">应用框架</text>
|
|
32
|
+
<text class="info-value">uni-app + Vue 3</text>
|
|
33
|
+
</view>
|
|
34
|
+
<view class="info-row">
|
|
35
|
+
<text class="info-label">开发语言</text>
|
|
36
|
+
<text class="info-value">TypeScript</text>
|
|
37
|
+
</view>
|
|
38
|
+
<view class="info-row">
|
|
39
|
+
<text class="info-label">状态管理</text>
|
|
40
|
+
<text class="info-value">Pinia</text>
|
|
41
|
+
</view>
|
|
42
|
+
<view class="info-row">
|
|
43
|
+
<text class="info-label">样式方案</text>
|
|
44
|
+
<text class="info-value">Sass + rpx</text>
|
|
45
|
+
</view>
|
|
46
|
+
</view>
|
|
47
|
+
</view>
|
|
48
|
+
|
|
49
|
+
<view class="section">
|
|
50
|
+
<text class="section-title">演示范围</text>
|
|
51
|
+
<view class="demo-list">
|
|
52
|
+
<view class="demo-item">
|
|
53
|
+
<view class="demo-dot">1</view>
|
|
54
|
+
<view class="demo-copy">
|
|
55
|
+
<text class="demo-title">跨端页面</text>
|
|
56
|
+
<text class="demo-desc">同一套页面代码适配 H5、微信小程序和 App</text>
|
|
57
|
+
</view>
|
|
58
|
+
</view>
|
|
59
|
+
<view class="demo-item">
|
|
60
|
+
<view class="demo-dot">2</view>
|
|
61
|
+
<view class="demo-copy">
|
|
62
|
+
<text class="demo-title">账户能力</text>
|
|
63
|
+
<text class="demo-desc">个人资料、应用设置与智能助手入口已保留</text>
|
|
64
|
+
</view>
|
|
65
|
+
</view>
|
|
66
|
+
<view class="demo-item">
|
|
67
|
+
<view class="demo-dot">3</view>
|
|
68
|
+
<view class="demo-copy">
|
|
69
|
+
<text class="demo-title">静态首页</text>
|
|
70
|
+
<text class="demo-desc">当前首页用于展示框架信息,不依赖业务接口</text>
|
|
71
|
+
</view>
|
|
72
|
+
</view>
|
|
73
|
+
</view>
|
|
74
|
+
</view>
|
|
75
|
+
</view>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<style lang="scss">
|
|
79
|
+
page {
|
|
80
|
+
background-color: #f7f7f8;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.page {
|
|
84
|
+
min-height: 100vh;
|
|
85
|
+
padding: 44rpx 28rpx 56rpx;
|
|
86
|
+
box-sizing: border-box;
|
|
87
|
+
|
|
88
|
+
/* #ifdef MP-WEIXIN */
|
|
89
|
+
padding-top: calc(44rpx + 44px);
|
|
90
|
+
/* #endif */
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.hero {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
padding: 34rpx 28rpx;
|
|
97
|
+
border-radius: 12rpx;
|
|
98
|
+
background-color: #27ba9b;
|
|
99
|
+
box-shadow: 0 8rpx 18rpx rgba(39, 186, 155, 0.18);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.logo {
|
|
103
|
+
flex: 0 0 auto;
|
|
104
|
+
width: 92rpx;
|
|
105
|
+
height: 92rpx;
|
|
106
|
+
border-radius: 20rpx;
|
|
107
|
+
background-color: #fff;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.hero-copy {
|
|
111
|
+
min-width: 0;
|
|
112
|
+
margin-left: 22rpx;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.eyebrow,
|
|
116
|
+
.title,
|
|
117
|
+
.subtitle {
|
|
118
|
+
display: block;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.eyebrow {
|
|
122
|
+
color: rgba(255, 255, 255, 0.8);
|
|
123
|
+
font-size: 20rpx;
|
|
124
|
+
letter-spacing: 2rpx;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.title {
|
|
128
|
+
margin-top: 8rpx;
|
|
129
|
+
color: #fff;
|
|
130
|
+
font-size: 40rpx;
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.subtitle {
|
|
135
|
+
margin-top: 10rpx;
|
|
136
|
+
color: rgba(255, 255, 255, 0.86);
|
|
137
|
+
font-size: 24rpx;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.section {
|
|
141
|
+
margin-top: 24rpx;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.section-title {
|
|
145
|
+
display: block;
|
|
146
|
+
margin: 0 8rpx 14rpx;
|
|
147
|
+
color: #26332f;
|
|
148
|
+
font-size: 28rpx;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.info-list,
|
|
153
|
+
.demo-list {
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
border-radius: 10rpx;
|
|
156
|
+
background-color: #fff;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.info-row {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
padding: 26rpx 24rpx;
|
|
164
|
+
border-bottom: 1rpx solid #edf1ef;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.info-row:last-child {
|
|
168
|
+
border-bottom: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.info-label {
|
|
172
|
+
color: #66736f;
|
|
173
|
+
font-size: 26rpx;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.info-value {
|
|
177
|
+
color: #26332f;
|
|
178
|
+
font-size: 26rpx;
|
|
179
|
+
font-weight: 500;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.demo-item {
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: flex-start;
|
|
185
|
+
padding: 26rpx 24rpx;
|
|
186
|
+
border-bottom: 1rpx solid #edf1ef;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.demo-item:last-child {
|
|
190
|
+
border-bottom: 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.demo-dot {
|
|
194
|
+
flex: 0 0 auto;
|
|
195
|
+
width: 42rpx;
|
|
196
|
+
height: 42rpx;
|
|
197
|
+
border-radius: 50%;
|
|
198
|
+
background-color: #e8f8f4;
|
|
199
|
+
color: #16806d;
|
|
200
|
+
font-size: 24rpx;
|
|
201
|
+
line-height: 42rpx;
|
|
202
|
+
text-align: center;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.demo-copy {
|
|
206
|
+
min-width: 0;
|
|
207
|
+
margin-left: 18rpx;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.demo-title,
|
|
211
|
+
.demo-desc {
|
|
212
|
+
display: block;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.demo-title {
|
|
216
|
+
color: #26332f;
|
|
217
|
+
font-size: 28rpx;
|
|
218
|
+
font-weight: 500;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.demo-desc {
|
|
222
|
+
margin-top: 8rpx;
|
|
223
|
+
color: #8a9691;
|
|
224
|
+
font-size: 24rpx;
|
|
225
|
+
line-height: 1.5;
|
|
226
|
+
}
|
|
227
|
+
</style>
|