af-mobile-client-vue3 1.4.10 → 1.4.11
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/.env +1 -0
- package/mock/modules/prose.mock.ts.timestamp-1761115794478.mjs +53 -0
- package/package.json +4 -2
- package/postcss.config.ts +8 -8
- package/src/components/data/XFormGroup/doc/DeviceForm.vue +1 -1
- package/src/components/data/XFormGroup/doc/UserForm.vue +1 -1
- package/src/components/data/XReportGrid/XReportDemo.vue +33 -33
- package/src/components/data/XReportGrid/print.js +184 -184
- package/src/router/routes.ts +1 -1
- package/src/stores/modules/homeApp/README.md +124 -0
- package/src/stores/modules/setting.ts +1 -1
- package/src/stores/modules/user.ts +4 -0
- package/src/types/crypto.ts +111 -0
- package/src/utils/EncryptUtil.ts +246 -0
- package/src/utils/http/index.ts +24 -0
- package/src/utils/timeUtil.ts +27 -27
- package/src/utils/wechat.ts +3 -29
- package/src/views/component/XCellListView/index.vue +8 -5
- package/src/views/component/XFormGroupView/index.vue +21 -8
- package/src/views/component/XFormView/index.vue +38 -18
- package/src/views/user/login/LoginForm.vue +2 -1
- package/vite.config.ts +0 -7
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# HomeApp Store 使用说明
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
`useHomeAppStore` 是一个专门用于管理首页应用列表的 Pinia store,与 `useSettingStore` 配合使用。
|
|
5
|
+
|
|
6
|
+
## 特性
|
|
7
|
+
- 独立管理 `homeAppList` 数据
|
|
8
|
+
- 提供完整的 CRUD 操作
|
|
9
|
+
- 与 setting store 自动同步数据
|
|
10
|
+
- 支持类型安全的 TypeScript 接口
|
|
11
|
+
|
|
12
|
+
## 基本使用
|
|
13
|
+
|
|
14
|
+
### 1. 导入和使用
|
|
15
|
+
```typescript
|
|
16
|
+
import { useHomeAppStore } from '@/stores'
|
|
17
|
+
|
|
18
|
+
const homeAppStore = useHomeAppStore()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. 获取应用列表
|
|
22
|
+
```typescript
|
|
23
|
+
// 获取所有应用
|
|
24
|
+
const apps = homeAppStore.getHomeAppList()
|
|
25
|
+
|
|
26
|
+
// 直接访问响应式数据
|
|
27
|
+
const apps = homeAppStore.homeAppList
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 3. 添加应用
|
|
31
|
+
```typescript
|
|
32
|
+
homeAppStore.addHomeApp({
|
|
33
|
+
id: 'new-app',
|
|
34
|
+
name: '新应用',
|
|
35
|
+
icon: '/icon.png',
|
|
36
|
+
url: 'https://example.com',
|
|
37
|
+
description: '应用描述'
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 4. 更新应用
|
|
42
|
+
```typescript
|
|
43
|
+
homeAppStore.updateHomeApp('app-id', {
|
|
44
|
+
name: '更新后的名称',
|
|
45
|
+
description: '新的描述'
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 5. 删除应用
|
|
50
|
+
```typescript
|
|
51
|
+
homeAppStore.removeHomeApp('app-id')
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 6. 清空列表
|
|
55
|
+
```typescript
|
|
56
|
+
homeAppStore.clearHomeAppList()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 数据同步
|
|
60
|
+
|
|
61
|
+
### 自动同步
|
|
62
|
+
- 当 `useSettingStore.init()` 被调用时,`homeAppList` 会自动同步到 `useHomeAppStore`
|
|
63
|
+
- 无需手动同步,数据会自动保持一致
|
|
64
|
+
|
|
65
|
+
### 手动同步
|
|
66
|
+
```typescript
|
|
67
|
+
import { useSettingStore, useHomeAppStore } from '@/stores'
|
|
68
|
+
|
|
69
|
+
const settingStore = useSettingStore()
|
|
70
|
+
const homeAppStore = useHomeAppStore()
|
|
71
|
+
|
|
72
|
+
// 手动同步数据
|
|
73
|
+
homeAppStore.setHomeAppList(settingStore.getSetting()?.homeAppList || [])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 在组件中使用
|
|
77
|
+
|
|
78
|
+
### Vue 组件示例
|
|
79
|
+
```vue
|
|
80
|
+
<template>
|
|
81
|
+
<div class="home-apps">
|
|
82
|
+
<div
|
|
83
|
+
v-for="app in homeAppStore.homeAppList"
|
|
84
|
+
:key="app.id"
|
|
85
|
+
class="app-item"
|
|
86
|
+
@click="openApp(app)"
|
|
87
|
+
>
|
|
88
|
+
<img :src="app.icon" :alt="app.name" />
|
|
89
|
+
<span>{{ app.name }}</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</template>
|
|
93
|
+
|
|
94
|
+
<script setup lang="ts">
|
|
95
|
+
import { useHomeAppStore } from '@/stores'
|
|
96
|
+
|
|
97
|
+
const homeAppStore = useHomeAppStore()
|
|
98
|
+
|
|
99
|
+
function openApp(app) {
|
|
100
|
+
// 处理应用点击
|
|
101
|
+
window.open(app.url, '_blank')
|
|
102
|
+
}
|
|
103
|
+
</script>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## 注意事项
|
|
107
|
+
|
|
108
|
+
1. **初始化顺序**:确保先调用 `useSettingStore.init()`,再使用 `useHomeAppStore`
|
|
109
|
+
2. **数据一致性**:两个 store 中的数据会自动保持同步
|
|
110
|
+
3. **类型安全**:使用 TypeScript 接口确保类型安全
|
|
111
|
+
4. **响应式**:所有数据都是响应式的,可以直接在模板中使用
|
|
112
|
+
|
|
113
|
+
## 接口定义
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
export interface HomeApp {
|
|
117
|
+
id: string // 应用唯一标识
|
|
118
|
+
name: string // 应用名称
|
|
119
|
+
icon: string // 应用图标
|
|
120
|
+
url: string // 应用链接
|
|
121
|
+
description?: string // 应用描述(可选)
|
|
122
|
+
[key: string]: any // 其他属性
|
|
123
|
+
}
|
|
124
|
+
```
|
|
@@ -21,6 +21,7 @@ export interface WebConfig {
|
|
|
21
21
|
homeAppList: Array<any>
|
|
22
22
|
slideshowList: Array<any>
|
|
23
23
|
registerRequire: boolean
|
|
24
|
+
requestEncrypt: boolean
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// 存放 webConfig 中的 setting 配置
|
|
@@ -56,7 +57,6 @@ export const useSettingStore = defineStore('setting', () => {
|
|
|
56
57
|
const res = await getConfigByNameAsync('webMobileConfig')
|
|
57
58
|
if (res.setting) {
|
|
58
59
|
useStore.set(APP_WEB_CONFIG_KEY, res)
|
|
59
|
-
console.log('res.setting', res.setting)
|
|
60
60
|
setSetting(res.setting)
|
|
61
61
|
// homeAppList 仅在用户缓存没有时才回填
|
|
62
62
|
if (!homeAppStore.homeAppList?.length && res.setting.homeAppList) {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '@af-mobile-client-vue3/stores/mutation-type'
|
|
18
18
|
import { getPlatformRoutePrefix } from '@af-mobile-client-vue3/types/platform'
|
|
19
19
|
import crypto from '@af-mobile-client-vue3/utils/crypto'
|
|
20
|
+
import { encryptUtil } from '@af-mobile-client-vue3/utils/EncryptUtil'
|
|
20
21
|
import { indexedDB } from '@af-mobile-client-vue3/utils/indexedDB'
|
|
21
22
|
import { createStorage } from '@af-mobile-client-vue3/utils/Storage'
|
|
22
23
|
import { defineStore } from 'pinia'
|
|
@@ -212,6 +213,9 @@ export const useUserStore = defineStore('app-user', () => {
|
|
|
212
213
|
setToken(data.access_token)
|
|
213
214
|
// 第三方教培系统鉴权兼容
|
|
214
215
|
const LoginTicket = crypto.AESEncrypt(JSON.stringify(params), '3KMKqvgwR8ULbR8Z')
|
|
216
|
+
if (data.session && useSettingStore().getSetting().requestEncrypt) {
|
|
217
|
+
localStorage.setItem('v4-session-key', encryptUtil.RSADecrypt(data.session as string))
|
|
218
|
+
}
|
|
215
219
|
Storage.set('LoginTicket', LoginTicket)
|
|
216
220
|
}
|
|
217
221
|
return Promise.resolve(data)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 加密相关类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 加密模式枚举
|
|
7
|
+
*/
|
|
8
|
+
export enum CryptoMode {
|
|
9
|
+
/** ECB模式 */
|
|
10
|
+
ECB = 'ECB',
|
|
11
|
+
/** CBC模式 */
|
|
12
|
+
CBC = 'CBC',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 填充方式枚举
|
|
17
|
+
*/
|
|
18
|
+
export enum PaddingMode {
|
|
19
|
+
/** PKCS7填充 */
|
|
20
|
+
PKCS7 = 'Pkcs7',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* AES加密配置接口
|
|
25
|
+
*/
|
|
26
|
+
export interface AESConfig {
|
|
27
|
+
/** 加密模式 */
|
|
28
|
+
mode: CryptoMode
|
|
29
|
+
/** 填充方式 */
|
|
30
|
+
padding: PaddingMode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* AES CBC模式加密结果接口
|
|
35
|
+
*/
|
|
36
|
+
export interface AESCBCResult {
|
|
37
|
+
/** IV的Base64编码 */
|
|
38
|
+
iv: string
|
|
39
|
+
/** 密文的Base64编码 */
|
|
40
|
+
ciphertext: string
|
|
41
|
+
/** 组合格式:IV:密文 */
|
|
42
|
+
combined: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 加密工具类接口
|
|
47
|
+
*/
|
|
48
|
+
export interface IEncryptUtil {
|
|
49
|
+
/**
|
|
50
|
+
* AES ECB模式加密
|
|
51
|
+
* @param word 待加密的字符串
|
|
52
|
+
* @param encryKey 加密密钥
|
|
53
|
+
* @returns 加密后的字符串
|
|
54
|
+
*/
|
|
55
|
+
AESEncrypt(word: string, encryKey: string): string
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* AES ECB模式解密
|
|
59
|
+
* @param word 待解密的字符串
|
|
60
|
+
* @param encryKey 解密密钥
|
|
61
|
+
* @returns 解密后的数据(可能是字符串或对象)
|
|
62
|
+
*/
|
|
63
|
+
AESDecrypt(word: string, encryKey: string): string | object
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* RSA加密
|
|
67
|
+
* @param word 待加密的字符串
|
|
68
|
+
* @returns 加密后的字符串
|
|
69
|
+
*/
|
|
70
|
+
RSAEncrypt(word: string): string
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* RSA解密
|
|
74
|
+
* @param word 待解密的字符串
|
|
75
|
+
* @returns 解密后的对象
|
|
76
|
+
*/
|
|
77
|
+
RSADecrypt(word: string): object
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* AES CBC模式加密
|
|
81
|
+
* @param data 待加密的数据
|
|
82
|
+
* @param cryptoKey 十六进制格式的加密密钥
|
|
83
|
+
* @returns 组合格式的加密结果
|
|
84
|
+
*/
|
|
85
|
+
AESEncryptCBC(data: any, cryptoKey: string): string
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* AES CBC模式解密
|
|
89
|
+
* @param combinedText 格式:Base64(iv):Base64(ciphertext)
|
|
90
|
+
* @param hexKey 十六进制格式的加密密钥
|
|
91
|
+
* @returns 解密后的原始字符串
|
|
92
|
+
*/
|
|
93
|
+
AESDecryptCBC(combinedText: string, hexKey: string): string
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 安全的AES-CBC解密(失败时返回null)
|
|
97
|
+
* @param combinedText 格式:Base64(iv):Base64(ciphertext)
|
|
98
|
+
* @param hexKey 十六进制格式的密钥
|
|
99
|
+
* @returns 解密后的字符串,失败时返回null
|
|
100
|
+
*/
|
|
101
|
+
safeAESDecryptCBC(combinedText: string, hexKey: string): string | null
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 智能响应解密
|
|
105
|
+
* @param responseData 响应数据
|
|
106
|
+
* @param sessionKey 会话密钥
|
|
107
|
+
* @returns 解密后的数据,失败时返回原数据
|
|
108
|
+
*/
|
|
109
|
+
decryptResponse(responseData: any, sessionKey: string): any
|
|
110
|
+
}
|
|
111
|
+
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import CryptoJS from 'crypto-js'
|
|
2
|
+
import JSEncrypt from 'jsencrypt'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 加密工具类
|
|
6
|
+
* 提供AES和RSA加密解密功能
|
|
7
|
+
*/
|
|
8
|
+
class EncryptUtil {
|
|
9
|
+
/**
|
|
10
|
+
* RSA公钥
|
|
11
|
+
*/
|
|
12
|
+
private readonly RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqPvovSfXcwBbW8cKMCgwqNpsYuzF8RPAPFb7LGsnVo44JhM/xxzDyzoYtdfNmtbIuKVi9PzIsyp6rg+09gbuI6UGwBZ5DWBDBMqv5MPdOF5dCQkB2Bbr5yPfURPENypUz+pBFBg41d+BC+rwRiXELwKy7Y9caD/MtJyHydj8OUwIDAQAB'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* RSA私钥
|
|
16
|
+
*/
|
|
17
|
+
private readonly RSA_PRIVATE_KEY = 'MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKo++i9J9dzAFtbxwowKDCo2mxi7MXxE8A8VvssaydWjjgmEz/HHMPLOhi1182a1si4pWL0/MizKnquD7T2Bu4jpQbAFnkNYEMEyq/kw904Xl0JCQHYFuvnI99RE8Q3KlTP6kEUGDjV34EL6vBGJcQvArLtj1xoP8y0nIfJ2Pw5TAgMBAAECgYAGGB8IllMwxceLhjf6n1l0IWRH7FuHIUieoZ6k0p6rASHSgWiYNRMxfecbtX8zDAoG0QAWNi7rn40ygpR5gS1fWDAKhmnhKgQIT6wW0VmD4hraaeyP78iy8BLhlvblri2nCPIhDH5+l96v7D47ZZi3ZSOzcj89s1eS/k7/N4peEQJBAPEtGGJY+lBoCxQMhGyzuzDmgcS1Un1ZE2pt+XNCVl2b+T8fxWJH3tRRR8wOY5uvtPiK1HM/IjT0T5qwQeH8Yk0CQQC0tcv3d/bDb7bOe9QzUFDQkUSpTdPWAgMX2OVPxjdq3Sls9oA5+fGNYEy0OgyqTjde0b4iRzlD1O0OhLqPSUMfAkEAh5FIvqezdRU2/PsYSR4yoAdCdLdT+h/jGRVefhqQ/6eYUJJkWp15tTFHQX3pIe9/s6IeT/XyHYAjaxmevxAmlQJBAKSdhvQjf9KAjZKDEsa7vyJ/coCXuQUWSCMNHbcR5aGfXgE4e45UtUoIE1eKGcd6AM6LWhx3rR6xdFDpb9je8BkCQB0SpevGfOQkMk5i8xkEt9eeYP0fi8nv6eOUcK96EXbzs4jV2SAoQJ9oJegPtPROHbhIvVUmNQTbuP10Yjg59+8='
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* AES ECB模式加密
|
|
21
|
+
* @param word 待加密的字符串
|
|
22
|
+
* @param encryKey 加密密钥
|
|
23
|
+
* @returns 加密后的字符串
|
|
24
|
+
*/
|
|
25
|
+
AESEncrypt(word: string, encryKey: string): string {
|
|
26
|
+
try {
|
|
27
|
+
const key = CryptoJS.enc.Utf8.parse(encryKey)
|
|
28
|
+
const srcs = CryptoJS.enc.Utf8.parse(word)
|
|
29
|
+
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
|
|
30
|
+
mode: CryptoJS.mode.ECB,
|
|
31
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
32
|
+
})
|
|
33
|
+
return encrypted.toString()
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`AES ECB加密失败: ${error}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* AES ECB模式解密
|
|
42
|
+
* @param word 待解密的字符串
|
|
43
|
+
* @param encryKey 解密密钥
|
|
44
|
+
* @returns 解密后的数据(可能是字符串或对象)
|
|
45
|
+
*/
|
|
46
|
+
AESDecrypt(word: string, encryKey: string): string | object {
|
|
47
|
+
try {
|
|
48
|
+
const key = CryptoJS.enc.Utf8.parse(encryKey)
|
|
49
|
+
const decrypt = CryptoJS.AES.decrypt(word, key, {
|
|
50
|
+
mode: CryptoJS.mode.ECB,
|
|
51
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
52
|
+
})
|
|
53
|
+
const ret = CryptoJS.enc.Utf8.stringify(decrypt).toString()
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(ret)
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return ret
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(`AES ECB解密失败: ${error}`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* RSA公钥加密
|
|
69
|
+
*
|
|
70
|
+
* @param data 需要加密的数据
|
|
71
|
+
* @param publicKey PEM格式的公钥
|
|
72
|
+
* @returns Base64编码的加密结果
|
|
73
|
+
*/
|
|
74
|
+
RSAEncrypt(data: string, publicKey: string): string {
|
|
75
|
+
try {
|
|
76
|
+
const encryptor = new JSEncrypt()
|
|
77
|
+
encryptor.setPublicKey(publicKey)
|
|
78
|
+
const encrypted = encryptor.encrypt(data)
|
|
79
|
+
|
|
80
|
+
if (!encrypted) {
|
|
81
|
+
throw new Error('加密失败:空结果')
|
|
82
|
+
}
|
|
83
|
+
return encrypted
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error('RSA加密错误:', error)
|
|
87
|
+
throw new Error('RSA加密失败,请检查公钥格式')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* RSA私钥解密
|
|
93
|
+
*
|
|
94
|
+
* @param encryptedData Base64编码的加密数据
|
|
95
|
+
* @returns 解密后的原始字符串
|
|
96
|
+
*/
|
|
97
|
+
RSADecrypt(encryptedData: string): string {
|
|
98
|
+
try {
|
|
99
|
+
const decryptor = new JSEncrypt()
|
|
100
|
+
decryptor.setPrivateKey(import.meta.env.VITE_RSA_PRIVATE_KEY)
|
|
101
|
+
const decrypted = decryptor.decrypt(encryptedData)
|
|
102
|
+
|
|
103
|
+
if (!decrypted) {
|
|
104
|
+
throw new Error('解密失败:空结果')
|
|
105
|
+
}
|
|
106
|
+
return decrypted
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error('RSA解密错误:', error)
|
|
110
|
+
throw new Error('RSA解密失败,请检查私钥格式')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* AES CBC模式加密
|
|
116
|
+
* @param data 待加密的数据
|
|
117
|
+
* @param cryptoKey 十六进制格式的加密密钥
|
|
118
|
+
* @returns 组合格式的加密结果
|
|
119
|
+
*/
|
|
120
|
+
AESEncryptCBC(data: any, cryptoKey: string): string {
|
|
121
|
+
try {
|
|
122
|
+
// 生成随机IV(每次加密不同)
|
|
123
|
+
const iv = CryptoJS.lib.WordArray.random(16)
|
|
124
|
+
|
|
125
|
+
// 使用 AES CBC 模式进行加密
|
|
126
|
+
const encrypted = CryptoJS.AES.encrypt(
|
|
127
|
+
JSON.stringify(data),
|
|
128
|
+
CryptoJS.enc.Hex.parse(cryptoKey),
|
|
129
|
+
{
|
|
130
|
+
iv, // IV
|
|
131
|
+
mode: CryptoJS.mode.CBC,
|
|
132
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// 组合IV和密文,分别Base64编码:IV(base64) + : + 密文(base64)
|
|
137
|
+
const ivBase64 = iv.toString(CryptoJS.enc.Base64)
|
|
138
|
+
const encryptedBase64 = encrypted.toString()
|
|
139
|
+
|
|
140
|
+
return `${ivBase64}:${encryptedBase64}`
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw new Error(`AES CBC加密失败: ${error}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* AES CBC模式解密
|
|
149
|
+
* @param combinedText 格式:Base64(iv):Base64(ciphertext)
|
|
150
|
+
* @param hexKey 十六进制格式的加密密钥
|
|
151
|
+
* @returns 解密后的原始字符串
|
|
152
|
+
*/
|
|
153
|
+
AESDecryptCBC(combinedText: string, hexKey: string): string {
|
|
154
|
+
try {
|
|
155
|
+
// 分离 IV 和 密文
|
|
156
|
+
const parts = combinedText.split(':')
|
|
157
|
+
if (parts.length !== 2) {
|
|
158
|
+
throw new Error('Invalid encrypted data format')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const ivBase64 = parts[0]
|
|
162
|
+
const encryptedBase64 = parts[1]
|
|
163
|
+
|
|
164
|
+
// 解析IV和密文
|
|
165
|
+
const iv = CryptoJS.enc.Base64.parse(ivBase64)
|
|
166
|
+
const key = CryptoJS.enc.Hex.parse(hexKey)
|
|
167
|
+
|
|
168
|
+
// 解密
|
|
169
|
+
const decrypted = CryptoJS.AES.decrypt(encryptedBase64, key, {
|
|
170
|
+
iv,
|
|
171
|
+
mode: CryptoJS.mode.CBC,
|
|
172
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// 将解密后的数据转换为字符串
|
|
176
|
+
return decrypted.toString(CryptoJS.enc.Utf8)
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
throw new Error(`AES-CBC解密失败: ${error}`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 安全的AES-CBC解密(失败时返回null)
|
|
185
|
+
* 用于响应自动解密,解密失败时不抛出异常
|
|
186
|
+
* @param combinedText 格式:Base64(iv):Base64(ciphertext)
|
|
187
|
+
* @param hexKey 十六进制格式的密钥
|
|
188
|
+
* @returns 解密后的字符串,失败时返回null
|
|
189
|
+
*/
|
|
190
|
+
safeAESDecryptCBC(combinedText: string, hexKey: string): string | null {
|
|
191
|
+
try {
|
|
192
|
+
return this.AESDecryptCBC(combinedText, hexKey)
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// AES-CBC解密失败,静默处理
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 智能响应解密
|
|
202
|
+
* 自动检测响应数据格式并进行解密
|
|
203
|
+
* @param responseData 响应数据
|
|
204
|
+
* @param sessionKey 会话密钥
|
|
205
|
+
* @returns 解密后的数据,失败时返回原数据
|
|
206
|
+
*/
|
|
207
|
+
decryptResponse(responseData: any, sessionKey: string): any {
|
|
208
|
+
try {
|
|
209
|
+
let dataStr = ''
|
|
210
|
+
|
|
211
|
+
// 处理不同类型的响应数据
|
|
212
|
+
if (typeof responseData === 'string') {
|
|
213
|
+
dataStr = responseData
|
|
214
|
+
}
|
|
215
|
+
else if (typeof responseData === 'object') {
|
|
216
|
+
dataStr = JSON.stringify(responseData)
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
dataStr = String(responseData)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 尝试解密
|
|
223
|
+
const decryptedStr = this.safeAESDecryptCBC(dataStr, sessionKey)
|
|
224
|
+
if (decryptedStr) {
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(decryptedStr)
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return decryptedStr
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.warn('响应解密处理失败:', error)
|
|
235
|
+
// 响应解密处理失败,静默处理
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 解密失败或数据格式不正确时,返回原数据
|
|
239
|
+
return responseData
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 创建单例实例
|
|
244
|
+
const encryptUtil = new EncryptUtil()
|
|
245
|
+
|
|
246
|
+
export { EncryptUtil, encryptUtil }
|
package/src/utils/http/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } fro
|
|
|
2
2
|
import { ContentTypeEnum, ResultEnum } from '@af-mobile-client-vue3/enums/requestEnum'
|
|
3
3
|
import { useUserStore } from '@af-mobile-client-vue3/stores/modules/user'
|
|
4
4
|
import { ACCESS_TOKEN } from '@af-mobile-client-vue3/stores/mutation-type'
|
|
5
|
+
import { encryptUtil } from '@af-mobile-client-vue3/utils/EncryptUtil'
|
|
5
6
|
import axios from 'axios'
|
|
6
7
|
import { showToast } from 'vant'
|
|
7
8
|
|
|
@@ -32,6 +33,17 @@ class Http {
|
|
|
32
33
|
// 如果 token 存在
|
|
33
34
|
if (savedToken)
|
|
34
35
|
config.headers[ACCESS_TOKEN] = savedToken
|
|
36
|
+
const v4SessionKey = localStorage.getItem('v4-session-key')
|
|
37
|
+
if (['post'].includes(config.method.toLowerCase()) && v4SessionKey) {
|
|
38
|
+
if (config.data && !(config.data instanceof FormData)) {
|
|
39
|
+
config.data = {
|
|
40
|
+
encrypted: encryptUtil.AESEncryptCBC(config.data, v4SessionKey),
|
|
41
|
+
}
|
|
42
|
+
config.headers['X-Sec'] = '1'
|
|
43
|
+
config.headers['X-Rand'] = Math.random().toString(36).substr(2, 5)
|
|
44
|
+
config.headers['X-Ts'] = Date.now()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
35
47
|
return config
|
|
36
48
|
},
|
|
37
49
|
(error: AxiosError) => {
|
|
@@ -48,6 +60,18 @@ class Http {
|
|
|
48
60
|
private httpInterceptorsResponse(): void {
|
|
49
61
|
Http.axiosInstance.interceptors.response.use(
|
|
50
62
|
async (response: AxiosResponse) => {
|
|
63
|
+
// 判断是否需要解密
|
|
64
|
+
if (response.headers && response.headers['x-encrypted'] === '1') {
|
|
65
|
+
const v4SessionKey = localStorage.getItem('v4-session-key')
|
|
66
|
+
if (v4SessionKey && response.data) {
|
|
67
|
+
const decryptedData = encryptUtil.decryptResponse(response?.data, v4SessionKey)
|
|
68
|
+
// 如果解密成功且不等于原数据,说明解密有效
|
|
69
|
+
if (decryptedData !== response.data) {
|
|
70
|
+
// 响应解密成功
|
|
71
|
+
response.data = decryptedData
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
51
75
|
const compatible = import.meta.env.VITE_APP_COMPATIBLE
|
|
52
76
|
if (compatible !== 'V4') {
|
|
53
77
|
return response.data
|
package/src/utils/timeUtil.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
// 时间工具:提供简单的日期时间格式化
|
|
2
|
-
|
|
3
|
-
export function pad2(num: number): string {
|
|
4
|
-
return num < 10 ? `0${num}` : `${num}`
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 按照简单占位符格式化当前时间
|
|
9
|
-
* 支持占位:YYYY MM DD HH mm ss
|
|
10
|
-
* @param format 默认 'YYYY-MM-DD HH:mm:ss'
|
|
11
|
-
*/
|
|
12
|
-
export function formatNow(format: string = 'YYYY-MM-DD HH:mm:ss'): string {
|
|
13
|
-
const d = new Date()
|
|
14
|
-
const map: Record<string, string> = {
|
|
15
|
-
YYYY: `${d.getFullYear()}`,
|
|
16
|
-
MM: pad2(d.getMonth() + 1),
|
|
17
|
-
DD: pad2(d.getDate()),
|
|
18
|
-
HH: pad2(d.getHours()),
|
|
19
|
-
mm: pad2(d.getMinutes()),
|
|
20
|
-
ss: pad2(d.getSeconds()),
|
|
21
|
-
}
|
|
22
|
-
let out = format
|
|
23
|
-
Object.keys(map).forEach((k) => {
|
|
24
|
-
out = out.replace(new RegExp(k, 'g'), map[k])
|
|
25
|
-
})
|
|
26
|
-
return out
|
|
27
|
-
}
|
|
1
|
+
// 时间工具:提供简单的日期时间格式化
|
|
2
|
+
|
|
3
|
+
export function pad2(num: number): string {
|
|
4
|
+
return num < 10 ? `0${num}` : `${num}`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 按照简单占位符格式化当前时间
|
|
9
|
+
* 支持占位:YYYY MM DD HH mm ss
|
|
10
|
+
* @param format 默认 'YYYY-MM-DD HH:mm:ss'
|
|
11
|
+
*/
|
|
12
|
+
export function formatNow(format: string = 'YYYY-MM-DD HH:mm:ss'): string {
|
|
13
|
+
const d = new Date()
|
|
14
|
+
const map: Record<string, string> = {
|
|
15
|
+
YYYY: `${d.getFullYear()}`,
|
|
16
|
+
MM: pad2(d.getMonth() + 1),
|
|
17
|
+
DD: pad2(d.getDate()),
|
|
18
|
+
HH: pad2(d.getHours()),
|
|
19
|
+
mm: pad2(d.getMinutes()),
|
|
20
|
+
ss: pad2(d.getSeconds()),
|
|
21
|
+
}
|
|
22
|
+
let out = format
|
|
23
|
+
Object.keys(map).forEach((k) => {
|
|
24
|
+
out = out.replace(new RegExp(k, 'g'), map[k])
|
|
25
|
+
})
|
|
26
|
+
return out
|
|
27
|
+
}
|
package/src/utils/wechat.ts
CHANGED
|
@@ -5,10 +5,10 @@ import wx from 'weixin-js-sdk'
|
|
|
5
5
|
export interface WechatConfig {
|
|
6
6
|
debug: boolean
|
|
7
7
|
appId: string
|
|
8
|
-
timestamp:
|
|
8
|
+
timestamp: number
|
|
9
9
|
nonceStr: string
|
|
10
10
|
signature: string
|
|
11
|
-
jsApiList:
|
|
11
|
+
jsApiList: any[]
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// 微信扫码配置
|
|
@@ -63,36 +63,10 @@ export function configWechatSDK(config: WechatConfig): Promise<void> {
|
|
|
63
63
|
})
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
// 微信扫码
|
|
67
|
-
export function scanQRCode(config: WechatScanConfig): Promise<WechatScanResult> {
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
if (!isWechatBrowser()) {
|
|
70
|
-
reject(new Error('非微信环境,无法使用微信扫码'))
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
wx.scanQRCode({
|
|
75
|
-
needResult: config.needResult,
|
|
76
|
-
scanType: config.scanType,
|
|
77
|
-
success: (res: WechatScanResult) => {
|
|
78
|
-
console.log('微信扫码成功:', res)
|
|
79
|
-
resolve(res)
|
|
80
|
-
},
|
|
81
|
-
fail: (res: any) => {
|
|
82
|
-
console.error('微信扫码失败:', res)
|
|
83
|
-
reject(new Error(`微信扫码失败: ${res.errMsg}`))
|
|
84
|
-
},
|
|
85
|
-
cancel: () => {
|
|
86
|
-
reject(new Error('用户取消扫码'))
|
|
87
|
-
},
|
|
88
|
-
})
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
66
|
// 获取微信签名(模拟)
|
|
93
67
|
export async function getWechatSignature(url: string): Promise<{
|
|
94
68
|
appId: string
|
|
95
|
-
timestamp:
|
|
69
|
+
timestamp: number
|
|
96
70
|
nonceStr: string
|
|
97
71
|
signature: string
|
|
98
72
|
}> {
|
|
@@ -9,11 +9,11 @@ const emit = defineEmits(['deleteRow'])
|
|
|
9
9
|
// 访问路由
|
|
10
10
|
const router = useRouter()
|
|
11
11
|
// 获取默认值
|
|
12
|
-
const idKey = ref('o_id')
|
|
12
|
+
// const idKey = ref('o_id')
|
|
13
13
|
|
|
14
14
|
// 简易crud表单测试
|
|
15
|
-
const configName = ref('
|
|
16
|
-
const serviceName = ref('af-
|
|
15
|
+
const configName = ref('ceshiCRUD')
|
|
16
|
+
const serviceName = ref('af-safecheck')
|
|
17
17
|
|
|
18
18
|
// 资源权限测试
|
|
19
19
|
// const configName = ref('crud_sources_test')
|
|
@@ -83,14 +83,17 @@ const serviceName = ref('af-revenue')
|
|
|
83
83
|
// })
|
|
84
84
|
// }
|
|
85
85
|
|
|
86
|
-
//
|
|
86
|
+
// 删除功能
|
|
87
87
|
// function deleteRow(result) {
|
|
88
88
|
// emit('deleteRow', result.o_id)
|
|
89
89
|
// }
|
|
90
|
+
// const fixQueryForm = ref({
|
|
91
|
+
// f_operator_id: '487184754014158848',
|
|
92
|
+
// })
|
|
90
93
|
</script>
|
|
91
94
|
|
|
92
95
|
<template>
|
|
93
|
-
<NormalDataLayout id="XCellListView" title="
|
|
96
|
+
<NormalDataLayout id="XCellListView" title="工作计划">
|
|
94
97
|
<template #layout_content>
|
|
95
98
|
<XCellList
|
|
96
99
|
:config-name="configName"
|