commit
247e14703d
|
|
@ -0,0 +1,12 @@
|
||||||
|
/node_modules
|
||||||
|
/oh_modules
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
**/build
|
||||||
|
/.hvigor
|
||||||
|
.cxx
|
||||||
|
/.clangd
|
||||||
|
/.clang-format
|
||||||
|
/.clang-tidy
|
||||||
|
**/.test
|
||||||
|
/.appanalyzer
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"bundleName": "com.img.rabbit",
|
||||||
|
"vendor": "zuomeng",
|
||||||
|
"versionCode": 1,
|
||||||
|
"versionName": "1.0.0",
|
||||||
|
"icon": "$media:layered_image",
|
||||||
|
"label": "$string:app_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"name": "app_name",
|
||||||
|
"value": "截图兔"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"layered-image":
|
||||||
|
{
|
||||||
|
"background" : "$media:background",
|
||||||
|
"foreground" : "$media:foreground"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# ProxyAI Instructions
|
||||||
|
|
||||||
|
Describe goals, conventions, risky areas, and review rules here.
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
{
|
||||||
|
"app": {
|
||||||
|
"signingConfigs": [
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"type": "HarmonyOS",
|
||||||
|
"material": {
|
||||||
|
"keyAlias": "__uni__1be0b2f",
|
||||||
|
"signAlg": "SHA256withECDSA",
|
||||||
|
"storeFile": "D:/Developer/signature_config/harmony/hmos.p12",
|
||||||
|
"keyPassword": "000000188D653F4B8748579F0084CAC913B6D6AB6FF60084FB0F318CC52AC425A874B607DD8261F9",
|
||||||
|
"storePassword": "00000018779445CEA4B7EA75CB8F722F9DA76CBDBC5CE7C083CE8B5E1973141BF17E654C7237F5EF",
|
||||||
|
"certpath": "D:/Developer/signature_config/harmony/cer/release.cer",
|
||||||
|
"profile": "D:/Developer/signature_config/harmony/p7b/rabbit_releaseRelease.p7b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debug",
|
||||||
|
"type": "HarmonyOS",
|
||||||
|
"material": {
|
||||||
|
"keyAlias": "__uni__1be0b2f",
|
||||||
|
"signAlg": "SHA256withECDSA",
|
||||||
|
"storeFile": "D:/Developer/signature_config/harmony/hmos.p12",
|
||||||
|
"keyPassword": "000000188D653F4B8748579F0084CAC913B6D6AB6FF60084FB0F318CC52AC425A874B607DD8261F9",
|
||||||
|
"storePassword": "00000018779445CEA4B7EA75CB8F722F9DA76CBDBC5CE7C083CE8B5E1973141BF17E654C7237F5EF",
|
||||||
|
"certpath": "D:/Developer/signature_config/harmony/cer/debug.cer",
|
||||||
|
"profile": "D:/Developer/signature_config/harmony/p7b/rabbit_debugDebug.p7b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"signingConfig": "release",
|
||||||
|
"targetSdkVersion": "6.0.2(22)",
|
||||||
|
"compatibleSdkVersion": "5.0.3(15)",
|
||||||
|
"compileSdkVersion": "6.0.2(22)",
|
||||||
|
"runtimeOS": "HarmonyOS",
|
||||||
|
"buildOption": {
|
||||||
|
"strictMode": {
|
||||||
|
"caseSensitiveCheck": true,
|
||||||
|
"useNormalizedOHMUrl": true
|
||||||
|
},
|
||||||
|
"arkOptions": {
|
||||||
|
"buildProfileFields": {
|
||||||
|
"CHANNEL": "harmony",
|
||||||
|
"BUILD_TIME": "2605070954"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"artifactName": "rabbit_harmony_release_2605070954"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"buildModeSet": [
|
||||||
|
{
|
||||||
|
"name": "debug",
|
||||||
|
"buildOption": {
|
||||||
|
"debuggable": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"buildOption": {
|
||||||
|
"debuggable": false,
|
||||||
|
"arkOptions": {
|
||||||
|
"buildProfileFields": {
|
||||||
|
"CHANNEL": "harmony"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"srcPath": "./products/app",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "common",
|
||||||
|
"srcPath": "./common",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mainLayout",
|
||||||
|
"srcPath": "./features/mainLayout",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"applyToProducts": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.ets"
|
||||||
|
],
|
||||||
|
"ignore": [
|
||||||
|
"**/src/ohosTest/**/*",
|
||||||
|
"**/src/test/**/*",
|
||||||
|
"**/src/mock/**/*",
|
||||||
|
"**/node_modules/**/*",
|
||||||
|
"**/oh_modules/**/*",
|
||||||
|
"**/build/**/*",
|
||||||
|
"**/.preview/**/*"
|
||||||
|
],
|
||||||
|
"ruleSet": [
|
||||||
|
"plugin:@performance/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@security/no-unsafe-aes": "error",
|
||||||
|
"@security/no-unsafe-hash": "error",
|
||||||
|
"@security/no-unsafe-mac": "warn",
|
||||||
|
"@security/no-unsafe-dh": "error",
|
||||||
|
"@security/no-unsafe-dsa": "error",
|
||||||
|
"@security/no-unsafe-ecdsa": "error",
|
||||||
|
"@security/no-unsafe-rsa-encrypt": "error",
|
||||||
|
"@security/no-unsafe-rsa-sign": "error",
|
||||||
|
"@security/no-unsafe-rsa-key": "error",
|
||||||
|
"@security/no-unsafe-dsa-key": "error",
|
||||||
|
"@security/no-unsafe-dh-key": "error",
|
||||||
|
"@security/no-unsafe-3des": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||||
|
*/
|
||||||
|
export const HAR_VERSION = '0.0.1';
|
||||||
|
export const BUILD_MODE_NAME = 'release';
|
||||||
|
export const DEBUG = false;
|
||||||
|
export const TARGET_NAME = 'default';
|
||||||
|
export const CHANNEL = 'harmony';
|
||||||
|
export const BUILD_TIME = '2605061515';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildProfile Class is used only for compatibility purposes.
|
||||||
|
*/
|
||||||
|
export default class BuildProfile {
|
||||||
|
static readonly HAR_VERSION = HAR_VERSION;
|
||||||
|
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||||
|
static readonly DEBUG = DEBUG;
|
||||||
|
static readonly TARGET_NAME = TARGET_NAME;
|
||||||
|
static readonly CHANNEL = CHANNEL;
|
||||||
|
static readonly BUILD_TIME = BUILD_TIME;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { Logger } from './src/main/ets/utils/Logger';
|
||||||
|
|
||||||
|
export { BreakpointSystem, BreakPointType } from './src/main/ets/utils/BreakpointSystem';
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"buildOption": {
|
||||||
|
"resOptions": {
|
||||||
|
"copyCodeResource": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buildOptionSet": [
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"arkOptions": {
|
||||||
|
"obfuscation": {
|
||||||
|
"ruleOptions": {
|
||||||
|
"enable": false,
|
||||||
|
"files": [
|
||||||
|
"./obfuscation-rules.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"consumerFiles": [
|
||||||
|
"./consumer-rules.txt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
|
||||||
|
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Define project specific obfuscation rules here.
|
||||||
|
# You can include the obfuscation configuration files in the current module's build-profile.json5.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/source-obfuscation
|
||||||
|
|
||||||
|
# Obfuscation options:
|
||||||
|
# -disable-obfuscation: disable all obfuscations
|
||||||
|
# -enable-property-obfuscation: obfuscate the property names
|
||||||
|
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
|
||||||
|
# -compact: remove unnecessary blank spaces and all line feeds
|
||||||
|
# -remove-log: remove all console.* statements
|
||||||
|
# -print-namecache: print the name cache that contains the mapping from the old names to new names
|
||||||
|
# -apply-namecache: reuse the given cache file
|
||||||
|
|
||||||
|
# Keep options:
|
||||||
|
# -keep-property-name: specifies property names that you want to keep
|
||||||
|
# -keep-global-name: specifies names that you want to keep in the global scope
|
||||||
|
|
||||||
|
-enable-property-obfuscation
|
||||||
|
-enable-toplevel-obfuscation
|
||||||
|
-enable-filename-obfuscation
|
||||||
|
-enable-export-obfuscation
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@ohos/common",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "通用模块,用于公共页面、工具或者请求的模块。",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/**
|
||||||
|
* AppConstants
|
||||||
|
*/
|
||||||
|
export const APP_ID: string = '10058'; //应用ID
|
||||||
|
export const RELEASE_BASE_URL: string = 'https://jitutu.batiao8.com'; //release
|
||||||
|
export const DEBUG_BASE_URL: string = 'https://jitutu.batiao8.com'; //debug
|
||||||
|
export const LOG_REQUEST: string = 'RabbitRequest';
|
||||||
|
export const AGREEMENT_URL: string = 'https://jitutu.batiao8.com/static/policy-jietutu/user_android.html'; //用户协议
|
||||||
|
export const PRIVACY_URL: string = 'https://jitutu.batiao8.com/static/policy-jietutu/provacy.html' //隐私政策
|
||||||
|
export const AESDecrypt = "e4rOtnF8tJjtHO7ecZeJHN1rapED5ImB" //解密
|
||||||
|
export const Signature = "xn08hYoizXhZ1zHP8DVqfCm2yHxPmhil" //加密字符
|
||||||
|
|
||||||
|
export const WXAPPID = "wx7d1a7d1507482cef" // 微信APPID
|
||||||
|
export const WXSECRET = "5264c353296db25405fc29e43c40d3a5" //微信secret
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kv store是否初始化完成
|
||||||
|
*/
|
||||||
|
export const IS_KV_STORE_INIT_FINISHED: string = 'kv_store_init_finished';
|
||||||
|
/**
|
||||||
|
* 是否同意用户协议
|
||||||
|
*/
|
||||||
|
export const IS_AGREE_AGREEMENT: string = 'agree_agreement_key';
|
||||||
|
/**
|
||||||
|
* OAID,解决过审率
|
||||||
|
*/
|
||||||
|
export const DEVICE_OAID: string = 'device_oaid';
|
||||||
|
/**
|
||||||
|
* 用户token(注意:所有用户都记录-游客同样记录)
|
||||||
|
*/
|
||||||
|
export const TOKEN: string = 'account_token_key';
|
||||||
|
/**
|
||||||
|
* 登录token(注意:登录用户才记录-游客不记录,退出登录会清空)
|
||||||
|
*/
|
||||||
|
export const TOKEN_LOGIN: string = 'login_token_key';
|
||||||
|
/**
|
||||||
|
* 用户配置
|
||||||
|
*/
|
||||||
|
export const USER_PROFILE: string = 'user_profile';
|
||||||
|
/**
|
||||||
|
* 登录信息
|
||||||
|
*/
|
||||||
|
export const LOGIN_PROFILE: string = 'login_profile';
|
||||||
|
/**
|
||||||
|
* 用户信息
|
||||||
|
*/
|
||||||
|
export const USER_INFO: string = 'user_info';
|
||||||
|
/**
|
||||||
|
* uniMp本地数据版本
|
||||||
|
*/
|
||||||
|
export const UNIMP_DATA_VERSION: string = 'unimp_data_version';
|
||||||
|
/**
|
||||||
|
* 微信授权码(authCode):微信登录(授权)时保存,微信绑定时会使用到
|
||||||
|
*/
|
||||||
|
export const WX_AUTH_CODE: string = 'wx_auth_code';
|
||||||
|
/**
|
||||||
|
* 微信绑定授权
|
||||||
|
*/
|
||||||
|
export const WX_BIND_AUTHOR: string = 'wx_bind_author';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台标识(目前后台接口仅支持android和ios,暂不支持harmony)
|
||||||
|
* android、ios or harmony
|
||||||
|
*/
|
||||||
|
export const PLATFORM: string = 'android';
|
||||||
|
/**
|
||||||
|
* 备案号(filing)
|
||||||
|
*/
|
||||||
|
export const FILING_NO='渝ICP备2025064932号-17A'
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
interface CommonConstantsInterface {
|
||||||
|
breakpointsSmName: string;
|
||||||
|
breakpointsMdName: string;
|
||||||
|
breakpointsLgName: string;
|
||||||
|
breakpointsXlName: string;
|
||||||
|
breakpointsSmSize: number;
|
||||||
|
breakpointsMdSize: number;
|
||||||
|
breakpointsLgSize: number;
|
||||||
|
breakpointsXlSize: number;
|
||||||
|
breakpointsInitializeName: string;
|
||||||
|
breakpointIdInitializeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common constants for all features.
|
||||||
|
*/
|
||||||
|
export const commonConstants: CommonConstantsInterface = {
|
||||||
|
/**
|
||||||
|
* Breakpoint sm.
|
||||||
|
*/
|
||||||
|
breakpointsSmName: 'sm',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breakpoint md.
|
||||||
|
*/
|
||||||
|
breakpointsMdName: 'md',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breakpoint lg.
|
||||||
|
*/
|
||||||
|
breakpointsLgName: 'lg',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breakpoint xl.
|
||||||
|
*/
|
||||||
|
breakpointsXlName: 'xl',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The break point value of sm device.
|
||||||
|
*/
|
||||||
|
breakpointsSmSize: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The break point value of md device.
|
||||||
|
*/
|
||||||
|
breakpointsMdSize: 600,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The break point value of lg device.
|
||||||
|
*/
|
||||||
|
breakpointsLgSize: 840,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The break point value of xl device.
|
||||||
|
*/
|
||||||
|
breakpointsXlSize: 1320,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize device breakpoints.
|
||||||
|
*/
|
||||||
|
breakpointsInitializeName: 'md',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize device breakpoint Id.
|
||||||
|
*/
|
||||||
|
breakpointIdInitializeName: 'unknown'
|
||||||
|
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KVStore 初始化完成事件
|
||||||
|
*/
|
||||||
|
export const EVT_KV_INIT_DONE: number = 0;
|
||||||
|
/**
|
||||||
|
* LOGIN 获取用户信息
|
||||||
|
*/
|
||||||
|
export const EVT_USER_INFO: number = 1;
|
||||||
|
/**
|
||||||
|
* LOGIN Author 微信登录授权成功
|
||||||
|
*/
|
||||||
|
export const EVT_LOGIN_WX_AUTHOR_SUCCESS: number = 2;
|
||||||
|
/**
|
||||||
|
* BIND Author 微信绑定授权成功
|
||||||
|
*/
|
||||||
|
export const EVT_BIND_WX_AUTHOR_SUCCESS: number = 3;
|
||||||
|
/**
|
||||||
|
* NORMAL 获取用户配置信息
|
||||||
|
*/
|
||||||
|
export const EVT_USER_CONFIG_PROFILE: number = 4;
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { common, Want } from "@kit.AbilityKit";
|
||||||
|
import { AGREEMENT_URL, PRIVACY_URL } from "../constants/AppConstants";
|
||||||
|
|
||||||
|
@CustomDialog
|
||||||
|
export struct AgreementDialog {
|
||||||
|
private context = getContext(this) as common.UIAbilityContext;
|
||||||
|
// 接收外部传入的参数
|
||||||
|
confirmText: string = '同意';
|
||||||
|
cancelText: string = '不同意';
|
||||||
|
|
||||||
|
controller?: CustomDialogController;
|
||||||
|
cancel: () => void = () => {};
|
||||||
|
confirm: () => void = () => {};
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_dialog_tips_mask_top'))
|
||||||
|
.width('100%')
|
||||||
|
.opacity(0.5)
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
// 标题
|
||||||
|
Text('用户协议与隐私政策')
|
||||||
|
.fontSize(18)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.margin({ top: 30, bottom: 32 })
|
||||||
|
|
||||||
|
// 描述文字
|
||||||
|
Text(){//'请您务必审慎阅读、充分理解《用户协议》与《隐私政策》各条款,包括但不限于:为了更好的向您提供服务,我们需要访问您的相册、相机等。您可以阅读《隐私政策》了解详细信息如果您同意,请点击下面“同意”按钮开始接受我们的服务。'
|
||||||
|
Span('请您务必审慎阅读、充分理解')
|
||||||
|
.fontColor('#CCAAAAAA')
|
||||||
|
Span('《用户协议》')
|
||||||
|
.fontColor('#FFAAAAAA') // 高亮颜色(深灰)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.onClick(() => {
|
||||||
|
const context = getContext(this) as common.UIAbilityContext;
|
||||||
|
webBrowser(context, AGREEMENT_URL)
|
||||||
|
})
|
||||||
|
Span('与')
|
||||||
|
.fontColor('#CCAAAAAA')
|
||||||
|
Span('《隐私政策》')
|
||||||
|
.fontColor('#FFAAAAAA') // 高亮颜色(深灰)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.onClick(() => {
|
||||||
|
const context = getContext(this) as common.UIAbilityContext;
|
||||||
|
webBrowser(context, PRIVACY_URL)
|
||||||
|
})
|
||||||
|
Span('各条款,包括但不限于:为了更好的向您提供服务,我们需要访问您的相册、相机等。您可以阅读')
|
||||||
|
.fontColor('#CCAAAAAA')
|
||||||
|
Span('《隐私政策》')
|
||||||
|
.fontColor('#FFAAAAAA') // 高亮颜色(深灰)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.onClick(() => {
|
||||||
|
const context = getContext(this) as common.UIAbilityContext;
|
||||||
|
webBrowser(context, PRIVACY_URL)
|
||||||
|
})
|
||||||
|
Span('了解详细信息如果您同意,请点击下面“同意”按钮开始接受我们的服务。')
|
||||||
|
.fontColor('#CCAAAAAA')
|
||||||
|
}
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.margin({ left: 16, right: 16 })
|
||||||
|
|
||||||
|
// 按钮区域
|
||||||
|
Column() {
|
||||||
|
Button(this.confirmText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.confirm();
|
||||||
|
this.controller?.close();
|
||||||
|
})
|
||||||
|
.width('100%')
|
||||||
|
.backgroundColor(Color.Black) // 对应图片中的黑色按钮
|
||||||
|
.fontColor(Color.White)
|
||||||
|
.height(44)
|
||||||
|
|
||||||
|
Button(this.cancelText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.controller?.close();
|
||||||
|
this.cancel();
|
||||||
|
this.context.terminateSelf();
|
||||||
|
})
|
||||||
|
.width('100%')
|
||||||
|
.backgroundColor('#FFFFFFFF')
|
||||||
|
.fontColor('#FFAAAAAA')
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.margin({ top: 68})
|
||||||
|
.padding({ left: 20, right: 20 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('90%')
|
||||||
|
.alignContent(Alignment.TopStart)
|
||||||
|
.backgroundColor(Color.White)
|
||||||
|
.borderRadius(24) // 较大的圆角
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动时,协议使用外部浏览器打开
|
||||||
|
* @param context
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
function webBrowser(context: common.UIAbilityContext, url: string){
|
||||||
|
const want: Want = {
|
||||||
|
action: 'ohos.want.action.viewData',
|
||||||
|
entities: ['entity.system.browsable'],
|
||||||
|
uri: url
|
||||||
|
};
|
||||||
|
|
||||||
|
context.startAbility(want);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
@CustomDialog
|
||||||
|
export struct GlobalDownloadingDialog {
|
||||||
|
controller: CustomDialogController;
|
||||||
|
@State show: boolean = false;
|
||||||
|
@State progress: number = 0;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
if(this.progress > 0){
|
||||||
|
Column(){
|
||||||
|
Progress({
|
||||||
|
value: this.progress,
|
||||||
|
total: 100,
|
||||||
|
type: ProgressType.Linear
|
||||||
|
})
|
||||||
|
.width('100%')
|
||||||
|
.height(10)
|
||||||
|
.color('#ff4f8af6')
|
||||||
|
.backgroundColor('#ebebeb')
|
||||||
|
.style({ strokeWidth: 10, enableSmoothEffect: true })
|
||||||
|
|
||||||
|
Text('下载中,请稍后:' + this.progress + '%').fontColor(Color.White).fontSize(12).margin({ top: 4})
|
||||||
|
}
|
||||||
|
.margin({ left:8, right:8 })
|
||||||
|
}else{
|
||||||
|
Column(){
|
||||||
|
LoadingProgress()
|
||||||
|
.width(60)
|
||||||
|
.color('#ff4f8af6')
|
||||||
|
|
||||||
|
Text('加载中,请稍后...').fontColor(Color.White).fontSize(12).margin({ top: 12})
|
||||||
|
}
|
||||||
|
.margin({ left:8, right:8 })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('#99000000')
|
||||||
|
.borderRadius(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
@CustomDialog
|
||||||
|
export struct TipsDialog {
|
||||||
|
// 接收外部传入的参数
|
||||||
|
title: string = '提示';
|
||||||
|
content: string = '';
|
||||||
|
desc: string = '';
|
||||||
|
textAlign: TextAlign = TextAlign.Center;
|
||||||
|
confirmText: string = '确定';
|
||||||
|
cancelText: string = '取消';
|
||||||
|
|
||||||
|
controller: CustomDialogController;
|
||||||
|
cancel: () => void = () => {};
|
||||||
|
confirm: () => void = () => {};
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_dialog_tips_mask_top'))
|
||||||
|
.width('100%')
|
||||||
|
.opacity(0.5)
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
// 标题
|
||||||
|
Text(this.title)
|
||||||
|
.fontSize(18)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.margin({ top: 30, bottom: 12 })
|
||||||
|
|
||||||
|
// 描述文字
|
||||||
|
Text(this.content)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.textAlign(this.textAlign)
|
||||||
|
.margin({ left: 16, right: 16 })
|
||||||
|
Text(this.desc)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.textAlign(this.textAlign)
|
||||||
|
.margin({ left: 16, right: 16, bottom: 30 })
|
||||||
|
|
||||||
|
// 按钮区域
|
||||||
|
Row({ space: 20 }) {
|
||||||
|
Button(this.cancelText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.controller.close();
|
||||||
|
this.cancel();
|
||||||
|
})
|
||||||
|
.backgroundColor('#FFFFFFFF')
|
||||||
|
.fontColor('#333333')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.borderWidth(0.5)
|
||||||
|
.borderColor('#FF252525')
|
||||||
|
.height(44)
|
||||||
|
|
||||||
|
Button(this.confirmText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.confirm();
|
||||||
|
this.controller.close();
|
||||||
|
})
|
||||||
|
.backgroundColor(Color.Black) // 对应图片中的黑色按钮
|
||||||
|
.fontColor(Color.White)
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 20, right: 20, bottom: 20 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('90%')
|
||||||
|
.alignContent(Alignment.TopStart)
|
||||||
|
.backgroundColor(Color.White)
|
||||||
|
.borderRadius(24) // 较大的圆角
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { UIContext, ComponentContent } from '@kit.ArkUI';
|
||||||
|
|
||||||
|
// 1. 定义包裹组件 (保持不变)
|
||||||
|
@Component
|
||||||
|
struct ToastDeleteImage {
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_toast_delete_account'))
|
||||||
|
.width(91)
|
||||||
|
.height(91)
|
||||||
|
.objectFit(ImageFit.Contain)
|
||||||
|
}
|
||||||
|
.width(120)
|
||||||
|
.height(120)
|
||||||
|
.backgroundColor('#00000000')
|
||||||
|
.borderRadius(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 包装 Builder (跨模块调用的核心)
|
||||||
|
@Builder
|
||||||
|
function toastDeleteBuilder() {
|
||||||
|
ToastDeleteImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToastDeleteUtils {
|
||||||
|
private static content: ComponentContent<Object> | null = null;
|
||||||
|
|
||||||
|
static showDeleteToast(uiContext: UIContext, duration: number = 2000): void {
|
||||||
|
// 如果已有弹窗,先清理
|
||||||
|
if (ToastDeleteUtils.content) {
|
||||||
|
uiContext.getPromptAction().closeCustomDialog(ToastDeleteUtils.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 wrapBuilder 确保 UI 上下文正确
|
||||||
|
ToastDeleteUtils.content = new ComponentContent(uiContext, wrapBuilder(toastDeleteBuilder));
|
||||||
|
|
||||||
|
const prompt = uiContext.getPromptAction();
|
||||||
|
|
||||||
|
// 参数1: content 实例 参数2: 配置对象 (BaseDialogOptions)
|
||||||
|
prompt.openCustomDialog(ToastDeleteUtils.content, {
|
||||||
|
autoCancel: true,
|
||||||
|
alignment: DialogAlignment.Center,
|
||||||
|
maskColor: '#00000000'
|
||||||
|
}).then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ToastDeleteUtils.content) {
|
||||||
|
prompt.closeCustomDialog(ToastDeleteUtils.content);
|
||||||
|
ToastDeleteUtils.content = null;
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { UIContext, ComponentContent, promptAction } from '@kit.ArkUI';
|
||||||
|
import deviceInfo from '@ohos.deviceInfo';
|
||||||
|
|
||||||
|
interface ToastParams {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
// 1.定义包裹组件 (保持不变)
|
||||||
|
@Component
|
||||||
|
struct Toast {
|
||||||
|
@Prop message: string = '';
|
||||||
|
build() {
|
||||||
|
Stack(){
|
||||||
|
Text(this.message)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
.margin(12)
|
||||||
|
}
|
||||||
|
.width(100)
|
||||||
|
.height(100)
|
||||||
|
.backgroundColor('#cc000000')
|
||||||
|
.borderRadius(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.包装 Builder (跨模块调用的核心)
|
||||||
|
@Builder
|
||||||
|
function toastBuilder(params: ToastParams) {
|
||||||
|
Toast({ message: params.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToastUtils {
|
||||||
|
private static content: ComponentContent<ToastParams> | null = null;
|
||||||
|
|
||||||
|
static showToast(uiContext: UIContext, message: string, duration: number = 2000): void {
|
||||||
|
// 如果已有弹窗,先关闭旧的
|
||||||
|
if (ToastUtils.content) {
|
||||||
|
uiContext.getPromptAction().closeCustomDialog(ToastUtils.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.在创建 ComponentContent 时,第三个参数传入数据对象
|
||||||
|
const params: ToastParams = { message: message };
|
||||||
|
ToastUtils.content = new ComponentContent(uiContext, wrapBuilder(toastBuilder), params);
|
||||||
|
|
||||||
|
const prompt = uiContext.getPromptAction();
|
||||||
|
|
||||||
|
prompt.openCustomDialog(ToastUtils.content, {
|
||||||
|
autoCancel: true,
|
||||||
|
alignment: DialogAlignment.Center,
|
||||||
|
maskColor: '#00000000'
|
||||||
|
}).then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ToastUtils.content) {
|
||||||
|
prompt.closeCustomDialog(ToastUtils.content);
|
||||||
|
ToastUtils.content = null;
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static normalToast(options: ToastOptions): void {
|
||||||
|
// 设置默认值
|
||||||
|
const message = options.message;
|
||||||
|
const duration = options.duration ?? 2000;
|
||||||
|
const bottom = options.bottom ?? '80%'; // 系统的默认位置通常在底部
|
||||||
|
|
||||||
|
if (deviceInfo.sdkApiVersion < 18) {
|
||||||
|
promptAction.showToast({
|
||||||
|
message: message,
|
||||||
|
duration: duration,
|
||||||
|
bottom: bottom
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promptAction.openToast({
|
||||||
|
message: message,
|
||||||
|
duration: duration,
|
||||||
|
bottom: bottom
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToastOptions {
|
||||||
|
message: string;
|
||||||
|
duration?: number; // 可选
|
||||||
|
bottom?: string | number; // 可选
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
@CustomDialog
|
||||||
|
export struct UpdateTipsDialog {
|
||||||
|
// 接收外部传入的参数
|
||||||
|
title: string = '提示';
|
||||||
|
version: string = '';
|
||||||
|
desc: string = '';
|
||||||
|
confirmText: string = '确定';
|
||||||
|
cancelText: string = '取消';
|
||||||
|
|
||||||
|
// 是否强制更新
|
||||||
|
isForce: boolean = false;
|
||||||
|
|
||||||
|
controller: CustomDialogController;
|
||||||
|
cancel: () => void = () => {};
|
||||||
|
confirm: () => void = () => {};
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_dialog_tips_mask_top'))
|
||||||
|
.width('100%')
|
||||||
|
.opacity(0.5)
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
// 标题
|
||||||
|
Text(this.title)
|
||||||
|
.fontSize(18)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.margin({ top: 30, bottom: 12 })
|
||||||
|
|
||||||
|
// 版本号
|
||||||
|
Text(this.version)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.margin({ bottom: 30 })
|
||||||
|
// 描述
|
||||||
|
Text(this.desc)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.lineHeight(14 * 1.5)
|
||||||
|
.margin({ bottom: 30 })
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 20, right: 20})
|
||||||
|
|
||||||
|
// 按钮区域
|
||||||
|
Row({ space: 20 }) {
|
||||||
|
if (!this.isForce) {
|
||||||
|
Button(this.cancelText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.controller.close();
|
||||||
|
this.cancel();
|
||||||
|
})
|
||||||
|
.backgroundColor('#FFFFFFFF')
|
||||||
|
.fontColor('#333333')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.borderWidth(0.5)
|
||||||
|
.borderColor('#FF252525')
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(this.confirmText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.confirm();
|
||||||
|
this.controller.close();
|
||||||
|
})
|
||||||
|
.backgroundColor(Color.Black) // 对应图片中的黑色按钮
|
||||||
|
.fontColor(Color.White)
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 20, right: 20, bottom: 20 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('90%')
|
||||||
|
.alignContent(Alignment.TopStart)
|
||||||
|
.backgroundColor(Color.White)
|
||||||
|
.borderRadius(24) // 较大的圆角
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
import axios, {
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosResponse,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
AxiosError,
|
||||||
|
FormData,
|
||||||
|
AxiosRequestHeaders,
|
||||||
|
AxiosRequestConfig
|
||||||
|
} from '@ohos/axios';
|
||||||
|
import { DEBUG_BASE_URL, RELEASE_BASE_URL } from '../constants/AppConstants';
|
||||||
|
import { BaseResponse } from './BaseResponse';
|
||||||
|
import { HeaderInterceptor } from './HeaderInterceptor';
|
||||||
|
import { EncryptConfig, RequestInterceptor } from './RequestInterceptor';
|
||||||
|
import { ResponseInterceptor } from './ResponseInterceptor';
|
||||||
|
import { AppUtil } from '@pura/harmony-utils';
|
||||||
|
import { Logger } from '../utils/Logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟 Android 的 ApiManager
|
||||||
|
*/
|
||||||
|
export class ApiManager {
|
||||||
|
private static instance: ApiManager | null = null;
|
||||||
|
private retrofit: AxiosInstance | null = null;
|
||||||
|
private unsafeRetrofit: AxiosInstance | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ApiManager {
|
||||||
|
if (ApiManager.instance === null) {
|
||||||
|
ApiManager.instance = new ApiManager();
|
||||||
|
}
|
||||||
|
return ApiManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 集成的泛型请求方法(简单通用的普通请求)
|
||||||
|
* @param url 请求路径
|
||||||
|
* @param params GET请求的Query参数 (对应 Android 的 @Query)
|
||||||
|
* @param method 请求方法
|
||||||
|
* @param data 可选的 Body 数据
|
||||||
|
*/
|
||||||
|
public async request<T>(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, Object>,
|
||||||
|
method: string = 'get',
|
||||||
|
data?: Object
|
||||||
|
): Promise<BaseResponse<T>> {
|
||||||
|
const service = this.getUnsafeService();
|
||||||
|
|
||||||
|
const response: AxiosResponse<BaseResponse<T>> = await service.request({
|
||||||
|
url: url,
|
||||||
|
method: method,
|
||||||
|
params: params,
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 集成的泛型请求方法(特殊处理需要映射字段问题)
|
||||||
|
* @param url 请求路径
|
||||||
|
* @param params GET请求的Query参数 (对应 Android 的 @Query)
|
||||||
|
* @param mapper 可选转换函数
|
||||||
|
* @param method 请求方法
|
||||||
|
* @param data 可选的 Body 数据
|
||||||
|
*/
|
||||||
|
public async requestForMapper<T>(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, Object>,
|
||||||
|
mapper?: (raw: Record<string, Object>) => T,
|
||||||
|
method: string = 'get', // 新增:默认为 get
|
||||||
|
data?: Object // 新增:用于 POST 的 Body 数据
|
||||||
|
): Promise<T> {
|
||||||
|
const service = this.getUnsafeService();
|
||||||
|
|
||||||
|
// 1. 使用通用的 request 方法,根据参数自动切换 GET/POST
|
||||||
|
const response: AxiosResponse<BaseResponse<Object>> = await service.request({
|
||||||
|
url: url,
|
||||||
|
method: method, // 传入 'get' 或 'post'
|
||||||
|
params: params, // URL 上的参数(nonce, timestamp, signature 都在这)
|
||||||
|
data: data // Body 里的参数(phone 在这)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 提取业务数据 data 部分
|
||||||
|
const rawData = (response.data.data ?? {}) as Record<string, Object>;
|
||||||
|
|
||||||
|
// 3. 逻辑转换
|
||||||
|
if (mapper !== undefined) {
|
||||||
|
return mapper(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 强转逻辑
|
||||||
|
return rawData as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传方法
|
||||||
|
* @param url 请求路径
|
||||||
|
* @param formData FormData 实例
|
||||||
|
* @param params Query 参数 (用于签名)
|
||||||
|
*/
|
||||||
|
public async upload<T>(
|
||||||
|
url: string,
|
||||||
|
formData: FormData,
|
||||||
|
params?: Record<string, Object>
|
||||||
|
): Promise<T> {
|
||||||
|
const service = this.getUnsafeService();
|
||||||
|
|
||||||
|
// 构造配置对象,避免使用 any
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
params: params,
|
||||||
|
context: getContext(),
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.total) {
|
||||||
|
Logger.info(RequestInterceptor.TAG, `[上传进度] 已传: ${progressEvent.loaded} Bytes/ 总计: ${progressEvent.total} Bytes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: AxiosResponse<BaseResponse<Object>> = await service.post(url, formData, config);
|
||||||
|
const rawData = (response.data.data ?? {}) as Record<string, Object>;
|
||||||
|
return rawData as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize(): void {
|
||||||
|
const isDebug: boolean = AppUtil.debug();
|
||||||
|
const baseUrl: string = isDebug ? DEBUG_BASE_URL : RELEASE_BASE_URL;
|
||||||
|
|
||||||
|
// 1. 初始化标准实例
|
||||||
|
if (this.retrofit === null) {
|
||||||
|
this.retrofit = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
// 基础请求拦截器
|
||||||
|
this.retrofit.interceptors.request.use(this.requestInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始化不安全实例
|
||||||
|
if (this.unsafeRetrofit === null) {
|
||||||
|
this.unsafeRetrofit = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
timeout: 60000,
|
||||||
|
// 这里的 data 类型使用 Object, headers 使用 AxiosRequestHeaders
|
||||||
|
transformRequest: [(data: Object, headers: AxiosRequestHeaders): Object => {
|
||||||
|
// 1. 判断是否为 FormData
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
headers.set('Content-Type', 'multipart/form-data');
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 判断是否为普通对象 (非空)
|
||||||
|
if (data !== null && typeof data === 'object') {
|
||||||
|
headers.set('Content-Type', 'application/json');
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 其他情况原样返回 (转为 Object 兼容类型)
|
||||||
|
return data;
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- 添加请求拦截器链 (注意执行顺序:从下往上/链式触发) ---
|
||||||
|
|
||||||
|
// 添加 Header 拦截器 (注入 Token, 设备信息等)
|
||||||
|
this.unsafeRetrofit.interceptors.request.use(HeaderInterceptor);
|
||||||
|
|
||||||
|
// 添加 RequestInterceptor (处理签名、UUID、Nonce、MD5 日志)
|
||||||
|
// 注意:因为 onPrepare 是 async,Axios 会自动等待它执行完
|
||||||
|
this.unsafeRetrofit.interceptors.request.use(
|
||||||
|
(config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> => {
|
||||||
|
return RequestInterceptor.onPrepare(config as EncryptConfig);
|
||||||
|
},
|
||||||
|
(error: AxiosError): Promise<AxiosError> => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 添加响应拦截器链 ---
|
||||||
|
|
||||||
|
// 添加 ResponseInterceptor (处理解密、状态码修正、响应日志)
|
||||||
|
this.unsafeRetrofit.interceptors.response.use(
|
||||||
|
(response: AxiosResponse): Promise<AxiosResponse> => {
|
||||||
|
// 先执行解密逻辑
|
||||||
|
return ResponseInterceptor.responseHandler(response);
|
||||||
|
},
|
||||||
|
(error: AxiosError): Promise<AxiosError> => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 可以在这里继续添加统一的错误处理拦截器
|
||||||
|
this.unsafeRetrofit.interceptors.response.use(
|
||||||
|
(response: AxiosResponse): AxiosResponse => {
|
||||||
|
// 执行日志打印
|
||||||
|
return RequestInterceptor.onResponse(response);
|
||||||
|
},
|
||||||
|
this.responseErrorInterceptor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新初始化 (对应 Android 的 reinitialize)
|
||||||
|
*/
|
||||||
|
public reinitialize(): void {
|
||||||
|
this.retrofit = null;
|
||||||
|
this.unsafeRetrofit = null;
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 强类型拦截器实现 ---
|
||||||
|
|
||||||
|
private requestInterceptor(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
|
||||||
|
// 类似于 Android 的 RequestInterceptor
|
||||||
|
config.headers.set('Content-Type', 'application/json');
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private responseErrorInterceptor(error: AxiosError): Promise<AxiosError> {
|
||||||
|
// 错误处理逻辑
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 获取 Service 实例 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取不安全/特殊配置的请求实例
|
||||||
|
* 在 ArkTS 中直接返回 AxiosInstance 进行调用
|
||||||
|
*/
|
||||||
|
public getUnsafeService(): AxiosInstance {
|
||||||
|
if (this.unsafeRetrofit === null) {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
return this.unsafeRetrofit as AxiosInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// 返回结构:基础返回结构
|
||||||
|
export interface BaseResponse<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
encrypt: boolean; // 对应你 ResponseInterceptor 中的加密标识
|
||||||
|
}
|
||||||
|
// 返回结构:用于文件上传
|
||||||
|
export interface UploadResponse {
|
||||||
|
status: boolean;
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface RequestCallback<T = Object> {
|
||||||
|
onSuccess?: (data: T) => void; // 修改这里:接收一个参数
|
||||||
|
onFailure?: (message:string) => void;
|
||||||
|
onError?: (err: Error) => void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { InternalAxiosRequestConfig } from '@ohos/axios';
|
||||||
|
import { bundleManager } from '@kit.AbilityKit';
|
||||||
|
import deviceInfo from '@ohos.deviceInfo';
|
||||||
|
import { KVStore } from '../utils/KVStore';
|
||||||
|
import { APP_ID, PLATFORM, TOKEN } from '../constants/AppConstants';
|
||||||
|
import { Logger } from '../utils/Logger';
|
||||||
|
import { DeviceUtil } from '@pura/harmony-utils';
|
||||||
|
import { CHANNEL } from '../../../../BuildProfile';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对应 Android 的 HeaderInterceptor
|
||||||
|
*/
|
||||||
|
export const HeaderInterceptor = async (config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> => {
|
||||||
|
try {
|
||||||
|
// 1. 获取应用信息 (对应 BuildConfig/getAppVersionName)
|
||||||
|
const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
|
||||||
|
const versionName = bundleInfo.versionName;
|
||||||
|
const bundleName = bundleInfo.name;
|
||||||
|
|
||||||
|
// 2. 获取 Header 所需参数
|
||||||
|
const accessToken: string = KVStore.getInstance().getSync(TOKEN, '');
|
||||||
|
const deviceId: string = DeviceUtil.getDeviceId();
|
||||||
|
const brand: string = deviceInfo.brand;
|
||||||
|
const model: string = deviceInfo.productModel;
|
||||||
|
|
||||||
|
// 3. 注入 Header (对应 mBuilder.header)
|
||||||
|
config.headers.set('x-token', accessToken);
|
||||||
|
config.headers.set('x-version', versionName);
|
||||||
|
config.headers.set('x-platform', PLATFORM); // 鸿蒙平台标识(目前只有android和ios,暂不支持harmony)
|
||||||
|
config.headers.set('x-device-id', deviceId);
|
||||||
|
config.headers.set('x-mobile-brand', brand);
|
||||||
|
config.headers.set('x-mobile-model', model);
|
||||||
|
config.headers.set('x-package', bundleName);
|
||||||
|
config.headers.set('x-base-version', versionName);
|
||||||
|
config.headers.set('x-app-id', APP_ID);
|
||||||
|
config.headers.set('x-channel', CHANNEL);
|
||||||
|
|
||||||
|
// 4. 打印日志 (对应 Android 的 StringBuilder 日志)
|
||||||
|
let logString = "\n-------------> 📤 header start<-------------\n";
|
||||||
|
logString += `│ x-token = ${accessToken}\n`;
|
||||||
|
logString += `│ x-version = ${versionName}\n`;
|
||||||
|
logString += `│ x-platform = ${PLATFORM}\n`;
|
||||||
|
logString += `│ x-device-id = ${deviceId}\n`;
|
||||||
|
logString += `│ x-mobile-brand = ${brand}\n`;
|
||||||
|
logString += `│ x-mobile-model = ${model}\n`;
|
||||||
|
logString += `│ x-package = ${bundleName}\n`;
|
||||||
|
logString += `│ x-base-version = ${versionName}\n`;
|
||||||
|
logString += `│ x-app-id = ${APP_ID}\n`;
|
||||||
|
logString += `│ x-channel = ${CHANNEL}\n`;
|
||||||
|
logString += "-------------> header end <-------------";
|
||||||
|
|
||||||
|
Logger.info('RabbitLog', logString);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 对应 Android 的 catch (e: Exception)
|
||||||
|
console.error(`HeaderInterceptor Error: ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { InternalAxiosRequestConfig, AxiosResponse, FormData } from '@ohos/axios';
|
||||||
|
import { Signature } from '../constants/AppConstants';
|
||||||
|
import { Logger } from '../utils/Logger';
|
||||||
|
import { RandomUtil, MD5, JSONUtil, StrUtil } from '@pura/harmony-utils';
|
||||||
|
import systemDateTime from '@ohos.systemDateTime';
|
||||||
|
|
||||||
|
export interface EncryptConfig extends InternalAxiosRequestConfig {
|
||||||
|
startTime?: number;
|
||||||
|
params?: Record<string, string | number | boolean | null | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RequestInterceptor {
|
||||||
|
static readonly TAG: string = "RabbitLog_Request";
|
||||||
|
|
||||||
|
static async onPrepare(config: EncryptConfig): Promise<EncryptConfig> {
|
||||||
|
config.startTime = Date.now();
|
||||||
|
|
||||||
|
// 1. 获取请求方法
|
||||||
|
const method = (config.method ?? "get").toLowerCase();
|
||||||
|
|
||||||
|
// 2. 准备基础参数 (Query Params)
|
||||||
|
config.params = config.params || {};
|
||||||
|
config.params.nonce = RandomUtil.generateUUID36()
|
||||||
|
config.params.timestamp = Math.trunc(systemDateTime.getTime() / 1000)
|
||||||
|
|
||||||
|
// 3. 对 Query 参数进行字典排序并拼接
|
||||||
|
let paramsMap = JSONUtil.jsonToMap(JSON.stringify(config.params));
|
||||||
|
let arrayMap = Array.from(paramsMap);
|
||||||
|
arrayMap.sort((a, b) => {
|
||||||
|
return a[0].localeCompare(b[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
paramsMap = new Map(arrayMap)
|
||||||
|
let sortQueryString = "";
|
||||||
|
paramsMap.forEach((value, key) => {
|
||||||
|
sortQueryString += key + "=" + value + "&"
|
||||||
|
})
|
||||||
|
sortQueryString = encodeURI(sortQueryString.substring(0, sortQueryString.length - 1))
|
||||||
|
|
||||||
|
// 4. 准备签名盐值
|
||||||
|
let signature = MD5.digestSync(sortQueryString + '&' + MD5.digestSync(Signature));
|
||||||
|
|
||||||
|
// 5. 【核心判断】识别是否为文件上传
|
||||||
|
const contentType = String(config.headers?.['Content-Type'] || "");
|
||||||
|
const isFormData = config.data instanceof FormData || contentType.includes('multipart/form-data');
|
||||||
|
|
||||||
|
// 6. 根据请求类型和内容计算签名
|
||||||
|
if ((method === 'post' || method === 'put') && config.data) {
|
||||||
|
if(!isFormData){
|
||||||
|
let dataStr = JSON.stringify(config.data);
|
||||||
|
if (StrUtil.isNotEmpty(dataStr)) {
|
||||||
|
signature = MD5.digestSync(sortQueryString + '&' + dataStr + "&" + MD5.digestSync(Signature));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 回填带签名的参数
|
||||||
|
config.params.signature = signature;
|
||||||
|
|
||||||
|
// 8. 打印请求日志
|
||||||
|
RequestInterceptor.logRequest(config, isFormData);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static onResponse(response: AxiosResponse): AxiosResponse {
|
||||||
|
const config = response.config as EncryptConfig;
|
||||||
|
const startTime: number = config.startTime ?? Date.now();
|
||||||
|
RequestInterceptor.logResponse(response, startTime);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static logRequest(config: EncryptConfig, isFormData: boolean): void {
|
||||||
|
let logString = `\n┌─── 📤 Request [ ${config.method?.toUpperCase()} ] ───\n`;
|
||||||
|
logString += `│ Url: ${config.baseURL}${config.url}\n`;
|
||||||
|
logString += `│ Final Params: ${JSON.stringify(config.params)}\n`;
|
||||||
|
|
||||||
|
if (isFormData) {
|
||||||
|
logString += `│ Body: [ Multipart/FormData - File Upload ]\n`;
|
||||||
|
} else {
|
||||||
|
logString += `│ Body: ${JSON.stringify(config.data)}\n`;
|
||||||
|
}
|
||||||
|
logString += `└─────────────────────────────────────\n`;
|
||||||
|
Logger.info(RequestInterceptor.TAG, logString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static logResponse(response: AxiosResponse, startTime: number): void {
|
||||||
|
const duration: number = Date.now() - startTime;
|
||||||
|
const config = response.config as EncryptConfig;
|
||||||
|
|
||||||
|
// 0. 完整的请求地址(包含参数拼接)
|
||||||
|
let fullUrl = `${config.baseURL ?? ""}${config.url ?? ""}`;
|
||||||
|
const params = config.params;
|
||||||
|
let queryStr = "";
|
||||||
|
if (params !== undefined && params !== null) {
|
||||||
|
const keys = Object.keys(params);
|
||||||
|
if (keys.length > 0) {
|
||||||
|
const queryParts: string[] = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = params[key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queryStr = queryParts.join('&');
|
||||||
|
const separator = fullUrl.includes('?') ? '&' : '?';
|
||||||
|
fullUrl += `${separator}${queryStr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 获取完整的 Data 字符串
|
||||||
|
const dataStr: string = JSON.stringify(response.data);
|
||||||
|
|
||||||
|
// 2. 构造基础信息
|
||||||
|
let header ='';
|
||||||
|
header =`\n┌─── 📥 响应开始🔜Response [ ${response.status} ] [ ${duration}ms ] ───\n`;
|
||||||
|
header += `│ Full URL: ${fullUrl}\n`;
|
||||||
|
header += `│ Data Content: `;
|
||||||
|
|
||||||
|
// 3. 拼接完整报文
|
||||||
|
const fullLog = header + dataStr + `\n└────────────────────响应结束🔚─────────────────`;
|
||||||
|
|
||||||
|
// 4. 执行分段连贯打印
|
||||||
|
RequestInterceptor.printInChunks(fullLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分段打印并确保数据完全打印
|
||||||
|
*/
|
||||||
|
private static printInChunks(message: string): void {
|
||||||
|
const CHUNK_SIZE = 3000;
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
while (start < message.length) {
|
||||||
|
let end = Math.min(start + CHUNK_SIZE, message.length);
|
||||||
|
let chunk = message.substring(start, end);
|
||||||
|
Logger.info(RequestInterceptor.TAG, chunk);
|
||||||
|
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
private static logResponse(response: AxiosResponse, startTime: number): void {
|
||||||
|
const duration: number = Date.now() - startTime;
|
||||||
|
const config = response.config as EncryptConfig; // 显式类型转换
|
||||||
|
|
||||||
|
// 1. 安全地获取 BaseURL 和 URL
|
||||||
|
const baseUrl: string = config.baseURL ?? "";
|
||||||
|
let urlPath: string = config.url ?? "";
|
||||||
|
let fullUrl: string = `${baseUrl}${urlPath}`;
|
||||||
|
|
||||||
|
// 2. 强类型处理 Query Params (对应 Android 的 request.url().toString())
|
||||||
|
const params = config.params;
|
||||||
|
if (params !== undefined && params !== null) {
|
||||||
|
const keys: string[] = Object.keys(params);
|
||||||
|
if (keys.length > 0) {
|
||||||
|
const queryParts: string[] = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = params[key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const queryString: string = queryParts.join('&');
|
||||||
|
const separator: string = fullUrl.includes('?') ? '&' : '?';
|
||||||
|
fullUrl += `${separator}${queryString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 构造日志字符串
|
||||||
|
let logString =''
|
||||||
|
logString =`\n┌─── 📥 Response [ ${String(response.status)} ] ───\n`;
|
||||||
|
logString += `│ Time: ${String(duration)}ms\n`;
|
||||||
|
logString += `│ Full Request URL: ${fullUrl}\n`;
|
||||||
|
|
||||||
|
// 4. 处理 Response Data (禁止使用 any,需转换为 string)
|
||||||
|
const dataObj = response.data as object; // 假设返回的是对象
|
||||||
|
const dataStr: string = JSON.stringify(dataObj);
|
||||||
|
const displayData: string = dataStr.length > 1000
|
||||||
|
? `${dataStr.substring(0, 1000)}... [Truncated]`
|
||||||
|
: dataStr;
|
||||||
|
|
||||||
|
logString += `│ Data: ${displayData}\n`;
|
||||||
|
logString += `└─────────────────────────────────────\n`;
|
||||||
|
|
||||||
|
Logger.info(RequestInterceptor.TAG, logString);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间同步工具
|
||||||
|
*/
|
||||||
|
export class TimeSync {
|
||||||
|
static timeOffset: number = 0; // 服务器时间 - 本地时间
|
||||||
|
|
||||||
|
static async sync(serverTimeStr: string) {
|
||||||
|
const serverTime = parseInt(serverTimeStr, 10);
|
||||||
|
const localTime = Math.trunc(Date.now() / 1000);
|
||||||
|
TimeSync.timeOffset = serverTime - localTime;
|
||||||
|
console.info(`[TimeSync] Offset calculated: ${TimeSync.timeOffset}s`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCorrectedTime(): number {
|
||||||
|
// return Math.trunc(Date.now() / 1000) + TimeSync.timeOffset;
|
||||||
|
return Math.trunc(systemDateTime.getTime() / 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { AxiosResponse } from '@ohos/axios';
|
||||||
|
import { AESDecrypt, DEBUG_BASE_URL, RELEASE_BASE_URL } from '../constants/AppConstants';
|
||||||
|
import { CryptoHelper, CryptoUtil, AES, StrUtil } from '@pura/harmony-utils';
|
||||||
|
|
||||||
|
export class ResponseInterceptor {
|
||||||
|
private static readonly TAG: string = "RabbitLog_Response";
|
||||||
|
|
||||||
|
static async responseHandler(response: AxiosResponse): Promise<AxiosResponse> {
|
||||||
|
const url: string = response.config.baseURL ?? "";
|
||||||
|
|
||||||
|
const isDebug: boolean = true;
|
||||||
|
const baseUrl: string = isDebug ? DEBUG_BASE_URL : RELEASE_BASE_URL;
|
||||||
|
|
||||||
|
// 1. 过滤非业务接口
|
||||||
|
if (!url.startsWith(baseUrl)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
// 2. 检查 Content-Type
|
||||||
|
let contentType = response.headers['content-type'] as string;
|
||||||
|
let dataString: string = '';
|
||||||
|
// 3. 解析响应体 (Axios 默认已将 data 转为对象)
|
||||||
|
if (StrUtil.isNotEmpty(contentType) && contentType?.includes("application/json") && response.data !== null) {
|
||||||
|
let isEncrypt = response.data['encrypt'] as boolean
|
||||||
|
dataString = response.data['data'] as string;
|
||||||
|
if (isEncrypt) {
|
||||||
|
console.info(`[${ResponseInterceptor.TAG}] 是否需要解密: 需要`);
|
||||||
|
dataString = ResponseInterceptor.decryptNormal(dataString, AESDecrypt);
|
||||||
|
} else {
|
||||||
|
console.info(`[${ResponseInterceptor.TAG}] 是否需要解密: 不需要`);
|
||||||
|
let newResponse: Record<string, Object> = {
|
||||||
|
'code': 0,
|
||||||
|
'data': dataString
|
||||||
|
}
|
||||||
|
dataString = JSON.stringify(newResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.data = JSON.parse(dataString);
|
||||||
|
|
||||||
|
// 兼容某些接口 400 情况
|
||||||
|
if (response.status === 400) {
|
||||||
|
response.status = 200;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as Error;
|
||||||
|
console.error(`[${ResponseInterceptor.TAG}] 解密失败: ${err.message}`);
|
||||||
|
return Promise.reject(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decryptNormal(encryptStr: string, key: string): string {
|
||||||
|
let dataBlob = CryptoHelper.strToDataBlob(encryptStr, 'base64');
|
||||||
|
let keyBytes = CryptoUtil.getConvertSymKeySync('AES256', key, 'utf-8')
|
||||||
|
let ivParams= CryptoUtil.getIvParamsSpec(key.substring(0, 16), 'utf-8')
|
||||||
|
let plain = AES.decryptCBCSync(dataBlob, keyBytes, ivParams);
|
||||||
|
return StrUtil.unit8ArrayToStr(plain.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { USER_INFO, USER_PROFILE, LOGIN_PROFILE, TOKEN_LOGIN, TOKEN } from "../constants/AppConstants";
|
||||||
|
import { KVStore } from "../utils/KVStore";
|
||||||
|
import { router } from "@kit.ArkUI";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注意:涉及Router的只有启动页(SplashScreenPage.ets)和登录页(LoginPage.ets),其他页面请使用Navigator进行跳转
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function routerLogin(fromSource: string, isClear: boolean) {
|
||||||
|
if (isClear) {
|
||||||
|
KVStore.getInstance().put(USER_INFO, '');
|
||||||
|
KVStore.getInstance().put(USER_PROFILE, '')
|
||||||
|
KVStore.getInstance().put(LOGIN_PROFILE, ''); //持久化登录信息
|
||||||
|
KVStore.getInstance().put(TOKEN_LOGIN, ''); //持久化token
|
||||||
|
KVStore.getInstance().put(TOKEN, ''); //持久化token
|
||||||
|
}
|
||||||
|
|
||||||
|
//回到登录页(登录页将会清理router栈和NavPathStack栈)
|
||||||
|
router.pushUrl({
|
||||||
|
url: 'pages/LoginPage',
|
||||||
|
params: {
|
||||||
|
from: fromSource
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export function routerIndex() {
|
||||||
|
router.replaceUrl({ url: 'pages/Index' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { bundleManager, common, Want } from '@kit.AbilityKit';
|
||||||
|
import { PermissionUtil, StrUtil } from '@pura/harmony-utils';
|
||||||
|
import { identifier } from '@kit.AdsKit';
|
||||||
|
import { ToastUtils } from '../dialog/ToastUtils';
|
||||||
|
|
||||||
|
export async function openAppStore(context: common.UIAbilityContext) {
|
||||||
|
const bundleName = bundleManager.getBundleInfoForSelfSync(
|
||||||
|
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
|
||||||
|
).name;
|
||||||
|
try {
|
||||||
|
// 1. 尝试直接拉起应用市场客户端
|
||||||
|
const marketWant: Want = {
|
||||||
|
action: 'ohos.want.action.appdetail',
|
||||||
|
uri: `store://appgallery.huawei.com/app/detail?id=${bundleName}` // 替换为实际包名
|
||||||
|
};
|
||||||
|
await context.startAbility(marketWant);
|
||||||
|
} catch (err) {
|
||||||
|
// 2. 彻底失败,提示手动更新
|
||||||
|
ToastUtils.normalToast({message:'无法跳转,请手动打开应用市场搜索更新'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOAID(): Promise<string>{
|
||||||
|
try {
|
||||||
|
const isGranted = await PermissionUtil.checkRequestPermissions('ohos.permission.APP_TRACKING_CONSENT');
|
||||||
|
if(isGranted){
|
||||||
|
const oaid = await identifier.getOAID();
|
||||||
|
if(StrUtil.isNotEmpty(oaid)) {
|
||||||
|
return Promise.resolve(oaid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return Promise.resolve('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAppVersion() {
|
||||||
|
try {
|
||||||
|
// 获取应用自身的 BundleInfo
|
||||||
|
// BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION 表示获取应用的基本信息
|
||||||
|
let bundleInfo = await bundleManager.getBundleInfoForSelf(
|
||||||
|
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
|
||||||
|
);
|
||||||
|
|
||||||
|
let versionName = bundleInfo.versionName; // 对应 app.json5 中的 "versionName" (如 1.0.0)
|
||||||
|
let versionCode = bundleInfo.versionCode; // 对应 app.json5 中的 "versionCode" (如 1000001)
|
||||||
|
|
||||||
|
console.info(`版本名称: ${versionName}, 版本代码: ${versionCode}`);
|
||||||
|
return versionName;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取版本信息失败: ' + JSON.stringify(err));
|
||||||
|
return '未知版本';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { mediaquery } from '@kit.ArkUI';
|
||||||
|
import { commonConstants } from '../constants/CommonConstants';
|
||||||
|
|
||||||
|
declare interface BreakPointTypeOption<T> extends Record<string, T | undefined> {
|
||||||
|
sm?: T;
|
||||||
|
md?: T;
|
||||||
|
lg?: T;
|
||||||
|
xl?: T;
|
||||||
|
xxl?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BreakPointType<T> {
|
||||||
|
options: BreakPointTypeOption<T>;
|
||||||
|
|
||||||
|
constructor(option: BreakPointTypeOption<T>) {
|
||||||
|
this.options = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(currentBreakPoint: string): T {
|
||||||
|
return this.options[currentBreakPoint] as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Breakpoint {
|
||||||
|
name: string = '';
|
||||||
|
size: number = 0;
|
||||||
|
mediaQueryListener?: mediaquery.MediaQueryListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BreakpointSystem {
|
||||||
|
private currentBreakpoint: string = commonConstants.breakpointsInitializeName;
|
||||||
|
private readonly breakpointId: string = commonConstants.breakpointIdInitializeName;
|
||||||
|
private readonly breakpoints: Breakpoint[] = [
|
||||||
|
{ name: commonConstants.breakpointsSmName, size: commonConstants.breakpointsSmSize },
|
||||||
|
{ name: commonConstants.breakpointsMdName, size: commonConstants.breakpointsMdSize },
|
||||||
|
{ name: commonConstants.breakpointsLgName, size: commonConstants.breakpointsLgSize },
|
||||||
|
{ name: commonConstants.breakpointsXlName, size: commonConstants.breakpointsXlSize }
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(breakpointId: string) {
|
||||||
|
this.breakpointId = breakpointId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public register(uiContext: UIContext): void {
|
||||||
|
this.breakpoints.forEach((breakpoint: Breakpoint, index: number) => {
|
||||||
|
let condition: string = '';
|
||||||
|
if (index === this.breakpoints.length - 1) {
|
||||||
|
condition = `(${breakpoint.size}vp<=width)`;
|
||||||
|
} else {
|
||||||
|
condition = `(${breakpoint.size}vp<=width<${this.breakpoints[index + 1].size}vp)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
breakpoint.mediaQueryListener = uiContext.getMediaQuery().matchMediaSync(condition);
|
||||||
|
breakpoint.mediaQueryListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
|
||||||
|
if (mediaQueryResult.matches) {
|
||||||
|
this.updateCurrentBreakpoint(breakpoint.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public unregister(): void {
|
||||||
|
this.breakpoints.forEach((breakpoint: Breakpoint) => {
|
||||||
|
breakpoint.mediaQueryListener?.off('change');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCurrentBreakpoint(breakpoint: string): void {
|
||||||
|
if (this.currentBreakpoint !== breakpoint) {
|
||||||
|
this.currentBreakpoint = breakpoint;
|
||||||
|
AppStorage.set<string>(this.breakpointId, this.currentBreakpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { request } from '@kit.BasicServicesKit';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
import { fileIo as fs } from '@kit.CoreFileKit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件下载
|
||||||
|
* @param callback
|
||||||
|
* status: 0-默认, 1-进行中(progress为百分比), 2-成功(progress为0, path为路径), -1-失败
|
||||||
|
*/
|
||||||
|
export async function downloadFile(
|
||||||
|
context: Context,
|
||||||
|
url: string,
|
||||||
|
fileName: string,
|
||||||
|
callback: (status: number, progress: number, path?: string) => void
|
||||||
|
) {
|
||||||
|
const filePath = `${context.filesDir}/${fileName}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// --- 清理旧文件逻辑 ---
|
||||||
|
if (fs.accessSync(filePath)) {
|
||||||
|
console.info(`检测到旧文件,正在删除: ${filePath}`);
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`删除旧文件失败: ${(e as BusinessError).message}`);
|
||||||
|
// 即使删除失败(如文件被占用),通常也可以继续,因为下载配置了 overwrite: true
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: request.agent.Config = {
|
||||||
|
action: request.agent.Action.DOWNLOAD,
|
||||||
|
url: url,
|
||||||
|
saveas: filePath,
|
||||||
|
mode: request.agent.Mode.FOREGROUND,
|
||||||
|
overwrite: true // 即使删除逻辑失败,此参数也会确保覆盖
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const task = await request.agent.create(context, config);
|
||||||
|
|
||||||
|
task.on('progress', (progress) => {
|
||||||
|
if (progress.sizes && progress.sizes.length > 0 && progress.sizes[0] > 0) {
|
||||||
|
const percent = Math.floor((progress.processed / progress.sizes[0]) * 100);
|
||||||
|
callback(1, percent);
|
||||||
|
} else {
|
||||||
|
callback(1, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
task.on('completed', () => {
|
||||||
|
callback(2, 100, filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
task.on('failed', (info) => {
|
||||||
|
console.error(`任务失败详情: ${JSON.stringify(info)}`);
|
||||||
|
callback(-1, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await task.start();
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as BusinessError;
|
||||||
|
console.error(`创建任务失败: ${err.code} ${err.message}`);
|
||||||
|
callback(-1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,410 @@
|
||||||
|
import { image } from '@kit.ImageKit';
|
||||||
|
import { fileIo as fs } from '@kit.CoreFileKit';
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { BusinessError } from '@ohos.base';
|
||||||
|
import { util } from '@kit.ArkTS';
|
||||||
|
import { PickerUtil } from '@pura/picker_utils';
|
||||||
|
import componentSnapshot from '@ohos.arkui.componentSnapshot';
|
||||||
|
import { promptAction } from '@kit.ArkUI';
|
||||||
|
import { photoAccessHelper } from '@kit.MediaLibraryKit';
|
||||||
|
import { subjectSegmentation } from '@kit.CoreVisionKit';
|
||||||
|
import { StickerItem } from '../viewmodel/LocalBean';
|
||||||
|
import buffer from '@ohos.buffer';
|
||||||
|
import fileIo from '@ohos.file.fs';
|
||||||
|
import { ToastUtils } from '../dialog/ToastUtils';
|
||||||
|
|
||||||
|
|
||||||
|
export class ImageUtils {
|
||||||
|
private static readonly TAG: string = 'ImageUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩图片并保存到沙箱缓存目录
|
||||||
|
* @param context 上下文
|
||||||
|
* @param uri 原始图片URI (如 datashare:// 或 file://)
|
||||||
|
* @param quality 压缩质量 (0-100),默认 80
|
||||||
|
* @returns 压缩后的沙箱文件路径
|
||||||
|
*/
|
||||||
|
static async compressImage(context: common.Context, uri: string, quality: number = 80): Promise<string> {
|
||||||
|
let file: fs.File | undefined;
|
||||||
|
try {
|
||||||
|
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||||||
|
const packingOptions: image.PackingOption = {
|
||||||
|
format: 'image/jpeg',
|
||||||
|
quality: quality
|
||||||
|
};
|
||||||
|
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||||||
|
const arrayBuffer: ArrayBuffer = await imagePacker.packing(imageSource, packingOptions);
|
||||||
|
const fileName = `compress_${Date.now()}.jpg`;
|
||||||
|
const cachePath = `${context.cacheDir}/${fileName}`;
|
||||||
|
const cacheFile = fs.openSync(cachePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||||||
|
fs.writeSync(cacheFile.fd, arrayBuffer);
|
||||||
|
fs.closeSync(cacheFile);
|
||||||
|
|
||||||
|
await imagePacker.release();
|
||||||
|
await imageSource.release();
|
||||||
|
|
||||||
|
console.info(ImageUtils.TAG, `图片压缩成功: ${cachePath}`);
|
||||||
|
return `file://${cachePath}`; // 返回符合上传要求的 file 协议路径
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as BusinessError;
|
||||||
|
console.error(ImageUtils.TAG, `压缩失败: ${err.code} ${err.message}`);
|
||||||
|
return uri; // 如果压缩失败,返回原路径尝试直接上传
|
||||||
|
} finally {
|
||||||
|
if (file) {
|
||||||
|
fs.closeSync(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片 URI 转换为 Base64 字符串
|
||||||
|
* @param uri 相册选择的 datashare:// 或沙箱路径
|
||||||
|
*/
|
||||||
|
static async base64Image(uri: string): Promise<string> {
|
||||||
|
const context = getContext();
|
||||||
|
// 使用临时路径存储拷贝的文件
|
||||||
|
const tempFileName = `${Date.now()}.jpg`;
|
||||||
|
const destPath = `${context.cacheDir}/${tempFileName}`;
|
||||||
|
|
||||||
|
let srcFile: fs.File | null = null;
|
||||||
|
try {
|
||||||
|
srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
fs.copyFileSync(srcFile.fd, destPath);
|
||||||
|
const base64Data = await ImageUtils.fileToBase64(destPath);
|
||||||
|
fs.unlinkSync(destPath);
|
||||||
|
|
||||||
|
return base64Data;
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as BusinessError;
|
||||||
|
console.error(`base64Image 转换失败: ${error.message}`);
|
||||||
|
return "";
|
||||||
|
} finally {
|
||||||
|
if (srcFile) {
|
||||||
|
fs.closeSync(srcFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async pixelMapToBase64(pixelMap: image.PixelMap): Promise<string> {
|
||||||
|
const imagePackerApi = image.createImagePacker();
|
||||||
|
const packOptions: image.PackingOption = { format: 'image/png', quality: 100 };
|
||||||
|
const data = await imagePackerApi.packing(pixelMap, packOptions);
|
||||||
|
|
||||||
|
let base64Str = buffer.from(data).toString('base64');
|
||||||
|
return `data:image/png;base64,${base64Str}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部方法:读取文件转 Base64
|
||||||
|
*/
|
||||||
|
private static async fileToBase64(filePath: string): Promise<string> {
|
||||||
|
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
|
||||||
|
const stat = fs.statSync(file.fd);
|
||||||
|
const buffer = new ArrayBuffer(stat.size);
|
||||||
|
fs.readSync(file.fd, buffer);
|
||||||
|
fs.closeSync(file);
|
||||||
|
|
||||||
|
const helper = new util.Base64Helper();
|
||||||
|
return helper.encodeToStringSync(new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存图片到指定路径(用户自己选择)
|
||||||
|
* @param pixelMap
|
||||||
|
*/
|
||||||
|
static async savePixelMapToGallery(pixelMap: image.PixelMap) {
|
||||||
|
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||||||
|
const packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.jpg`]);
|
||||||
|
|
||||||
|
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||||||
|
|
||||||
|
if (!targetUri) {
|
||||||
|
console.warn('用户取消保存或路径获取失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packOpts) as ArrayBuffer;
|
||||||
|
const file = fs.openSync(targetUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||||||
|
fs.writeSync(file.fd, buffer);
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
|
||||||
|
console.info('保存成功!路径: ' + targetUri);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('保存过程中出错: ' + JSON.stringify(err));
|
||||||
|
} finally {
|
||||||
|
imagePacker.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片(image.PixelMap)转成jpg、png、webp等
|
||||||
|
* 保存图片到指定路径(用户自己选择)
|
||||||
|
* @param pixelMap
|
||||||
|
* @param format 支持: 'image/jpeg', 'image/png', 'image/webp'
|
||||||
|
*/
|
||||||
|
static async saveFormatToGallery(pixelMap: image.PixelMap, format: string) {
|
||||||
|
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||||||
|
const packOpts: image.PackingOption = { format: format, quality: 100 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.${format === 'image/jpeg' ? 'jpg' : format.substring(6)}`]);
|
||||||
|
|
||||||
|
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||||||
|
|
||||||
|
if (!targetUri) {
|
||||||
|
console.warn('用户取消保存或路径获取失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packOpts) as ArrayBuffer;
|
||||||
|
const file = fs.openSync(targetUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||||||
|
fs.writeSync(file.fd, buffer);
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
|
||||||
|
|
||||||
|
ToastUtils.normalToast({ message: '图片已保存' });
|
||||||
|
} catch (err) {
|
||||||
|
ToastUtils.normalToast({ message: '保存失败' });
|
||||||
|
} finally {
|
||||||
|
imagePacker.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片(image.PixelMap)转成SVG
|
||||||
|
* @param pixelMap
|
||||||
|
*/
|
||||||
|
static async saveSvgToGallery(pixelMap: image.PixelMap) {
|
||||||
|
let imagePackerApi = image.createImagePacker();
|
||||||
|
const info = await pixelMap.getImageInfo();
|
||||||
|
const width = info.size.width;
|
||||||
|
const height = info.size.height;
|
||||||
|
|
||||||
|
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.svg`]);
|
||||||
|
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||||||
|
if (!targetUri) {
|
||||||
|
console.warn('用户取消保存或路径获取失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packOptions: image.PackingOption = {
|
||||||
|
format: 'image/png',
|
||||||
|
quality: 100
|
||||||
|
};
|
||||||
|
const arrayBuffer = await imagePackerApi.packing(pixelMap, packOptions);
|
||||||
|
|
||||||
|
// 将二进制数据转为 Base64 字符串,buffer.from().toString('base64') 默认不换行的 (等同于 NO_WRAP)
|
||||||
|
const base64Data = buffer.from(arrayBuffer).toString('base64');
|
||||||
|
|
||||||
|
// 构建 SVG 内容
|
||||||
|
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<image width="${width}" height="${height}"
|
||||||
|
xlink:href="data:image/png;base64,${base64Data}"
|
||||||
|
href="data:image/png;base64,${base64Data}" />
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
let file = fileIo.openSync(targetUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
|
||||||
|
fileIo.writeSync(file.fd, svgContent);
|
||||||
|
fileIo.closeSync(file);
|
||||||
|
ToastUtils.normalToast({ message: '图片已保存' });
|
||||||
|
} catch (err) {
|
||||||
|
ToastUtils.normalToast({ message: '保存失败' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存图片(通过布局ID保存图片)
|
||||||
|
* 注意:id为控件的ID
|
||||||
|
*/
|
||||||
|
static async saveImageForID(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
let pixelMap = await componentSnapshot.get(id);
|
||||||
|
|
||||||
|
if (pixelMap) {
|
||||||
|
await ImageUtils.savePixelMapToGallery(pixelMap);
|
||||||
|
ToastUtils.normalToast({ message: '图片已保存' });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`快照捕获失败: ${JSON.stringify(err)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开图库(仅可选单张图片)
|
||||||
|
*/
|
||||||
|
static async openGallery(): Promise<StickerItem|null> {
|
||||||
|
let file: fs.File | null = null;
|
||||||
|
try {
|
||||||
|
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||||||
|
const options = new photoAccessHelper.PhotoSelectOptions();
|
||||||
|
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||||
|
options.maxSelectNumber = 1;
|
||||||
|
|
||||||
|
const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options);
|
||||||
|
if (result.photoUris.length === 0) return null;
|
||||||
|
|
||||||
|
const uri: string = result.photoUris[0];
|
||||||
|
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||||||
|
const pixelMap: image.PixelMap = await imageSource.createPixelMap();
|
||||||
|
let newItem = new StickerItem(pixelMap,0);
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
} catch (err) {
|
||||||
|
ToastUtils.normalToast({ message: '获取图片失败!' });
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (file) fs.closeSync(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开图库(仅可选单张图片-抠图)
|
||||||
|
*/
|
||||||
|
static async openCutoutGallery(): Promise<StickerItem|null> {
|
||||||
|
let file: fs.File | null = null;
|
||||||
|
try {
|
||||||
|
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||||||
|
const options = new photoAccessHelper.PhotoSelectOptions();
|
||||||
|
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||||
|
options.maxSelectNumber = 1;
|
||||||
|
|
||||||
|
const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options);
|
||||||
|
if (result.photoUris.length === 0) return null;
|
||||||
|
|
||||||
|
const uri: string = result.photoUris[0];
|
||||||
|
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||||||
|
const pixelMap: image.PixelMap = await imageSource.createPixelMap();
|
||||||
|
|
||||||
|
ToastUtils.normalToast({ message: '抠图中...' });
|
||||||
|
|
||||||
|
await subjectSegmentation.init();
|
||||||
|
const visionInfo: subjectSegmentation.VisionInfo = { pixelMap: pixelMap };
|
||||||
|
const config: subjectSegmentation.SegmentationConfig = {
|
||||||
|
enableSubjectForegroundImage: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const segResult: subjectSegmentation.SegmentationResult =
|
||||||
|
await subjectSegmentation.doSegmentation(visionInfo, config);
|
||||||
|
if (segResult && segResult.fullSubject && segResult.fullSubject.foregroundImage) {
|
||||||
|
const pixelMap = segResult.fullSubject.foregroundImage;
|
||||||
|
let newItem = new StickerItem(pixelMap,0);
|
||||||
|
|
||||||
|
await subjectSegmentation.release();
|
||||||
|
return newItem;
|
||||||
|
}else{
|
||||||
|
await subjectSegmentation.release();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ToastUtils.normalToast({ message: '抠图失败,请使用规范图片' });
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (file) fs.closeSync(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开图库(可选择多张图片)
|
||||||
|
*/
|
||||||
|
static async openMultipleGallery(): Promise<image.PixelMap[]> {
|
||||||
|
const pixelMaps: image.PixelMap[] = [];
|
||||||
|
try {
|
||||||
|
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
|
||||||
|
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||||
|
photoSelectOptions.maxSelectNumber = 10;
|
||||||
|
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||||||
|
const photoSelectResult = await photoPicker.select(photoSelectOptions);
|
||||||
|
|
||||||
|
if (!photoSelectResult || photoSelectResult.photoUris.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
for (const uri of photoSelectResult.photoUris) {
|
||||||
|
let file: fs.File | null = null;
|
||||||
|
try {
|
||||||
|
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||||||
|
const decodingOptions: image.DecodingOptions = {
|
||||||
|
editable: true,
|
||||||
|
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
|
||||||
|
};
|
||||||
|
const pm = await imageSource.createPixelMap(decodingOptions);
|
||||||
|
if (pm) {
|
||||||
|
pixelMaps.push(pm);
|
||||||
|
}
|
||||||
|
imageSource.release();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`解析图片失败: ${uri}, 错误: ${JSON.stringify(e)}`);
|
||||||
|
} finally {
|
||||||
|
if (file) {
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pixelMaps;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('选择图片过程发生错误: ' + JSON.stringify(err));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Resource 对应的 PixelMap
|
||||||
|
* @param res Resource 对象
|
||||||
|
* @returns PixelMap 对象
|
||||||
|
*/
|
||||||
|
static async getPixelMapFromResource(res: Resource, context: common.UIAbilityContext): Promise<image.PixelMap> {
|
||||||
|
const resourceMgr = context.resourceManager;
|
||||||
|
const fileData = await resourceMgr.getMediaContent(res);
|
||||||
|
const imageSource = image.createImageSource(fileData.buffer as ArrayBuffer);
|
||||||
|
return await imageSource.createPixelMap({
|
||||||
|
editable: true,
|
||||||
|
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 PixelMap 转换为 ImageSource
|
||||||
|
* @param pixelMap 输入的位图对象
|
||||||
|
* @returns 转换后的 ImageSource 对象
|
||||||
|
*/
|
||||||
|
static async pixelMapToImageSource(pixelMap: image.PixelMap): Promise<image.ImageSource|undefined> {
|
||||||
|
const imagePacker = image.createImagePacker();
|
||||||
|
|
||||||
|
const packingOptions: image.PackingOption = {
|
||||||
|
format: "image/jpeg",
|
||||||
|
quality: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packingOptions);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(buffer);
|
||||||
|
|
||||||
|
return imageSource;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('转换失败: ' + JSON.stringify(err));
|
||||||
|
return undefined;
|
||||||
|
} finally {
|
||||||
|
imagePacker.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { isExistsUniMP } from '@dcloudio/uni-app-runtime';
|
||||||
|
import { UNIMP_DATA_VERSION, WX_AUTH_CODE } from '../constants/AppConstants'
|
||||||
|
import { UniMpLocalVersion } from '../viewmodel/LocalBean';
|
||||||
|
import { KVStore } from './KVStore'
|
||||||
|
import { StringUtils } from './StringUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断小程序是否需要下载(跳转指定路径)
|
||||||
|
* @param uniId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function isUniMpNeedDownloadForPath(uniId: string): Promise<boolean>{
|
||||||
|
let uniMpVersions = await getUniMpNeedDownload();
|
||||||
|
let uniMpVersion = uniMpVersions.find(item => {return item.id === uniId;});
|
||||||
|
|
||||||
|
let isExists = false;
|
||||||
|
if(uniMpVersion !== undefined && isExistsUniMP(uniId)){
|
||||||
|
isExists = true
|
||||||
|
} else {
|
||||||
|
isExists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断小程序是否需要下载(默认,目标版本判断)
|
||||||
|
* @param uniId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function isUniMpNeedDownload(uniId: string, targetVersion: string|undefined): Promise<boolean> {
|
||||||
|
if(!targetVersion){
|
||||||
|
true
|
||||||
|
}
|
||||||
|
let uniMpVersions = await getUniMpNeedDownload();
|
||||||
|
let uniMpVersion = uniMpVersions.find(item => {return item.id === uniId});
|
||||||
|
if(!StringUtils.checkVersionLegal(uniMpVersion?.version??'0.0.0', targetVersion??'0.0.0') || !StringUtils.isNewVersionAvailable(uniMpVersion?.version??'0.0.0', targetVersion??'0.0.0')){
|
||||||
|
return false;
|
||||||
|
}else{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUniMpNeedDownload(): Promise<UniMpLocalVersion[]> {
|
||||||
|
// 注意:JSON 内部必须使用双引号 "",单引号会引发解析错误
|
||||||
|
const defaultJson = '[]';//'[{"id":"","type":"","version":"1.0.0","path":""},{"id":"","type":"","version":"1.0.0","path":""}]';
|
||||||
|
|
||||||
|
let storeJson = await KVStore.getInstance()
|
||||||
|
.get(UNIMP_DATA_VERSION, defaultJson);
|
||||||
|
let uniMpVersions = JSON.parse(storeJson) as UniMpLocalVersion[];
|
||||||
|
return uniMpVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setUniMpNeedDownload(id: string, type: string, version: string, path: string): Promise<void> {
|
||||||
|
let uniMpVersions = await getUniMpNeedDownload();
|
||||||
|
let uniMpVersion = uniMpVersions.find(item => {return item.id === id});
|
||||||
|
if(uniMpVersion){
|
||||||
|
uniMpVersion.type = type;
|
||||||
|
uniMpVersion.version = version;
|
||||||
|
uniMpVersion.path = path;
|
||||||
|
}else{
|
||||||
|
uniMpVersions.push({
|
||||||
|
id: id,
|
||||||
|
type: type,
|
||||||
|
version: version,
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
}
|
||||||
|
KVStore.getInstance().put(UNIMP_DATA_VERSION, JSON.stringify(uniMpVersions))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setWechatAuthCode(wechatCode: string): void {
|
||||||
|
KVStore.getInstance().put(WX_AUTH_CODE, wechatCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWechatAuthCode(): Promise<string> {
|
||||||
|
return await KVStore.getInstance().get(WX_AUTH_CODE, '')
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { preferences } from '@kit.ArkData';
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { Logger } from './Logger';
|
||||||
|
import emitter from '@ohos.events.emitter';
|
||||||
|
import { EVT_KV_INIT_DONE } from '../constants/EventConstants';
|
||||||
|
import { IS_KV_STORE_INIT_FINISHED } from '../constants/AppConstants';
|
||||||
|
|
||||||
|
export class KVStore {
|
||||||
|
private static instance: KVStore | null = null;
|
||||||
|
private pref: preferences.Preferences | null = null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
// 初始化(建议在 UIAbility 的 onCreate 中调用一次)
|
||||||
|
static async init(context: common.Context): Promise<void> {
|
||||||
|
if (!KVStore.instance) {
|
||||||
|
const store = new KVStore();
|
||||||
|
// 获取 Preferences 实例必须异步等待
|
||||||
|
store.pref = await preferences.getPreferences(context, 'cn_img_rabbit_kv_store');
|
||||||
|
KVStore.instance = store;
|
||||||
|
|
||||||
|
emitter.emit({ eventId: EVT_KV_INIT_DONE });
|
||||||
|
AppStorage.setOrCreate<boolean>(IS_KV_STORE_INIT_FINISHED, true);
|
||||||
|
Logger.info('Rabbit_KVStore', 'KVStore initialized finish.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): KVStore {
|
||||||
|
if (!KVStore.instance) {
|
||||||
|
// 抛出错误比返回 undefined 更好,能让你一眼看到是不是顺序错了
|
||||||
|
throw new Error("Rabbit_KVStore is not initialized. Please call await KVStore.init() in Ability onCreate.");
|
||||||
|
}
|
||||||
|
return KVStore.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储数据 (模拟 MMKV put)
|
||||||
|
async put(key: string, value: preferences.ValueType): Promise<void> {
|
||||||
|
if (!this.pref) {
|
||||||
|
// 如果 pref 为空,尝试重新补救(或者抛出错误方便排查)
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: pref is null when putting key: ${key}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.pref.put(key, value);
|
||||||
|
await this.pref.flush(); // 持久化到磁盘
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: Successfully saved ${key} ---- ${value}`);
|
||||||
|
} catch (err) {
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: Save failed, error: ${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 读取数据 (模拟 MMKV get)
|
||||||
|
async get<T extends preferences.ValueType>(key: string, defaultValue: T): Promise<T> {
|
||||||
|
if (this.pref) {
|
||||||
|
const val = await this.pref.get(key, defaultValue) as T;
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: Fetching ${key}, value is: ${val}`);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: pref is null, returning default for ${key}`);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步读取数据 (对应 Android MMKV.decodeString)
|
||||||
|
* 必须确保 pref 已经初始化完成
|
||||||
|
*/
|
||||||
|
getSync<T extends preferences.ValueType>(key: string, defaultValue: T): T {
|
||||||
|
if (this.pref !== null) {
|
||||||
|
// 使用系统提供的 getSync 接口
|
||||||
|
return this.pref.getSync(key, defaultValue) as T;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除数据
|
||||||
|
async delete(key: string): Promise<void> {
|
||||||
|
if (this.pref) {
|
||||||
|
await this.pref.delete(key);
|
||||||
|
await this.pref.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* Logger工具类,提供统一的日志记录功能
|
||||||
|
*/
|
||||||
|
export class Logger {
|
||||||
|
private static readonly TAG: string = 'AppLogger'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录方法调用日志
|
||||||
|
* @param className 类名
|
||||||
|
* @param methodName 方法名
|
||||||
|
*/
|
||||||
|
static logMethodCall(className: string, methodName: string): void {
|
||||||
|
console.info(`[${Logger.TAG}] ${className}.${methodName}() called`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录信息级别日志
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param message 日志消息
|
||||||
|
*/
|
||||||
|
static info(tag: string, message: string): void {
|
||||||
|
console.info(`[${Logger.TAG}] [${tag}] INFO: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录错误级别日志
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param error 错误对象或消息
|
||||||
|
* @param message 错误描述
|
||||||
|
*/
|
||||||
|
static logError(tag: string, error: object, message: string): void {
|
||||||
|
console.error(`[${Logger.TAG}] [${tag}] ERROR: ${message}`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录错误级别日志(兼容性方法)
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param message 日志消息
|
||||||
|
*/
|
||||||
|
static error(tag: string, message: string): void {
|
||||||
|
console.error(`[${Logger.TAG}] [${tag}] ERROR: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录调试级别日志
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param message 日志消息
|
||||||
|
*/
|
||||||
|
static debug(tag: string, message: string): void {
|
||||||
|
console.debug(`[${Logger.TAG}] [${tag}] DEBUG: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录警告级别日志
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param message 日志消息
|
||||||
|
*/
|
||||||
|
static warn(tag: string, message: string): void {
|
||||||
|
console.warn(`[${Logger.TAG}] [${tag}] WARN: ${message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { pasteboard } from '@kit.BasicServicesKit';
|
||||||
|
import { BusinessError } from '@ohos.base';
|
||||||
|
import { promptAction } from '@kit.ArkUI';
|
||||||
|
import { Logger } from './Logger';
|
||||||
|
import { ToastUtils } from '../dialog/ToastUtils';
|
||||||
|
|
||||||
|
export class StringUtils {
|
||||||
|
/**
|
||||||
|
* 复制纯文本到剪切板
|
||||||
|
* @param text 待复制的字符串
|
||||||
|
* @param showToast 是否显示成功提示,默认为 true
|
||||||
|
*/
|
||||||
|
static copyText(text: string, showToast: boolean = true): void {
|
||||||
|
if (!text) {
|
||||||
|
console.warn('ClipboardUtil: Text is empty, skip copying.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 创建剪贴板数据对象
|
||||||
|
const pasteData: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
|
||||||
|
// 2. 获取系统剪贴板
|
||||||
|
const systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();
|
||||||
|
|
||||||
|
// 3. 写入数据 (显式指定 BusinessError 类型消除 any 报错)
|
||||||
|
systemPasteboard.setData(pasteData, (err: BusinessError) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`ClipboardUtil: Failed to set data. Code: ${err.code}, message: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (showToast) {
|
||||||
|
ToastUtils.normalToast({message:'已复制到剪切板'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 4. 类型断言消除 catch 中的 any/unknown 报错
|
||||||
|
const err: BusinessError = error as BusinessError;
|
||||||
|
console.error(`ClipboardUtil: Unexpected error. Code: ${err.code}, message: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查版本是否符合要求(要求规定三段式:0.0.0)
|
||||||
|
* @param v1 基准版本(本地安装的版本号)
|
||||||
|
* @param v2 目标版本(服务器要求更新的版本号)
|
||||||
|
* @returns true 可用于比较版本号,不可(且无法)用于比较版本号
|
||||||
|
*/
|
||||||
|
static checkVersionLegal(v1: string, v2: string): boolean {
|
||||||
|
const v1Parts: string[] = v1.split('.');
|
||||||
|
const v2Parts: string[] = v2.split('.');
|
||||||
|
if(v1Parts.length === v2Parts.length){
|
||||||
|
return true
|
||||||
|
}else{
|
||||||
|
if(v1Parts.length < 3 || v1Parts.length > 3){
|
||||||
|
Logger.info('isNewVersionAvailable', '基准版本:'+v1 +',版本长度不合规,应该(三段式)为:0.0.0')
|
||||||
|
}
|
||||||
|
if(v2Parts.length < 3 || v2Parts.length > 3){
|
||||||
|
Logger.info('isNewVersionAvailable', '目标版本:'+v2 + ',版本长度不合规,应该(三段式)为:0.0.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较版本号
|
||||||
|
* @param v1 基准版本 (如 '1.1.0')
|
||||||
|
* @param v2 目标版本 (如 '1.0.1')
|
||||||
|
* @returns 如果 v2 < v1 返回 true,否则返回 false
|
||||||
|
*/
|
||||||
|
static isNewVersionAvailable(v1: string, v2: string): boolean {
|
||||||
|
Logger.info('isNewVersionAvailable', v1 +'----------'+v2)
|
||||||
|
// 1. 按点拆分数组
|
||||||
|
const v1Parts: string[] = v1.split('.');
|
||||||
|
const v2Parts: string[] = v2.split('.');
|
||||||
|
// 2. 长度不相等,无法比较
|
||||||
|
if(v1Parts.length!=v2Parts.length){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 确定最大比较长度
|
||||||
|
const maxLength: number = Math.max(v1Parts.length, v2Parts.length);
|
||||||
|
|
||||||
|
// 4. 版本比较
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
// 使用 Number() 或 parseInt,并处理 NaN 的情况
|
||||||
|
const n1: number = i < v1Parts.length ? (parseInt(v1Parts[i], 10) || 0) : 0;
|
||||||
|
const n2: number = i < v2Parts.length ? (parseInt(v2Parts[i], 10) || 0) : 0;
|
||||||
|
|
||||||
|
Logger.info('isNewVersionAvailable', n1 +'*************'+n2)
|
||||||
|
|
||||||
|
if (n1 < n2) return true;
|
||||||
|
if (n1 > n2) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 循环结束仍相等
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { openUniMP, releaseWgtToRunPath, UniMP } from '@dcloudio/uni-app-runtime'
|
||||||
|
import { AESDecrypt, PLATFORM, RELEASE_BASE_URL, Signature, TOKEN } from '../constants/AppConstants'
|
||||||
|
import { ToastUtils } from '../dialog/ToastUtils'
|
||||||
|
import { UniVersionEntity,TradeData, UniIconEntity } from '../viewmodel/DataBean'
|
||||||
|
import { downloadFile } from './DownloaderUtils'
|
||||||
|
import { isUniMpNeedDownload, isUniMpNeedDownloadForPath } from './KVManager'
|
||||||
|
import { KVStore } from './KVStore'
|
||||||
|
import { Logger } from './Logger'
|
||||||
|
import { bundleManager, common } from '@kit.AbilityKit'
|
||||||
|
import { DeviceUtil } from '@pura/harmony-utils'
|
||||||
|
import { deviceInfo } from '@kit.BasicServicesKit'
|
||||||
|
import { CHANNEL } from '../../../../BuildProfile'
|
||||||
|
import { CommonService } from '../viewmodel/CommonService'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载并启动模拟器
|
||||||
|
* @param context
|
||||||
|
* @param entity
|
||||||
|
* 注意:status: -1-失败, 0-默认, 1-进行中(progress为百分比), 2-成功(progress为0, path为路径), 3-启动, 4-释放成功, 5-释放失败
|
||||||
|
*/
|
||||||
|
export function downStartUp(context: UIContext, entity: UniVersionEntity, callback: (status: number, progress: number, path?: string) => void) {
|
||||||
|
let uniMpId = entity.unimp_id;//'__UNI__D535736';
|
||||||
|
let uniMpType = entity.unimp_type;
|
||||||
|
let uniMpUrl = entity.url;//'https://cdn.batiao8.com/flaunt/uni_mp/wgt/alipay/__UNI__D535736_2026-4-20-test4.wgt';
|
||||||
|
let uniVersion = entity.version??'0.0.0';
|
||||||
|
if(uniMpId){
|
||||||
|
isUniMpNeedDownload(uniMpId, uniVersion).then(res => {
|
||||||
|
if(res == true){
|
||||||
|
callback(1, 0)
|
||||||
|
//需要更新,则删除旧文件,重新下载wgt文件
|
||||||
|
if(!uniMpId){
|
||||||
|
ToastUtils.showToast(context, '加载失败,请重试或联系客服!')
|
||||||
|
|
||||||
|
callback(-1, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//下载模拟器
|
||||||
|
downloadFile(getContext(), uniMpUrl??'', uniMpId + '.wgt', (status, progress, path) => {
|
||||||
|
if(status == 2){
|
||||||
|
callback(status, progress, path)
|
||||||
|
//已下载完成,释放并启动
|
||||||
|
releaseRunUniMp(context, uniMpId??'', '', (status) => {
|
||||||
|
callback(status, progress, path)
|
||||||
|
})
|
||||||
|
}else if(status == -1){
|
||||||
|
ToastUtils.showToast(context, '加载失败,请重试或联系客服!')
|
||||||
|
callback(status, progress, path)
|
||||||
|
}else{
|
||||||
|
callback(status, progress, path)
|
||||||
|
}
|
||||||
|
Logger.info('UniMp','downloadFile status:' + status + ', progress:' + progress)
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
//不需要更新,已释放则直接打开模拟器,否则先释放,再打开
|
||||||
|
runUniMp(context, uniMpId??'', '', (status) => {
|
||||||
|
callback(status, 100)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Logger.info('UniMp','uniMpId:'+uniMpId+ ', uniMpType:' + uniMpType + ', uniMpUrl:' + uniMpUrl + ', uniMpVersion:' + uniVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downStartUpForPath(context: UIContext, uniMpVersions: UniVersionEntity[], uniIconEntity: UniIconEntity, callback: (status: number, progress: number, path?: string) => void) {
|
||||||
|
const uniMpType = uniIconEntity.type
|
||||||
|
const uniMpEntity = uniMpVersions.find((item: UniVersionEntity) => {
|
||||||
|
return item.unimp_type === uniMpType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetPath = uniIconEntity.url??''
|
||||||
|
const uniMpUrl = uniMpEntity?.url??''//'https://cdn.batiao8.com/flaunt/uni_mp/wgt/alipay/__UNI__D535736_2026-4-20-test4.wgt';
|
||||||
|
const uniMpId = uniMpEntity?.unimp_id??''
|
||||||
|
isUniMpNeedDownloadForPath(uniMpId).then(res => {
|
||||||
|
if(res == true){
|
||||||
|
// 已释放,直接运行
|
||||||
|
runUniMp(context, uniMpId??'', '', (status) => {
|
||||||
|
callback(status, 100)
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
// 未下载,先下载释放,再运行
|
||||||
|
downloadFile(getContext(), uniMpUrl??'', uniMpId + '.wgt', (status, progress, path) => {
|
||||||
|
if(status == 2){
|
||||||
|
callback(status, progress, path)
|
||||||
|
//已下载完成,释放并启动
|
||||||
|
releaseRunUniMp(context, uniMpId??'', targetPath, (status) => {
|
||||||
|
callback(status, progress, path)
|
||||||
|
})
|
||||||
|
}else if(status == -1){
|
||||||
|
ToastUtils.showToast(context, '加载失败,请重试或联系客服!')
|
||||||
|
callback(status, progress, path)
|
||||||
|
}else{
|
||||||
|
callback(status, progress, path)
|
||||||
|
}
|
||||||
|
Logger.info('UniMp','downloadFile status:' + status + ', progress:' + progress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行模拟器
|
||||||
|
* 1、如已释放,直接运行
|
||||||
|
* 2、如未释放,先释放,再运行
|
||||||
|
* @param uniMpId 小程序ID
|
||||||
|
* @param targetPath 跳转小程序路径
|
||||||
|
* 注意:status: -1-失败, 0-默认, 1-进行中(progress为百分比), 2-成功(progress为0, path为路径), 3-启动, 4-释放成功, 5-释放失败
|
||||||
|
*/
|
||||||
|
export function runUniMp(context: UIContext, uniMpId: string, targetPath:string, callback: (status: number) => void){
|
||||||
|
releaseRunUniMp(context, uniMpId, targetPath, callback)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 释放并运行,需要先释放,再运行
|
||||||
|
* 1、删除旧文件
|
||||||
|
* 2、先释放,再运行
|
||||||
|
* @param uniMpId 小程序ID
|
||||||
|
* @param targetPath 跳转小程序路径
|
||||||
|
* 注意:status: -1-失败, 0-默认, 1-进行中(progress为百分比), 2-成功(progress为0, path为路径), 3-启动, 4-释放成功, 5-释放失败
|
||||||
|
*/
|
||||||
|
export function releaseRunUniMp(context: UIContext, uniMpId: string, targetPath: string, callback: (status: number) => void){
|
||||||
|
let path = getContext().filesDir + "/"+uniMpId+".wgt"
|
||||||
|
uniMpConfig().then(res => {
|
||||||
|
releaseWgtToRunPath(uniMpId, path, (code: 1 | -1, _) =>{
|
||||||
|
if(code === 1){
|
||||||
|
callback(4)
|
||||||
|
// 释放成功,运行
|
||||||
|
const mp = openUniMP(uniMpId, {
|
||||||
|
redirectPath: targetPath,
|
||||||
|
extraData: res,
|
||||||
|
showCapsuleButton: false,
|
||||||
|
showSplashScreen: true,
|
||||||
|
}) as UniMP
|
||||||
|
|
||||||
|
mp.on('close',()=>{
|
||||||
|
console.log('UniMP-close')
|
||||||
|
})
|
||||||
|
mp.on('show',()=>{
|
||||||
|
console.log('UniMP-show')
|
||||||
|
callback(3)
|
||||||
|
})
|
||||||
|
mp.on('hide',()=>{
|
||||||
|
console.log('UniMP-hide')
|
||||||
|
})
|
||||||
|
mp.on('uniMPEvent', (event: string, data: object, _notify: boolean) => {
|
||||||
|
Logger.info('UniMp', 'uniMPEvent:' + event + ', data:' + data)
|
||||||
|
|
||||||
|
if(event === 'start_combo_pay'){
|
||||||
|
const jsonStr = typeof data === 'string' ? data : JSON.stringify(data);
|
||||||
|
const obj = JSON.parse(jsonStr) as TradeData;
|
||||||
|
|
||||||
|
const weixinMpOriId: string = obj.weixinMpOriId ?? "";
|
||||||
|
const outTradeNo: string = obj.outTradeNo ?? "";
|
||||||
|
CommonService.startUniPay(getContext() as common.UIAbilityContext, weixinMpOriId, outTradeNo)
|
||||||
|
}else if(event === 'closeUniAPP'){
|
||||||
|
mp.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
// 释放失败,提示用户
|
||||||
|
ToastUtils.showToast(context, '加载失败,请重试或联系客服!')
|
||||||
|
callback(5)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uniMpConfig(): Promise<Record<string, string>>{
|
||||||
|
let extraData: Record<string, string> = {};
|
||||||
|
|
||||||
|
const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
|
||||||
|
const versionName = bundleInfo.versionName;
|
||||||
|
const bundleName = bundleInfo.name;
|
||||||
|
|
||||||
|
const accessToken: string = KVStore.getInstance().getSync(TOKEN, '');
|
||||||
|
const deviceId: string = DeviceUtil.getDeviceId();
|
||||||
|
const brand: string = deviceInfo.brand;
|
||||||
|
const model: string = deviceInfo.productModel;
|
||||||
|
|
||||||
|
extraData["x-token"] = accessToken;
|
||||||
|
extraData["x-version"] = versionName;
|
||||||
|
extraData["x-platform"] = PLATFORM;
|
||||||
|
extraData["x-device-id"] = deviceId;
|
||||||
|
extraData["x-mobile-brand"] = brand;
|
||||||
|
extraData["x-mobile-model"] = model;
|
||||||
|
extraData["x-channel"] = CHANNEL;
|
||||||
|
extraData["x-package"] = bundleName;
|
||||||
|
extraData["x-click-id"] = '';
|
||||||
|
extraData["host"] = RELEASE_BASE_URL + '/';
|
||||||
|
extraData["decrypt"] = AESDecrypt;
|
||||||
|
extraData["encrypt"] = Signature;
|
||||||
|
extraData["isCombo"] = 'ok';
|
||||||
|
|
||||||
|
return extraData
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
@Component
|
||||||
|
export struct SplashWxUniMpView {
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_splash_wx_unimp'))
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
import { AlipayProfile, CaptchaProfile, LoginProfile, UserInfo, UserProfile, UserProfileMapper } from "./DataBean";
|
||||||
|
import deviceInfo from "@ohos.deviceInfo";
|
||||||
|
import { DeviceUtil } from "@pura/harmony-utils";
|
||||||
|
import { Logger } from "../utils/Logger";
|
||||||
|
import { ApiManager } from "../provider/ApiManager";
|
||||||
|
import { DEVICE_OAID, LOGIN_PROFILE, TOKEN, TOKEN_LOGIN, USER_INFO, USER_PROFILE,
|
||||||
|
WXAPPID} from "../constants/AppConstants";
|
||||||
|
import { KVStore } from "../utils/KVStore";
|
||||||
|
import { RequestCallback } from "../provider/Callback";
|
||||||
|
import { EVT_USER_INFO,EVT_USER_CONFIG_PROFILE } from "../constants/EventConstants";
|
||||||
|
import { emitter } from "@kit.BasicServicesKit";
|
||||||
|
import { AFAuthServiceResponse, AFService, AFServiceCenter, AFServiceParams, AFWantParams } from "@alipay/afservicesdk";
|
||||||
|
import { common } from "@kit.AbilityKit";
|
||||||
|
import { ToastUtils } from "../dialog/ToastUtils";
|
||||||
|
|
||||||
|
import * as wxopensdk from '@tencent/wechat_open_sdk';
|
||||||
|
// 注意:全局只能用同一个WXApi对象,否则无法监听回调
|
||||||
|
export const WXApi = wxopensdk.WXAPIFactory.createWXAPI(WXAPPID)
|
||||||
|
|
||||||
|
export class CommonService {
|
||||||
|
private static userConfigEvent(isSuccess: boolean){
|
||||||
|
const eventData: emitter.EventData = {
|
||||||
|
data: { "result": isSuccess }
|
||||||
|
};
|
||||||
|
emitter.emit({ eventId: EVT_USER_CONFIG_PROFILE }, eventData);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 请求用户配置信息 (带参请求)
|
||||||
|
*/
|
||||||
|
static async getUserProfile(): Promise<UserProfile> {
|
||||||
|
const osVersion: number = deviceInfo.sdkApiVersion;
|
||||||
|
const oaid: string = AppStorage.get<string>(DEVICE_OAID)??'';
|
||||||
|
const imei: string = DeviceUtil.getDeviceId();
|
||||||
|
|
||||||
|
Logger.info('UserService',
|
||||||
|
` 请求参数:----> oaid:${oaid}---> osVersion:${osVersion}---> imei:${imei}`);
|
||||||
|
const queryParams: Record<string, Object> = {
|
||||||
|
'oaid': oaid,
|
||||||
|
'os_version': osVersion,
|
||||||
|
"imei": imei,
|
||||||
|
"cid": ''
|
||||||
|
};
|
||||||
|
let result = await ApiManager.getInstance().requestForMapper<UserProfile>(
|
||||||
|
'/api/user/config',
|
||||||
|
queryParams, // 传参,如有则传入
|
||||||
|
UserProfileMapper // 映射,数据映射器
|
||||||
|
);
|
||||||
|
AppStorage.setOrCreate<UserProfile>(USER_PROFILE, result);
|
||||||
|
if (result.token && result.token.length > 0) {
|
||||||
|
KVStore.getInstance().put(TOKEN, result.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonService.userConfigEvent(true)
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送验证码
|
||||||
|
* @param phone
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
static async sendSmsCode(phone: string, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
const postParms: Record<string, string> = {
|
||||||
|
"phone": phone
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await ApiManager.getInstance().request<CaptchaProfile>(
|
||||||
|
'api/user/code', // api
|
||||||
|
undefined, // get请求参数,会拼接到url中
|
||||||
|
'post', // 请求方式
|
||||||
|
postParms // post请求参数
|
||||||
|
);
|
||||||
|
|
||||||
|
if(result.code === 200 || result.code === 0){
|
||||||
|
if (callback && callback.onSuccess) {
|
||||||
|
callback.onSuccess(result.data.timestamp);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if (callback && callback.onFailure) {
|
||||||
|
callback.onFailure(result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录:验证码登录
|
||||||
|
* @param phone
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
static async loginForCaptcha(phone: string, captchaCode: string, captchaTimestamp: string, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
const jsonPhone: Record<string, string | number> = {
|
||||||
|
"timestamp": captchaTimestamp, // 确保这个 value 是 string 或 number
|
||||||
|
"phone": phone,
|
||||||
|
"code": captchaCode
|
||||||
|
};
|
||||||
|
const postData: Record<string, Object> = {
|
||||||
|
"login_type": "phone",
|
||||||
|
"phone": jsonPhone
|
||||||
|
};
|
||||||
|
|
||||||
|
CommonService.login(postData, callback);
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录(通用)
|
||||||
|
* @param requestData
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
static async login(requestData: Record<string, Object>, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
let result = await ApiManager.getInstance().request<LoginProfile>(
|
||||||
|
'/api/user/login',
|
||||||
|
undefined,
|
||||||
|
'post',
|
||||||
|
requestData
|
||||||
|
);
|
||||||
|
|
||||||
|
if(result.code === 200 || result.code === 0){
|
||||||
|
const token = result.data.token
|
||||||
|
if(token && token.length > 0){
|
||||||
|
//将登录信息持久化(const savedProfile = JSON.parse(rawJson) as LoginProfile;)
|
||||||
|
const loginJson: string = JSON.stringify(result);
|
||||||
|
KVStore.getInstance().put(LOGIN_PROFILE, loginJson); //持久化登录信息
|
||||||
|
KVStore.getInstance().put(TOKEN_LOGIN, token ?? ''); //持久化token
|
||||||
|
KVStore.getInstance().put(TOKEN, token ?? ''); //持久化token
|
||||||
|
}
|
||||||
|
if (callback && callback.onSuccess) {
|
||||||
|
callback.onSuccess(result.data);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if (callback && callback.onFailure) {
|
||||||
|
callback.onFailure(result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static userInfoEvent(isSuccess: boolean){
|
||||||
|
const eventData: emitter.EventData = {
|
||||||
|
data: { "result": isSuccess }
|
||||||
|
};
|
||||||
|
emitter.emit({ eventId: EVT_USER_INFO }, eventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
static async getUserInfo(): Promise<UserInfo> {
|
||||||
|
const result = await ApiManager.getInstance().request<UserInfo>(
|
||||||
|
'/api/user',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const userInfo = result.data
|
||||||
|
|
||||||
|
//将用户信息持久化(const savedProfile = JSON.parse(rawJson) as UserInfo;)
|
||||||
|
const userInfoJson: string = JSON.stringify(userInfo);
|
||||||
|
KVStore.getInstance().put(USER_INFO, userInfoJson); //持久化用户信息
|
||||||
|
|
||||||
|
CommonService.userInfoEvent(true)
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉起微信,获取微信授权
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
|
static async wxAuthorize(context: common.UIAbilityContext, isBindAuth: boolean){
|
||||||
|
const isInstalled = WXApi.isWXAppInstalled()
|
||||||
|
if(!isInstalled){
|
||||||
|
const uiContext = context.windowStage.getMainWindowSync().getUIContext();
|
||||||
|
ToastUtils.showToast(uiContext, '您没有安装微信客户端,请先下载安装')
|
||||||
|
}else{
|
||||||
|
const bundleName: string = context.abilityInfo.bundleName;
|
||||||
|
let req = new wxopensdk.SendAuthReq;
|
||||||
|
req.isOption1 = false
|
||||||
|
req.nonAutomatic = true
|
||||||
|
req.scope = 'snsapi_userinfo';// 只能填 snsapi_userinfo
|
||||||
|
req.state = bundleName + Math.random() * 1000 + "_phone";
|
||||||
|
req.transaction = isBindAuth ? 'rabbit_bind' : 'rabbit_login'
|
||||||
|
|
||||||
|
WXApi.sendReq(context, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async wxLogin(authCode: string, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
const jsonWxAuth: Record<string, string | number> = {
|
||||||
|
"code": authCode,
|
||||||
|
"code_type": ''
|
||||||
|
};
|
||||||
|
const postData: Record<string, Object> = {
|
||||||
|
"login_type": "weixin",
|
||||||
|
"weixin": jsonWxAuth
|
||||||
|
};
|
||||||
|
|
||||||
|
CommonService.login(postData, callback);
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付宝登录参数
|
||||||
|
*/
|
||||||
|
static async getAlipayLoginParams(): Promise<AlipayProfile> {
|
||||||
|
const result = await ApiManager.getInstance().request<AlipayProfile>(
|
||||||
|
'api/alipay/app_param',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async aliAuthorize(profile: AlipayProfile, callback: (result: string) => void) {
|
||||||
|
const queryStr: string = profile.param??'';
|
||||||
|
const match: RegExpMatchArray | null = queryStr.match(/app_id=([^&]*)/);
|
||||||
|
let appId: string = '';
|
||||||
|
if (match !== null && match[1]) {
|
||||||
|
appId = match[1];
|
||||||
|
}
|
||||||
|
// 获取支付宝登录参数
|
||||||
|
//1. 构建参数
|
||||||
|
let bizParams = new Map<string, string>()
|
||||||
|
let url = encodeURIComponent('https://authweb.alipay.com/auth?auth_type=PURE_OAUTH_SDK&app_id='+appId+'&scope=auth_user&state=init')
|
||||||
|
bizParams.set("url", url);
|
||||||
|
|
||||||
|
let backWant: AFWantParams = {
|
||||||
|
bundleName: "com.img.rabbit",
|
||||||
|
moduleName: "app",
|
||||||
|
abilityName: "AppAbility"
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = new AFServiceParams(bizParams, false, true, '', backWant, (response: AFAuthServiceResponse) => {
|
||||||
|
//授权返回值
|
||||||
|
let result = response.result
|
||||||
|
if (result) {
|
||||||
|
let parameters = result['parameters']
|
||||||
|
if (parameters) {
|
||||||
|
let authCode: string = parameters['auth_code']
|
||||||
|
callback(authCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
AFServiceCenter.call(AFService.AFServiceAuth, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async aliLogin(authCode: string, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
const jsonAliAuth: Record<string, string | number> = {
|
||||||
|
"auth_code": authCode
|
||||||
|
};
|
||||||
|
const postData: Record<string, Object> = {
|
||||||
|
"type": "alipay",
|
||||||
|
"bind": "",
|
||||||
|
"data": jsonAliAuth
|
||||||
|
};
|
||||||
|
|
||||||
|
CommonService.login(postData, callback);
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static startUniPay(context: common.UIAbilityContext, weixinMpOriId: string, outTradeNo: string) {
|
||||||
|
const req: wxopensdk.LaunchMiniProgramReq = new wxopensdk.LaunchMiniProgramReq();
|
||||||
|
|
||||||
|
req.userName = weixinMpOriId;
|
||||||
|
req.path = `pages/index/index?outTradeNo=${outTradeNo}`;
|
||||||
|
req.miniprogramType = 0;
|
||||||
|
|
||||||
|
WXApi.sendReq(context, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
import ArrayList from "@ohos.util.ArrayList";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserProfile 的专属转换器
|
||||||
|
*/
|
||||||
|
export const UserProfileMapper = (raw: Record<string, Object>): UserProfile => {
|
||||||
|
return {
|
||||||
|
token: (raw['token'] as string) ?? "",
|
||||||
|
temp: (raw['temp'] as boolean) ?? false,
|
||||||
|
name: (raw['name'] as string) ?? "",
|
||||||
|
user_id: (raw['user_id'] as string) ?? "",
|
||||||
|
config: ConfigMapper(raw['config'] as Record<string, Object>)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置转换器
|
||||||
|
*/
|
||||||
|
export const ConfigMapper = (raw: Record<string, Object>): ConfigEntity => {
|
||||||
|
return {
|
||||||
|
versionEntity: (raw['client.version.upgrade'] as VersionEntity) ?? null,
|
||||||
|
uniVersionList: (raw['client.uni.version.upgrade'] as UniVersionEntity[]) ?? [],
|
||||||
|
homeIconEntity: (raw['client.icon.uni.home'] as UniIconEntity[]) ?? [],
|
||||||
|
wgtPassword: (raw['client.wgt.password'] as string) ?? null,
|
||||||
|
isUniMpOpen: (raw['client.uni.open'] as boolean) ?? false
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用户配置(Profile)
|
||||||
|
export interface UserProfile {
|
||||||
|
token: string;
|
||||||
|
temp: boolean;
|
||||||
|
name: string; // 显示的文本内容
|
||||||
|
user_id: string;
|
||||||
|
config: ConfigEntity; // 可选的图标资源
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置(Config)
|
||||||
|
export interface ConfigEntity {
|
||||||
|
versionEntity: VersionEntity | null;
|
||||||
|
uniVersionList: UniVersionEntity[];
|
||||||
|
homeIconEntity: UniIconEntity[];
|
||||||
|
wgtPassword: string | null;
|
||||||
|
isUniMpOpen: boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置(App版本)
|
||||||
|
export interface VersionEntity {
|
||||||
|
description: string | null;
|
||||||
|
force: boolean;
|
||||||
|
last_version_force: string | null;
|
||||||
|
title: string | null;
|
||||||
|
url: string | null;
|
||||||
|
app_size: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置(UniMp版本)
|
||||||
|
export interface UniVersionEntity {
|
||||||
|
version: string | null;
|
||||||
|
url: string | null;
|
||||||
|
last_version_force: string | null;
|
||||||
|
force: boolean;
|
||||||
|
title: string | null;
|
||||||
|
description: string | null;
|
||||||
|
unimp_id: string | null;
|
||||||
|
unimp_type: string | null;
|
||||||
|
icon: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置(UniMp指定路径:Path)
|
||||||
|
export interface UniIconEntity {
|
||||||
|
icon: string | null;
|
||||||
|
text: string | null;
|
||||||
|
url: string | null;
|
||||||
|
type: string | null;
|
||||||
|
enable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码信息
|
||||||
|
export interface CaptchaProfile {
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录信息
|
||||||
|
export interface LoginProfile {
|
||||||
|
user_id: string | null;
|
||||||
|
name: string | null;
|
||||||
|
avater: string | null;
|
||||||
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 用户信息(字段由/api/user 与 /api/user/account共同提供)
|
||||||
|
export interface UserInfo {
|
||||||
|
alipayid: string | null;
|
||||||
|
app_id: string | null;
|
||||||
|
appleid: string | null;
|
||||||
|
avater: string | null;
|
||||||
|
balance: string | null;
|
||||||
|
client_cid: string | null;
|
||||||
|
coupon_count: string | null;
|
||||||
|
ip_area: string | null;
|
||||||
|
ip_area_name: string | null;
|
||||||
|
name: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
role: string | null;
|
||||||
|
temp: boolean | true;
|
||||||
|
unionid: string | null;
|
||||||
|
user_id: string | null;
|
||||||
|
vip: number | 0;
|
||||||
|
vip_expire: string | null;
|
||||||
|
vip_expire_time: string | null;
|
||||||
|
vip_name: string | null;
|
||||||
|
weixinAppId: string | null;
|
||||||
|
weixinAppIdType: string | null;
|
||||||
|
weixinAppOpenId: string | null;
|
||||||
|
weixinOpenId: string | null;
|
||||||
|
//以下字段,目前由/api/user/account提供
|
||||||
|
vip_type: number | 0;
|
||||||
|
create_time: string | null;
|
||||||
|
bind: ArrayList<string> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付宝授权登录参数
|
||||||
|
export interface AlipayProfile {
|
||||||
|
param: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UniMp小程序微信支付参数
|
||||||
|
export interface TradeData {
|
||||||
|
weixinMpOriId?: string;
|
||||||
|
outTradeNo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {
|
||||||
|
* "icon": "https://cdn.batiao8.com/flaunt/uni_mp/icon/other/ticket.png",
|
||||||
|
* "text": "机票",
|
||||||
|
* "url": "pages/other/tickets-app/index",
|
||||||
|
* "type": "alipay",
|
||||||
|
* "enable": true
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
// 小程序(指定Page)
|
||||||
|
export interface UniMpItem {
|
||||||
|
id: string;
|
||||||
|
text: string; // 显示的文本内容
|
||||||
|
icon: Resource; // 可选的图标资源
|
||||||
|
url: string; // 跳转路径
|
||||||
|
type: string; // 类型
|
||||||
|
enable: boolean; // 是否可用
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { image } from "@kit.ImageKit";
|
||||||
|
import { util } from "@kit.ArkTS";
|
||||||
|
|
||||||
|
// 本地数据,用于处理抠图
|
||||||
|
@Observed
|
||||||
|
export class StickerItem {
|
||||||
|
id: string = util.generateRandomUUID();
|
||||||
|
pixelMap: image.PixelMap;
|
||||||
|
type: number = 0; // 0:人像, 1:服装, 2:发型
|
||||||
|
posX: number = 0;
|
||||||
|
posY: number = 0;
|
||||||
|
lastX: number = 0;
|
||||||
|
lastY: number = 0;
|
||||||
|
scaleValue: number = 1.0;
|
||||||
|
lastScale: number = 1.0;
|
||||||
|
rotateValue: number = 0;
|
||||||
|
lastRotate: number = 0;
|
||||||
|
zIndex: number = 0;
|
||||||
|
isHidden: boolean = false;
|
||||||
|
|
||||||
|
constructor(pm: image.PixelMap, type: number) {
|
||||||
|
this.pixelMap = pm;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地数据,用于处理下载更新UniMp资源
|
||||||
|
export class UniMpLocalVersion {
|
||||||
|
id:string =''; // __UNI__F1F2FC0
|
||||||
|
type: string = ''; // wx:微信, alipay:阿里
|
||||||
|
version: string = ''; // 1.0.0
|
||||||
|
path: string = ''; // data/storage/el2/base/haps/app/files/__UNI__F1F2FC0.wgt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
// 页面参数数据
|
||||||
|
export interface WebParams {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抠图参数数据
|
||||||
|
export interface CuteParams {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定参数数据
|
||||||
|
export interface BindParams {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: number;//1:phone,2:wx
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"module": {
|
||||||
|
"name": "common",
|
||||||
|
"type": "har",
|
||||||
|
"deviceTypes": [
|
||||||
|
"phone"
|
||||||
|
],
|
||||||
|
"requestPermissions": [
|
||||||
|
{
|
||||||
|
"name": "ohos.permission.INTERNET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ohos.permission.APP_TRACKING_CONSENT",
|
||||||
|
"reason": "$string:reason_oaid",
|
||||||
|
"usedScene": {
|
||||||
|
"abilities": ["AppAbility"],
|
||||||
|
"when": "inuse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"name": "page_show",
|
||||||
|
"value": "page from package"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reason_read_image",
|
||||||
|
"value": "我们需要读取您的相册以选择图片完成拼图。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reason_oaid",
|
||||||
|
"value": "用于创建唯一的oaid服务标识。"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 881 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
import localUnitTest from './LocalUnit.test';
|
||||||
|
|
||||||
|
export default function testsuite() {
|
||||||
|
localUnitTest();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
|
||||||
|
|
||||||
|
export default function localUnitTest() {
|
||||||
|
describe('localUnitTest', () => {
|
||||||
|
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
|
||||||
|
beforeAll(() => {
|
||||||
|
// Presets an action, which is performed only once before all test cases of the test suite start.
|
||||||
|
// This API supports only one parameter: preset action function.
|
||||||
|
});
|
||||||
|
beforeEach(() => {
|
||||||
|
// Presets an action, which is performed before each unit test case starts.
|
||||||
|
// The number of execution times is the same as the number of test cases defined by **it**.
|
||||||
|
// This API supports only one parameter: preset action function.
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
// Presets a clear action, which is performed after each unit test case ends.
|
||||||
|
// The number of execution times is the same as the number of test cases defined by **it**.
|
||||||
|
// This API supports only one parameter: clear action function.
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
// Presets a clear action, which is performed after all test cases of the test suite end.
|
||||||
|
// This API supports only one parameter: clear action function.
|
||||||
|
});
|
||||||
|
it('assertContain', 0, () => {
|
||||||
|
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
|
||||||
|
let a = 'abc';
|
||||||
|
let b = 'b';
|
||||||
|
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
|
||||||
|
expect(a).assertContain(b);
|
||||||
|
expect(a).assertEqual(a);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||||
|
*/
|
||||||
|
export const HAR_VERSION = '1.0.0';
|
||||||
|
export const BUILD_MODE_NAME = 'release';
|
||||||
|
export const DEBUG = false;
|
||||||
|
export const TARGET_NAME = 'default';
|
||||||
|
export const CHANNEL = 'harmony';
|
||||||
|
export const BUILD_TIME = '2605061515';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildProfile Class is used only for compatibility purposes.
|
||||||
|
*/
|
||||||
|
export default class BuildProfile {
|
||||||
|
static readonly HAR_VERSION = HAR_VERSION;
|
||||||
|
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||||
|
static readonly DEBUG = DEBUG;
|
||||||
|
static readonly TARGET_NAME = TARGET_NAME;
|
||||||
|
static readonly CHANNEL = CHANNEL;
|
||||||
|
static readonly BUILD_TIME = BUILD_TIME;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { FeedbackLayout } from './src/main/ets/pages/FeedbackLayout';
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"buildOption": {
|
||||||
|
"resOptions": {
|
||||||
|
"copyCodeResource": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buildOptionSet": [
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"arkOptions": {
|
||||||
|
"obfuscation": {
|
||||||
|
"ruleOptions": {
|
||||||
|
"enable": false,
|
||||||
|
"files": [
|
||||||
|
"./obfuscation-rules.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"consumerFiles": [
|
||||||
|
"./consumer-rules.txt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
|
||||||
|
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Define project specific obfuscation rules here.
|
||||||
|
# You can include the obfuscation configuration files in the current module's build-profile.json5.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/source-obfuscation
|
||||||
|
|
||||||
|
# Obfuscation options:
|
||||||
|
# -disable-obfuscation: disable all obfuscations
|
||||||
|
# -enable-property-obfuscation: obfuscate the property names
|
||||||
|
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
|
||||||
|
# -compact: remove unnecessary blank spaces and all line feeds
|
||||||
|
# -remove-log: remove all console.* statements
|
||||||
|
# -print-namecache: print the name cache that contains the mapping from the old names to new names
|
||||||
|
# -apply-namecache: reuse the given cache file
|
||||||
|
|
||||||
|
# Keep options:
|
||||||
|
# -keep-property-name: specifies property names that you want to keep
|
||||||
|
# -keep-global-name: specifies names that you want to keep in the global scope
|
||||||
|
|
||||||
|
-enable-property-obfuscation
|
||||||
|
-enable-toplevel-obfuscation
|
||||||
|
-enable-filename-obfuscation
|
||||||
|
-enable-export-obfuscation
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"stableOrder": true,
|
||||||
|
"enableUnifiedLockfile": false
|
||||||
|
},
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||||
|
"specifiers": {
|
||||||
|
"@ohos/common@../../common": "@ohos/common@../../common"
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@ohos/common@../../common": {
|
||||||
|
"name": "@ohos/common",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "../../common",
|
||||||
|
"registryType": "local"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "main_layout",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "主功能模块,大多数页面和功能放于此模块。",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@ohos/common": "file:../../common",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Use these variables when you tailor your ArkTS code. They must be of the const type.
|
||||||
|
*/
|
||||||
|
export const HAR_VERSION = '0.0.1';
|
||||||
|
export const BUILD_MODE_NAME = 'release';
|
||||||
|
export const DEBUG = false;
|
||||||
|
export const TARGET_NAME = 'default';
|
||||||
|
export const CHANNEL = 'harmony';
|
||||||
|
export const BUILD_TIME = '2605061515';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BuildProfile Class is used only for compatibility purposes.
|
||||||
|
*/
|
||||||
|
export default class BuildProfile {
|
||||||
|
static readonly HAR_VERSION = HAR_VERSION;
|
||||||
|
static readonly BUILD_MODE_NAME = BUILD_MODE_NAME;
|
||||||
|
static readonly DEBUG = DEBUG;
|
||||||
|
static readonly TARGET_NAME = TARGET_NAME;
|
||||||
|
static readonly CHANNEL = CHANNEL;
|
||||||
|
static readonly BUILD_TIME = BUILD_TIME;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { Logger } from './src/main/ets/utils/Logger';
|
||||||
|
|
||||||
|
export { BreakpointSystem, BreakPointType } from './src/main/ets/utils/BreakpointSystem';
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"apiType": "stageMode",
|
||||||
|
"buildOption": {
|
||||||
|
"resOptions": {
|
||||||
|
"copyCodeResource": {
|
||||||
|
"enable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buildOptionSet": [
|
||||||
|
{
|
||||||
|
"name": "release",
|
||||||
|
"arkOptions": {
|
||||||
|
"obfuscation": {
|
||||||
|
"ruleOptions": {
|
||||||
|
"enable": false,
|
||||||
|
"files": [
|
||||||
|
"./obfuscation-rules.txt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"consumerFiles": [
|
||||||
|
"./consumer-rules.txt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { harTasks } from '@ohos/hvigor-ohos-plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
|
||||||
|
plugins: [] /* Custom plugin to extend the functionality of Hvigor. */
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Define project specific obfuscation rules here.
|
||||||
|
# You can include the obfuscation configuration files in the current module's build-profile.json5.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/source-obfuscation
|
||||||
|
|
||||||
|
# Obfuscation options:
|
||||||
|
# -disable-obfuscation: disable all obfuscations
|
||||||
|
# -enable-property-obfuscation: obfuscate the property names
|
||||||
|
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
|
||||||
|
# -compact: remove unnecessary blank spaces and all line feeds
|
||||||
|
# -remove-log: remove all console.* statements
|
||||||
|
# -print-namecache: print the name cache that contains the mapping from the old names to new names
|
||||||
|
# -apply-namecache: reuse the given cache file
|
||||||
|
|
||||||
|
# Keep options:
|
||||||
|
# -keep-property-name: specifies property names that you want to keep
|
||||||
|
# -keep-global-name: specifies names that you want to keep in the global scope
|
||||||
|
|
||||||
|
-enable-property-obfuscation
|
||||||
|
-enable-toplevel-obfuscation
|
||||||
|
-enable-filename-obfuscation
|
||||||
|
-enable-export-obfuscation
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "@ohos/common",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "通用模块,用于公共页面、工具或者请求的模块。",
|
||||||
|
"main": "Index.ets",
|
||||||
|
"author": "",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/**
|
||||||
|
* AppConstants
|
||||||
|
*/
|
||||||
|
export const APP_ID: string = '10058'; //应用ID
|
||||||
|
export const RELEASE_BASE_URL: string = 'https://jitutu.batiao8.com'; //release
|
||||||
|
export const DEBUG_BASE_URL: string = 'https://jitutu.batiao8.com'; //debug
|
||||||
|
export const LOG_REQUEST: string = 'RabbitRequest';
|
||||||
|
export const AGREEMENT_URL: string = 'https://jitutu.batiao8.com/static/policy-jietutu/user_android.html'; //用户协议
|
||||||
|
export const PRIVACY_URL: string = 'https://jitutu.batiao8.com/static/policy-jietutu/provacy.html' //隐私政策
|
||||||
|
export const AESDecrypt = "e4rOtnF8tJjtHO7ecZeJHN1rapED5ImB" //解密
|
||||||
|
export const Signature = "xn08hYoizXhZ1zHP8DVqfCm2yHxPmhil" //加密字符
|
||||||
|
|
||||||
|
export const WXAPPID = "wx7d1a7d1507482cef" // 微信APPID
|
||||||
|
export const WXSECRET = "5264c353296db25405fc29e43c40d3a5" //微信secret
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kv store是否初始化完成
|
||||||
|
*/
|
||||||
|
export const IS_KV_STORE_INIT_FINISHED: string = 'kv_store_init_finished';
|
||||||
|
/**
|
||||||
|
* 是否同意用户协议
|
||||||
|
*/
|
||||||
|
export const IS_AGREE_AGREEMENT: string = 'agree_agreement_key';
|
||||||
|
/**
|
||||||
|
* OAID,解决过审率
|
||||||
|
*/
|
||||||
|
export const DEVICE_OAID: string = 'device_oaid';
|
||||||
|
/**
|
||||||
|
* 用户token(注意:所有用户都记录-游客同样记录)
|
||||||
|
*/
|
||||||
|
export const TOKEN: string = 'account_token_key';
|
||||||
|
/**
|
||||||
|
* 登录token(注意:登录用户才记录-游客不记录,退出登录会清空)
|
||||||
|
*/
|
||||||
|
export const TOKEN_LOGIN: string = 'login_token_key';
|
||||||
|
/**
|
||||||
|
* 用户配置
|
||||||
|
*/
|
||||||
|
export const USER_PROFILE: string = 'user_profile';
|
||||||
|
/**
|
||||||
|
* 登录信息
|
||||||
|
*/
|
||||||
|
export const LOGIN_PROFILE: string = 'login_profile';
|
||||||
|
/**
|
||||||
|
* 用户信息
|
||||||
|
*/
|
||||||
|
export const USER_INFO: string = 'user_info';
|
||||||
|
/**
|
||||||
|
* uniMp本地数据版本
|
||||||
|
*/
|
||||||
|
export const UNIMP_DATA_VERSION: string = 'unimp_data_version';
|
||||||
|
/**
|
||||||
|
* 微信授权码(authCode):微信登录(授权)时保存,微信绑定时会使用到
|
||||||
|
*/
|
||||||
|
export const WX_AUTH_CODE: string = 'wx_auth_code';
|
||||||
|
/**
|
||||||
|
* 微信绑定授权
|
||||||
|
*/
|
||||||
|
export const WX_BIND_AUTHOR: string = 'wx_bind_author';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台标识(目前后台接口仅支持android和ios,暂不支持harmony)
|
||||||
|
* android、ios or harmony
|
||||||
|
*/
|
||||||
|
export const PLATFORM: string = 'android';
|
||||||
|
/**
|
||||||
|
* 备案号(filing)
|
||||||
|
*/
|
||||||
|
export const FILING_NO='渝ICP备2025064932号-17A'
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
interface CommonConstantsInterface {
|
||||||
|
breakpointsSmName: string;
|
||||||
|
breakpointsMdName: string;
|
||||||
|
breakpointsLgName: string;
|
||||||
|
breakpointsXlName: string;
|
||||||
|
breakpointsSmSize: number;
|
||||||
|
breakpointsMdSize: number;
|
||||||
|
breakpointsLgSize: number;
|
||||||
|
breakpointsXlSize: number;
|
||||||
|
breakpointsInitializeName: string;
|
||||||
|
breakpointIdInitializeName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common constants for all features.
|
||||||
|
*/
|
||||||
|
export const commonConstants: CommonConstantsInterface = {
|
||||||
|
/**
|
||||||
|
* Breakpoint sm.
|
||||||
|
*/
|
||||||
|
breakpointsSmName: 'sm',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breakpoint md.
|
||||||
|
*/
|
||||||
|
breakpointsMdName: 'md',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breakpoint lg.
|
||||||
|
*/
|
||||||
|
breakpointsLgName: 'lg',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Breakpoint xl.
|
||||||
|
*/
|
||||||
|
breakpointsXlName: 'xl',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The break point value of sm device.
|
||||||
|
*/
|
||||||
|
breakpointsSmSize: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The break point value of md device.
|
||||||
|
*/
|
||||||
|
breakpointsMdSize: 600,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The break point value of lg device.
|
||||||
|
*/
|
||||||
|
breakpointsLgSize: 840,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The break point value of xl device.
|
||||||
|
*/
|
||||||
|
breakpointsXlSize: 1320,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize device breakpoints.
|
||||||
|
*/
|
||||||
|
breakpointsInitializeName: 'md',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize device breakpoint Id.
|
||||||
|
*/
|
||||||
|
breakpointIdInitializeName: 'unknown'
|
||||||
|
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KVStore 初始化完成事件
|
||||||
|
*/
|
||||||
|
export const EVT_KV_INIT_DONE: number = 0;
|
||||||
|
/**
|
||||||
|
* LOGIN 获取用户信息
|
||||||
|
*/
|
||||||
|
export const EVT_USER_INFO: number = 1;
|
||||||
|
/**
|
||||||
|
* LOGIN Author 微信登录授权成功
|
||||||
|
*/
|
||||||
|
export const EVT_LOGIN_WX_AUTHOR_SUCCESS: number = 2;
|
||||||
|
/**
|
||||||
|
* BIND Author 微信绑定授权成功
|
||||||
|
*/
|
||||||
|
export const EVT_BIND_WX_AUTHOR_SUCCESS: number = 3;
|
||||||
|
/**
|
||||||
|
* NORMAL 获取用户配置信息
|
||||||
|
*/
|
||||||
|
export const EVT_USER_CONFIG_PROFILE: number = 4;
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { common, Want } from "@kit.AbilityKit";
|
||||||
|
import { AGREEMENT_URL, PRIVACY_URL } from "../constants/AppConstants";
|
||||||
|
|
||||||
|
@CustomDialog
|
||||||
|
export struct AgreementDialog {
|
||||||
|
private context = getContext(this) as common.UIAbilityContext;
|
||||||
|
// 接收外部传入的参数
|
||||||
|
confirmText: string = '同意';
|
||||||
|
cancelText: string = '不同意';
|
||||||
|
|
||||||
|
controller?: CustomDialogController;
|
||||||
|
cancel: () => void = () => {};
|
||||||
|
confirm: () => void = () => {};
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_dialog_tips_mask_top'))
|
||||||
|
.width('100%')
|
||||||
|
.opacity(0.5)
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
// 标题
|
||||||
|
Text('用户协议与隐私政策')
|
||||||
|
.fontSize(18)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.margin({ top: 30, bottom: 32 })
|
||||||
|
|
||||||
|
// 描述文字
|
||||||
|
Text(){//'请您务必审慎阅读、充分理解《用户协议》与《隐私政策》各条款,包括但不限于:为了更好的向您提供服务,我们需要访问您的相册、相机等。您可以阅读《隐私政策》了解详细信息如果您同意,请点击下面“同意”按钮开始接受我们的服务。'
|
||||||
|
Span('请您务必审慎阅读、充分理解')
|
||||||
|
.fontColor('#CCAAAAAA')
|
||||||
|
Span('《用户协议》')
|
||||||
|
.fontColor('#FFAAAAAA') // 高亮颜色(深灰)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.onClick(() => {
|
||||||
|
const context = getContext(this) as common.UIAbilityContext;
|
||||||
|
webBrowser(context, AGREEMENT_URL)
|
||||||
|
})
|
||||||
|
Span('与')
|
||||||
|
.fontColor('#CCAAAAAA')
|
||||||
|
Span('《隐私政策》')
|
||||||
|
.fontColor('#FFAAAAAA') // 高亮颜色(深灰)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.onClick(() => {
|
||||||
|
const context = getContext(this) as common.UIAbilityContext;
|
||||||
|
webBrowser(context, PRIVACY_URL)
|
||||||
|
})
|
||||||
|
Span('各条款,包括但不限于:为了更好的向您提供服务,我们需要访问您的相册、相机等。您可以阅读')
|
||||||
|
.fontColor('#CCAAAAAA')
|
||||||
|
Span('《隐私政策》')
|
||||||
|
.fontColor('#FFAAAAAA') // 高亮颜色(深灰)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.onClick(() => {
|
||||||
|
const context = getContext(this) as common.UIAbilityContext;
|
||||||
|
webBrowser(context, PRIVACY_URL)
|
||||||
|
})
|
||||||
|
Span('了解详细信息如果您同意,请点击下面“同意”按钮开始接受我们的服务。')
|
||||||
|
.fontColor('#CCAAAAAA')
|
||||||
|
}
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.margin({ left: 16, right: 16 })
|
||||||
|
|
||||||
|
// 按钮区域
|
||||||
|
Column() {
|
||||||
|
Button(this.confirmText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.confirm();
|
||||||
|
this.controller?.close();
|
||||||
|
})
|
||||||
|
.width('100%')
|
||||||
|
.backgroundColor(Color.Black) // 对应图片中的黑色按钮
|
||||||
|
.fontColor(Color.White)
|
||||||
|
.height(44)
|
||||||
|
|
||||||
|
Button(this.cancelText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.controller?.close();
|
||||||
|
this.cancel();
|
||||||
|
this.context.terminateSelf();
|
||||||
|
})
|
||||||
|
.width('100%')
|
||||||
|
.backgroundColor('#FFFFFFFF')
|
||||||
|
.fontColor('#FFAAAAAA')
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.margin({ top: 68})
|
||||||
|
.padding({ left: 20, right: 20 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('90%')
|
||||||
|
.alignContent(Alignment.TopStart)
|
||||||
|
.backgroundColor(Color.White)
|
||||||
|
.borderRadius(24) // 较大的圆角
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动时,协议使用外部浏览器打开
|
||||||
|
* @param context
|
||||||
|
* @param url
|
||||||
|
*/
|
||||||
|
function webBrowser(context: common.UIAbilityContext, url: string){
|
||||||
|
const want: Want = {
|
||||||
|
action: 'ohos.want.action.viewData',
|
||||||
|
entities: ['entity.system.browsable'],
|
||||||
|
uri: url
|
||||||
|
};
|
||||||
|
|
||||||
|
context.startAbility(want);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
@CustomDialog
|
||||||
|
export struct GlobalDownloadingDialog {
|
||||||
|
controller: CustomDialogController;
|
||||||
|
@State show: boolean = false;
|
||||||
|
@State progress: number = 0;
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
if(this.progress > 0){
|
||||||
|
Column(){
|
||||||
|
Progress({
|
||||||
|
value: this.progress,
|
||||||
|
total: 100,
|
||||||
|
type: ProgressType.Linear
|
||||||
|
})
|
||||||
|
.width('100%')
|
||||||
|
.height(10)
|
||||||
|
.color('#ff4f8af6')
|
||||||
|
.backgroundColor('#ebebeb')
|
||||||
|
.style({ strokeWidth: 10, enableSmoothEffect: true })
|
||||||
|
|
||||||
|
Text('下载中,请稍后:' + this.progress + '%').fontColor(Color.White).fontSize(12).margin({ top: 4})
|
||||||
|
}
|
||||||
|
.margin({ left:8, right:8 })
|
||||||
|
}else{
|
||||||
|
Column(){
|
||||||
|
LoadingProgress()
|
||||||
|
.width(60)
|
||||||
|
.color('#ff4f8af6')
|
||||||
|
|
||||||
|
Text('加载中,请稍后...').fontColor(Color.White).fontSize(12).margin({ top: 12})
|
||||||
|
}
|
||||||
|
.margin({ left:8, right:8 })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
.backgroundColor('#99000000')
|
||||||
|
.borderRadius(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
@CustomDialog
|
||||||
|
export struct TipsDialog {
|
||||||
|
// 接收外部传入的参数
|
||||||
|
title: string = '提示';
|
||||||
|
content: string = '';
|
||||||
|
desc: string = '';
|
||||||
|
textAlign: TextAlign = TextAlign.Center;
|
||||||
|
confirmText: string = '确定';
|
||||||
|
cancelText: string = '取消';
|
||||||
|
|
||||||
|
controller: CustomDialogController;
|
||||||
|
cancel: () => void = () => {};
|
||||||
|
confirm: () => void = () => {};
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_dialog_tips_mask_top'))
|
||||||
|
.width('100%')
|
||||||
|
.opacity(0.5)
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
// 标题
|
||||||
|
Text(this.title)
|
||||||
|
.fontSize(18)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.margin({ top: 30, bottom: 12 })
|
||||||
|
|
||||||
|
// 描述文字
|
||||||
|
Text(this.content)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.textAlign(this.textAlign)
|
||||||
|
.margin({ left: 16, right: 16 })
|
||||||
|
Text(this.desc)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.textAlign(this.textAlign)
|
||||||
|
.margin({ left: 16, right: 16, bottom: 30 })
|
||||||
|
|
||||||
|
// 按钮区域
|
||||||
|
Row({ space: 20 }) {
|
||||||
|
Button(this.cancelText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.controller.close();
|
||||||
|
this.cancel();
|
||||||
|
})
|
||||||
|
.backgroundColor('#FFFFFFFF')
|
||||||
|
.fontColor('#333333')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.borderWidth(0.5)
|
||||||
|
.borderColor('#FF252525')
|
||||||
|
.height(44)
|
||||||
|
|
||||||
|
Button(this.confirmText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.confirm();
|
||||||
|
this.controller.close();
|
||||||
|
})
|
||||||
|
.backgroundColor(Color.Black) // 对应图片中的黑色按钮
|
||||||
|
.fontColor(Color.White)
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 20, right: 20, bottom: 20 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('90%')
|
||||||
|
.alignContent(Alignment.TopStart)
|
||||||
|
.backgroundColor(Color.White)
|
||||||
|
.borderRadius(24) // 较大的圆角
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { UIContext, ComponentContent } from '@kit.ArkUI';
|
||||||
|
|
||||||
|
// 1. 定义包裹组件 (保持不变)
|
||||||
|
@Component
|
||||||
|
struct ToastDeleteImage {
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_toast_delete_account'))
|
||||||
|
.width(91)
|
||||||
|
.height(91)
|
||||||
|
.objectFit(ImageFit.Contain)
|
||||||
|
}
|
||||||
|
.width(120)
|
||||||
|
.height(120)
|
||||||
|
.backgroundColor('#00000000')
|
||||||
|
.borderRadius(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 包装 Builder (跨模块调用的核心)
|
||||||
|
@Builder
|
||||||
|
function toastDeleteBuilder() {
|
||||||
|
ToastDeleteImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToastDeleteUtils {
|
||||||
|
private static content: ComponentContent<Object> | null = null;
|
||||||
|
|
||||||
|
static showDeleteToast(uiContext: UIContext, duration: number = 2000): void {
|
||||||
|
// 如果已有弹窗,先清理
|
||||||
|
if (ToastDeleteUtils.content) {
|
||||||
|
uiContext.getPromptAction().closeCustomDialog(ToastDeleteUtils.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 wrapBuilder 确保 UI 上下文正确
|
||||||
|
ToastDeleteUtils.content = new ComponentContent(uiContext, wrapBuilder(toastDeleteBuilder));
|
||||||
|
|
||||||
|
const prompt = uiContext.getPromptAction();
|
||||||
|
|
||||||
|
// 参数1: content 实例 参数2: 配置对象 (BaseDialogOptions)
|
||||||
|
prompt.openCustomDialog(ToastDeleteUtils.content, {
|
||||||
|
autoCancel: true,
|
||||||
|
alignment: DialogAlignment.Center,
|
||||||
|
maskColor: '#00000000'
|
||||||
|
}).then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ToastDeleteUtils.content) {
|
||||||
|
prompt.closeCustomDialog(ToastDeleteUtils.content);
|
||||||
|
ToastDeleteUtils.content = null;
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { UIContext, ComponentContent, promptAction } from '@kit.ArkUI';
|
||||||
|
import deviceInfo from '@ohos.deviceInfo';
|
||||||
|
|
||||||
|
interface ToastParams {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
// 1.定义包裹组件 (保持不变)
|
||||||
|
@Component
|
||||||
|
struct Toast {
|
||||||
|
@Prop message: string = '';
|
||||||
|
build() {
|
||||||
|
Stack(){
|
||||||
|
Text(this.message)
|
||||||
|
.fontSize(12)
|
||||||
|
.fontColor('#FFFFFF')
|
||||||
|
.textAlign(TextAlign.Center)
|
||||||
|
.margin(12)
|
||||||
|
}
|
||||||
|
.width(100)
|
||||||
|
.height(100)
|
||||||
|
.backgroundColor('#cc000000')
|
||||||
|
.borderRadius(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.包装 Builder (跨模块调用的核心)
|
||||||
|
@Builder
|
||||||
|
function toastBuilder(params: ToastParams) {
|
||||||
|
Toast({ message: params.message })
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToastUtils {
|
||||||
|
private static content: ComponentContent<ToastParams> | null = null;
|
||||||
|
|
||||||
|
static showToast(uiContext: UIContext, message: string, duration: number = 2000): void {
|
||||||
|
// 如果已有弹窗,先关闭旧的
|
||||||
|
if (ToastUtils.content) {
|
||||||
|
uiContext.getPromptAction().closeCustomDialog(ToastUtils.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.在创建 ComponentContent 时,第三个参数传入数据对象
|
||||||
|
const params: ToastParams = { message: message };
|
||||||
|
ToastUtils.content = new ComponentContent(uiContext, wrapBuilder(toastBuilder), params);
|
||||||
|
|
||||||
|
const prompt = uiContext.getPromptAction();
|
||||||
|
|
||||||
|
prompt.openCustomDialog(ToastUtils.content, {
|
||||||
|
autoCancel: true,
|
||||||
|
alignment: DialogAlignment.Center,
|
||||||
|
maskColor: '#00000000'
|
||||||
|
}).then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ToastUtils.content) {
|
||||||
|
prompt.closeCustomDialog(ToastUtils.content);
|
||||||
|
ToastUtils.content = null;
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static normalToast(options: ToastOptions): void {
|
||||||
|
// 设置默认值
|
||||||
|
const message = options.message;
|
||||||
|
const duration = options.duration ?? 2000;
|
||||||
|
const bottom = options.bottom ?? '80%'; // 系统的默认位置通常在底部
|
||||||
|
|
||||||
|
if (deviceInfo.sdkApiVersion < 18) {
|
||||||
|
promptAction.showToast({
|
||||||
|
message: message,
|
||||||
|
duration: duration,
|
||||||
|
bottom: bottom
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promptAction.openToast({
|
||||||
|
message: message,
|
||||||
|
duration: duration,
|
||||||
|
bottom: bottom
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToastOptions {
|
||||||
|
message: string;
|
||||||
|
duration?: number; // 可选
|
||||||
|
bottom?: string | number; // 可选
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
@CustomDialog
|
||||||
|
export struct UpdateTipsDialog {
|
||||||
|
// 接收外部传入的参数
|
||||||
|
title: string = '提示';
|
||||||
|
version: string = '';
|
||||||
|
desc: string = '';
|
||||||
|
confirmText: string = '确定';
|
||||||
|
cancelText: string = '取消';
|
||||||
|
|
||||||
|
// 是否强制更新
|
||||||
|
isForce: boolean = false;
|
||||||
|
|
||||||
|
controller: CustomDialogController;
|
||||||
|
cancel: () => void = () => {};
|
||||||
|
confirm: () => void = () => {};
|
||||||
|
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_dialog_tips_mask_top'))
|
||||||
|
.width('100%')
|
||||||
|
.opacity(0.5)
|
||||||
|
|
||||||
|
Column() {
|
||||||
|
// 标题
|
||||||
|
Text(this.title)
|
||||||
|
.fontSize(18)
|
||||||
|
.fontWeight(FontWeight.Bold)
|
||||||
|
.margin({ top: 30, bottom: 12 })
|
||||||
|
|
||||||
|
// 版本号
|
||||||
|
Text(this.version)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.margin({ bottom: 30 })
|
||||||
|
// 描述
|
||||||
|
Text(this.desc)
|
||||||
|
.fontSize(14)
|
||||||
|
.fontColor('#FF999999')
|
||||||
|
.lineHeight(14 * 1.5)
|
||||||
|
.margin({ bottom: 30 })
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 20, right: 20})
|
||||||
|
|
||||||
|
// 按钮区域
|
||||||
|
Row({ space: 20 }) {
|
||||||
|
if (!this.isForce) {
|
||||||
|
Button(this.cancelText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.controller.close();
|
||||||
|
this.cancel();
|
||||||
|
})
|
||||||
|
.backgroundColor('#FFFFFFFF')
|
||||||
|
.fontColor('#333333')
|
||||||
|
.layoutWeight(1)
|
||||||
|
.borderWidth(0.5)
|
||||||
|
.borderColor('#FF252525')
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(this.confirmText)
|
||||||
|
.onClick(() => {
|
||||||
|
this.confirm();
|
||||||
|
this.controller.close();
|
||||||
|
})
|
||||||
|
.backgroundColor(Color.Black) // 对应图片中的黑色按钮
|
||||||
|
.fontColor(Color.White)
|
||||||
|
.layoutWeight(1)
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.padding({ left: 20, right: 20, bottom: 20 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width('90%')
|
||||||
|
.alignContent(Alignment.TopStart)
|
||||||
|
.backgroundColor(Color.White)
|
||||||
|
.borderRadius(24) // 较大的圆角
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
import axios, {
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosResponse,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
AxiosError,
|
||||||
|
FormData,
|
||||||
|
AxiosRequestHeaders,
|
||||||
|
AxiosRequestConfig
|
||||||
|
} from '@ohos/axios';
|
||||||
|
import { DEBUG_BASE_URL, RELEASE_BASE_URL } from '../constants/AppConstants';
|
||||||
|
import { BaseResponse } from './BaseResponse';
|
||||||
|
import { HeaderInterceptor } from './HeaderInterceptor';
|
||||||
|
import { EncryptConfig, RequestInterceptor } from './RequestInterceptor';
|
||||||
|
import { ResponseInterceptor } from './ResponseInterceptor';
|
||||||
|
import { AppUtil } from '@pura/harmony-utils';
|
||||||
|
import { Logger } from '../utils/Logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模拟 Android 的 ApiManager
|
||||||
|
*/
|
||||||
|
export class ApiManager {
|
||||||
|
private static instance: ApiManager | null = null;
|
||||||
|
private retrofit: AxiosInstance | null = null;
|
||||||
|
private unsafeRetrofit: AxiosInstance | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ApiManager {
|
||||||
|
if (ApiManager.instance === null) {
|
||||||
|
ApiManager.instance = new ApiManager();
|
||||||
|
}
|
||||||
|
return ApiManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 集成的泛型请求方法(简单通用的普通请求)
|
||||||
|
* @param url 请求路径
|
||||||
|
* @param params GET请求的Query参数 (对应 Android 的 @Query)
|
||||||
|
* @param method 请求方法
|
||||||
|
* @param data 可选的 Body 数据
|
||||||
|
*/
|
||||||
|
public async request<T>(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, Object>,
|
||||||
|
method: string = 'get',
|
||||||
|
data?: Object
|
||||||
|
): Promise<BaseResponse<T>> {
|
||||||
|
const service = this.getUnsafeService();
|
||||||
|
|
||||||
|
const response: AxiosResponse<BaseResponse<T>> = await service.request({
|
||||||
|
url: url,
|
||||||
|
method: method,
|
||||||
|
params: params,
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 集成的泛型请求方法(特殊处理需要映射字段问题)
|
||||||
|
* @param url 请求路径
|
||||||
|
* @param params GET请求的Query参数 (对应 Android 的 @Query)
|
||||||
|
* @param mapper 可选转换函数
|
||||||
|
* @param method 请求方法
|
||||||
|
* @param data 可选的 Body 数据
|
||||||
|
*/
|
||||||
|
public async requestForMapper<T>(
|
||||||
|
url: string,
|
||||||
|
params?: Record<string, Object>,
|
||||||
|
mapper?: (raw: Record<string, Object>) => T,
|
||||||
|
method: string = 'get', // 新增:默认为 get
|
||||||
|
data?: Object // 新增:用于 POST 的 Body 数据
|
||||||
|
): Promise<T> {
|
||||||
|
const service = this.getUnsafeService();
|
||||||
|
|
||||||
|
// 1. 使用通用的 request 方法,根据参数自动切换 GET/POST
|
||||||
|
const response: AxiosResponse<BaseResponse<Object>> = await service.request({
|
||||||
|
url: url,
|
||||||
|
method: method, // 传入 'get' 或 'post'
|
||||||
|
params: params, // URL 上的参数(nonce, timestamp, signature 都在这)
|
||||||
|
data: data // Body 里的参数(phone 在这)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 提取业务数据 data 部分
|
||||||
|
const rawData = (response.data.data ?? {}) as Record<string, Object>;
|
||||||
|
|
||||||
|
// 3. 逻辑转换
|
||||||
|
if (mapper !== undefined) {
|
||||||
|
return mapper(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 强转逻辑
|
||||||
|
return rawData as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件上传方法
|
||||||
|
* @param url 请求路径
|
||||||
|
* @param formData FormData 实例
|
||||||
|
* @param params Query 参数 (用于签名)
|
||||||
|
*/
|
||||||
|
public async upload<T>(
|
||||||
|
url: string,
|
||||||
|
formData: FormData,
|
||||||
|
params?: Record<string, Object>
|
||||||
|
): Promise<T> {
|
||||||
|
const service = this.getUnsafeService();
|
||||||
|
|
||||||
|
// 构造配置对象,避免使用 any
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
params: params,
|
||||||
|
context: getContext(),
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
if (progressEvent.total) {
|
||||||
|
Logger.info(RequestInterceptor.TAG, `[上传进度] 已传: ${progressEvent.loaded} Bytes/ 总计: ${progressEvent.total} Bytes`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: AxiosResponse<BaseResponse<Object>> = await service.post(url, formData, config);
|
||||||
|
const rawData = (response.data.data ?? {}) as Record<string, Object>;
|
||||||
|
return rawData as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize(): void {
|
||||||
|
const isDebug: boolean = AppUtil.debug();
|
||||||
|
const baseUrl: string = isDebug ? DEBUG_BASE_URL : RELEASE_BASE_URL;
|
||||||
|
|
||||||
|
// 1. 初始化标准实例
|
||||||
|
if (this.retrofit === null) {
|
||||||
|
this.retrofit = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
// 基础请求拦截器
|
||||||
|
this.retrofit.interceptors.request.use(this.requestInterceptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 初始化不安全实例
|
||||||
|
if (this.unsafeRetrofit === null) {
|
||||||
|
this.unsafeRetrofit = axios.create({
|
||||||
|
baseURL: baseUrl,
|
||||||
|
timeout: 60000,
|
||||||
|
// 这里的 data 类型使用 Object, headers 使用 AxiosRequestHeaders
|
||||||
|
transformRequest: [(data: Object, headers: AxiosRequestHeaders): Object => {
|
||||||
|
// 1. 判断是否为 FormData
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
headers.set('Content-Type', 'multipart/form-data');
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 判断是否为普通对象 (非空)
|
||||||
|
if (data !== null && typeof data === 'object') {
|
||||||
|
headers.set('Content-Type', 'application/json');
|
||||||
|
return JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 其他情况原样返回 (转为 Object 兼容类型)
|
||||||
|
return data;
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- 添加请求拦截器链 (注意执行顺序:从下往上/链式触发) ---
|
||||||
|
|
||||||
|
// 添加 Header 拦截器 (注入 Token, 设备信息等)
|
||||||
|
this.unsafeRetrofit.interceptors.request.use(HeaderInterceptor);
|
||||||
|
|
||||||
|
// 添加 RequestInterceptor (处理签名、UUID、Nonce、MD5 日志)
|
||||||
|
// 注意:因为 onPrepare 是 async,Axios 会自动等待它执行完
|
||||||
|
this.unsafeRetrofit.interceptors.request.use(
|
||||||
|
(config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> => {
|
||||||
|
return RequestInterceptor.onPrepare(config as EncryptConfig);
|
||||||
|
},
|
||||||
|
(error: AxiosError): Promise<AxiosError> => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 添加响应拦截器链 ---
|
||||||
|
|
||||||
|
// 添加 ResponseInterceptor (处理解密、状态码修正、响应日志)
|
||||||
|
this.unsafeRetrofit.interceptors.response.use(
|
||||||
|
(response: AxiosResponse): Promise<AxiosResponse> => {
|
||||||
|
// 先执行解密逻辑
|
||||||
|
return ResponseInterceptor.responseHandler(response);
|
||||||
|
},
|
||||||
|
(error: AxiosError): Promise<AxiosError> => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 可以在这里继续添加统一的错误处理拦截器
|
||||||
|
this.unsafeRetrofit.interceptors.response.use(
|
||||||
|
(response: AxiosResponse): AxiosResponse => {
|
||||||
|
// 执行日志打印
|
||||||
|
return RequestInterceptor.onResponse(response);
|
||||||
|
},
|
||||||
|
this.responseErrorInterceptor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新初始化 (对应 Android 的 reinitialize)
|
||||||
|
*/
|
||||||
|
public reinitialize(): void {
|
||||||
|
this.retrofit = null;
|
||||||
|
this.unsafeRetrofit = null;
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 强类型拦截器实现 ---
|
||||||
|
|
||||||
|
private requestInterceptor(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
|
||||||
|
// 类似于 Android 的 RequestInterceptor
|
||||||
|
config.headers.set('Content-Type', 'application/json');
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private responseErrorInterceptor(error: AxiosError): Promise<AxiosError> {
|
||||||
|
// 错误处理逻辑
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 获取 Service 实例 ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取不安全/特殊配置的请求实例
|
||||||
|
* 在 ArkTS 中直接返回 AxiosInstance 进行调用
|
||||||
|
*/
|
||||||
|
public getUnsafeService(): AxiosInstance {
|
||||||
|
if (this.unsafeRetrofit === null) {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
return this.unsafeRetrofit as AxiosInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
// 返回结构:基础返回结构
|
||||||
|
export interface BaseResponse<T> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T;
|
||||||
|
encrypt: boolean; // 对应你 ResponseInterceptor 中的加密标识
|
||||||
|
}
|
||||||
|
// 返回结构:用于文件上传
|
||||||
|
export interface UploadResponse {
|
||||||
|
status: boolean;
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface RequestCallback<T = Object> {
|
||||||
|
onSuccess?: (data: T) => void; // 修改这里:接收一个参数
|
||||||
|
onFailure?: (message:string) => void;
|
||||||
|
onError?: (err: Error) => void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { InternalAxiosRequestConfig } from '@ohos/axios';
|
||||||
|
import { bundleManager } from '@kit.AbilityKit';
|
||||||
|
import deviceInfo from '@ohos.deviceInfo';
|
||||||
|
import { KVStore } from '../utils/KVStore';
|
||||||
|
import { APP_ID, PLATFORM, TOKEN } from '../constants/AppConstants';
|
||||||
|
import { Logger } from '../utils/Logger';
|
||||||
|
import { DeviceUtil } from '@pura/harmony-utils';
|
||||||
|
import { CHANNEL } from '../../../../BuildProfile';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对应 Android 的 HeaderInterceptor
|
||||||
|
*/
|
||||||
|
export const HeaderInterceptor = async (config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> => {
|
||||||
|
try {
|
||||||
|
// 1. 获取应用信息 (对应 BuildConfig/getAppVersionName)
|
||||||
|
const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
|
||||||
|
const versionName = bundleInfo.versionName;
|
||||||
|
const bundleName = bundleInfo.name;
|
||||||
|
|
||||||
|
// 2. 获取 Header 所需参数
|
||||||
|
const accessToken: string = KVStore.getInstance().getSync(TOKEN, '');
|
||||||
|
const deviceId: string = DeviceUtil.getDeviceId();
|
||||||
|
const brand: string = deviceInfo.brand;
|
||||||
|
const model: string = deviceInfo.productModel;
|
||||||
|
|
||||||
|
// 3. 注入 Header (对应 mBuilder.header)
|
||||||
|
config.headers.set('x-token', accessToken);
|
||||||
|
config.headers.set('x-version', versionName);
|
||||||
|
config.headers.set('x-platform', PLATFORM); // 鸿蒙平台标识(目前只有android和ios,暂不支持harmony)
|
||||||
|
config.headers.set('x-device-id', deviceId);
|
||||||
|
config.headers.set('x-mobile-brand', brand);
|
||||||
|
config.headers.set('x-mobile-model', model);
|
||||||
|
config.headers.set('x-package', bundleName);
|
||||||
|
config.headers.set('x-base-version', versionName);
|
||||||
|
config.headers.set('x-app-id', APP_ID);
|
||||||
|
config.headers.set('x-channel', CHANNEL);
|
||||||
|
|
||||||
|
// 4. 打印日志 (对应 Android 的 StringBuilder 日志)
|
||||||
|
let logString = "\n-------------> 📤 header start<-------------\n";
|
||||||
|
logString += `│ x-token = ${accessToken}\n`;
|
||||||
|
logString += `│ x-version = ${versionName}\n`;
|
||||||
|
logString += `│ x-platform = ${PLATFORM}\n`;
|
||||||
|
logString += `│ x-device-id = ${deviceId}\n`;
|
||||||
|
logString += `│ x-mobile-brand = ${brand}\n`;
|
||||||
|
logString += `│ x-mobile-model = ${model}\n`;
|
||||||
|
logString += `│ x-package = ${bundleName}\n`;
|
||||||
|
logString += `│ x-base-version = ${versionName}\n`;
|
||||||
|
logString += `│ x-app-id = ${APP_ID}\n`;
|
||||||
|
logString += `│ x-channel = ${CHANNEL}\n`;
|
||||||
|
logString += "-------------> header end <-------------";
|
||||||
|
|
||||||
|
Logger.info('RabbitLog', logString);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// 对应 Android 的 catch (e: Exception)
|
||||||
|
console.error(`HeaderInterceptor Error: ${JSON.stringify(error)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { InternalAxiosRequestConfig, AxiosResponse, FormData } from '@ohos/axios';
|
||||||
|
import { Signature } from '../constants/AppConstants';
|
||||||
|
import { Logger } from '../utils/Logger';
|
||||||
|
import { RandomUtil, MD5, JSONUtil, StrUtil } from '@pura/harmony-utils';
|
||||||
|
import systemDateTime from '@ohos.systemDateTime';
|
||||||
|
|
||||||
|
export interface EncryptConfig extends InternalAxiosRequestConfig {
|
||||||
|
startTime?: number;
|
||||||
|
params?: Record<string, string | number | boolean | null | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RequestInterceptor {
|
||||||
|
static readonly TAG: string = "RabbitLog_Request";
|
||||||
|
|
||||||
|
static async onPrepare(config: EncryptConfig): Promise<EncryptConfig> {
|
||||||
|
config.startTime = Date.now();
|
||||||
|
|
||||||
|
// 1. 获取请求方法
|
||||||
|
const method = (config.method ?? "get").toLowerCase();
|
||||||
|
|
||||||
|
// 2. 准备基础参数 (Query Params)
|
||||||
|
config.params = config.params || {};
|
||||||
|
config.params.nonce = RandomUtil.generateUUID36()
|
||||||
|
config.params.timestamp = Math.trunc(systemDateTime.getTime() / 1000)
|
||||||
|
|
||||||
|
// 3. 对 Query 参数进行字典排序并拼接
|
||||||
|
let paramsMap = JSONUtil.jsonToMap(JSON.stringify(config.params));
|
||||||
|
let arrayMap = Array.from(paramsMap);
|
||||||
|
arrayMap.sort((a, b) => {
|
||||||
|
return a[0].localeCompare(b[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
paramsMap = new Map(arrayMap)
|
||||||
|
let sortQueryString = "";
|
||||||
|
paramsMap.forEach((value, key) => {
|
||||||
|
sortQueryString += key + "=" + value + "&"
|
||||||
|
})
|
||||||
|
sortQueryString = encodeURI(sortQueryString.substring(0, sortQueryString.length - 1))
|
||||||
|
|
||||||
|
// 4. 准备签名盐值
|
||||||
|
let signature = MD5.digestSync(sortQueryString + '&' + MD5.digestSync(Signature));
|
||||||
|
|
||||||
|
// 5. 【核心判断】识别是否为文件上传
|
||||||
|
const contentType = String(config.headers?.['Content-Type'] || "");
|
||||||
|
const isFormData = config.data instanceof FormData || contentType.includes('multipart/form-data');
|
||||||
|
|
||||||
|
// 6. 根据请求类型和内容计算签名
|
||||||
|
if ((method === 'post' || method === 'put') && config.data) {
|
||||||
|
if(!isFormData){
|
||||||
|
let dataStr = JSON.stringify(config.data);
|
||||||
|
if (StrUtil.isNotEmpty(dataStr)) {
|
||||||
|
signature = MD5.digestSync(sortQueryString + '&' + dataStr + "&" + MD5.digestSync(Signature));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 回填带签名的参数
|
||||||
|
config.params.signature = signature;
|
||||||
|
|
||||||
|
// 8. 打印请求日志
|
||||||
|
RequestInterceptor.logRequest(config, isFormData);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static onResponse(response: AxiosResponse): AxiosResponse {
|
||||||
|
const config = response.config as EncryptConfig;
|
||||||
|
const startTime: number = config.startTime ?? Date.now();
|
||||||
|
RequestInterceptor.logResponse(response, startTime);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static logRequest(config: EncryptConfig, isFormData: boolean): void {
|
||||||
|
let logString = `\n┌─── 📤 Request [ ${config.method?.toUpperCase()} ] ───\n`;
|
||||||
|
logString += `│ Url: ${config.baseURL}${config.url}\n`;
|
||||||
|
logString += `│ Final Params: ${JSON.stringify(config.params)}\n`;
|
||||||
|
|
||||||
|
if (isFormData) {
|
||||||
|
logString += `│ Body: [ Multipart/FormData - File Upload ]\n`;
|
||||||
|
} else {
|
||||||
|
logString += `│ Body: ${JSON.stringify(config.data)}\n`;
|
||||||
|
}
|
||||||
|
logString += `└─────────────────────────────────────\n`;
|
||||||
|
Logger.info(RequestInterceptor.TAG, logString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static logResponse(response: AxiosResponse, startTime: number): void {
|
||||||
|
const duration: number = Date.now() - startTime;
|
||||||
|
const config = response.config as EncryptConfig;
|
||||||
|
|
||||||
|
// 0. 完整的请求地址(包含参数拼接)
|
||||||
|
let fullUrl = `${config.baseURL ?? ""}${config.url ?? ""}`;
|
||||||
|
const params = config.params;
|
||||||
|
let queryStr = "";
|
||||||
|
if (params !== undefined && params !== null) {
|
||||||
|
const keys = Object.keys(params);
|
||||||
|
if (keys.length > 0) {
|
||||||
|
const queryParts: string[] = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = params[key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queryStr = queryParts.join('&');
|
||||||
|
const separator = fullUrl.includes('?') ? '&' : '?';
|
||||||
|
fullUrl += `${separator}${queryStr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 获取完整的 Data 字符串
|
||||||
|
const dataStr: string = JSON.stringify(response.data);
|
||||||
|
|
||||||
|
// 2. 构造基础信息
|
||||||
|
let header ='';
|
||||||
|
header =`\n┌─── 📥 响应开始🔜Response [ ${response.status} ] [ ${duration}ms ] ───\n`;
|
||||||
|
header += `│ Full URL: ${fullUrl}\n`;
|
||||||
|
header += `│ Data Content: `;
|
||||||
|
|
||||||
|
// 3. 拼接完整报文
|
||||||
|
const fullLog = header + dataStr + `\n└────────────────────响应结束🔚─────────────────`;
|
||||||
|
|
||||||
|
// 4. 执行分段连贯打印
|
||||||
|
RequestInterceptor.printInChunks(fullLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分段打印并确保数据完全打印
|
||||||
|
*/
|
||||||
|
private static printInChunks(message: string): void {
|
||||||
|
const CHUNK_SIZE = 3000;
|
||||||
|
let start = 0;
|
||||||
|
|
||||||
|
while (start < message.length) {
|
||||||
|
let end = Math.min(start + CHUNK_SIZE, message.length);
|
||||||
|
let chunk = message.substring(start, end);
|
||||||
|
Logger.info(RequestInterceptor.TAG, chunk);
|
||||||
|
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
private static logResponse(response: AxiosResponse, startTime: number): void {
|
||||||
|
const duration: number = Date.now() - startTime;
|
||||||
|
const config = response.config as EncryptConfig; // 显式类型转换
|
||||||
|
|
||||||
|
// 1. 安全地获取 BaseURL 和 URL
|
||||||
|
const baseUrl: string = config.baseURL ?? "";
|
||||||
|
let urlPath: string = config.url ?? "";
|
||||||
|
let fullUrl: string = `${baseUrl}${urlPath}`;
|
||||||
|
|
||||||
|
// 2. 强类型处理 Query Params (对应 Android 的 request.url().toString())
|
||||||
|
const params = config.params;
|
||||||
|
if (params !== undefined && params !== null) {
|
||||||
|
const keys: string[] = Object.keys(params);
|
||||||
|
if (keys.length > 0) {
|
||||||
|
const queryParts: string[] = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = params[key];
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const queryString: string = queryParts.join('&');
|
||||||
|
const separator: string = fullUrl.includes('?') ? '&' : '?';
|
||||||
|
fullUrl += `${separator}${queryString}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 构造日志字符串
|
||||||
|
let logString =''
|
||||||
|
logString =`\n┌─── 📥 Response [ ${String(response.status)} ] ───\n`;
|
||||||
|
logString += `│ Time: ${String(duration)}ms\n`;
|
||||||
|
logString += `│ Full Request URL: ${fullUrl}\n`;
|
||||||
|
|
||||||
|
// 4. 处理 Response Data (禁止使用 any,需转换为 string)
|
||||||
|
const dataObj = response.data as object; // 假设返回的是对象
|
||||||
|
const dataStr: string = JSON.stringify(dataObj);
|
||||||
|
const displayData: string = dataStr.length > 1000
|
||||||
|
? `${dataStr.substring(0, 1000)}... [Truncated]`
|
||||||
|
: dataStr;
|
||||||
|
|
||||||
|
logString += `│ Data: ${displayData}\n`;
|
||||||
|
logString += `└─────────────────────────────────────\n`;
|
||||||
|
|
||||||
|
Logger.info(RequestInterceptor.TAG, logString);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间同步工具
|
||||||
|
*/
|
||||||
|
export class TimeSync {
|
||||||
|
static timeOffset: number = 0; // 服务器时间 - 本地时间
|
||||||
|
|
||||||
|
static async sync(serverTimeStr: string) {
|
||||||
|
const serverTime = parseInt(serverTimeStr, 10);
|
||||||
|
const localTime = Math.trunc(Date.now() / 1000);
|
||||||
|
TimeSync.timeOffset = serverTime - localTime;
|
||||||
|
console.info(`[TimeSync] Offset calculated: ${TimeSync.timeOffset}s`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCorrectedTime(): number {
|
||||||
|
// return Math.trunc(Date.now() / 1000) + TimeSync.timeOffset;
|
||||||
|
return Math.trunc(systemDateTime.getTime() / 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { AxiosResponse } from '@ohos/axios';
|
||||||
|
import { AESDecrypt, DEBUG_BASE_URL, RELEASE_BASE_URL } from '../constants/AppConstants';
|
||||||
|
import { CryptoHelper, CryptoUtil, AES, StrUtil } from '@pura/harmony-utils';
|
||||||
|
|
||||||
|
export class ResponseInterceptor {
|
||||||
|
private static readonly TAG: string = "RabbitLog_Response";
|
||||||
|
|
||||||
|
static async responseHandler(response: AxiosResponse): Promise<AxiosResponse> {
|
||||||
|
const url: string = response.config.baseURL ?? "";
|
||||||
|
|
||||||
|
const isDebug: boolean = true;
|
||||||
|
const baseUrl: string = isDebug ? DEBUG_BASE_URL : RELEASE_BASE_URL;
|
||||||
|
|
||||||
|
// 1. 过滤非业务接口
|
||||||
|
if (!url.startsWith(baseUrl)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
// 2. 检查 Content-Type
|
||||||
|
let contentType = response.headers['content-type'] as string;
|
||||||
|
let dataString: string = '';
|
||||||
|
// 3. 解析响应体 (Axios 默认已将 data 转为对象)
|
||||||
|
if (StrUtil.isNotEmpty(contentType) && contentType?.includes("application/json") && response.data !== null) {
|
||||||
|
let isEncrypt = response.data['encrypt'] as boolean
|
||||||
|
dataString = response.data['data'] as string;
|
||||||
|
if (isEncrypt) {
|
||||||
|
console.info(`[${ResponseInterceptor.TAG}] 是否需要解密: 需要`);
|
||||||
|
dataString = ResponseInterceptor.decryptNormal(dataString, AESDecrypt);
|
||||||
|
} else {
|
||||||
|
console.info(`[${ResponseInterceptor.TAG}] 是否需要解密: 不需要`);
|
||||||
|
let newResponse: Record<string, Object> = {
|
||||||
|
'code': 0,
|
||||||
|
'data': dataString
|
||||||
|
}
|
||||||
|
dataString = JSON.stringify(newResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.data = JSON.parse(dataString);
|
||||||
|
|
||||||
|
// 兼容某些接口 400 情况
|
||||||
|
if (response.status === 400) {
|
||||||
|
response.status = 200;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
const err = e as Error;
|
||||||
|
console.error(`[${ResponseInterceptor.TAG}] 解密失败: ${err.message}`);
|
||||||
|
return Promise.reject(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decryptNormal(encryptStr: string, key: string): string {
|
||||||
|
let dataBlob = CryptoHelper.strToDataBlob(encryptStr, 'base64');
|
||||||
|
let keyBytes = CryptoUtil.getConvertSymKeySync('AES256', key, 'utf-8')
|
||||||
|
let ivParams= CryptoUtil.getIvParamsSpec(key.substring(0, 16), 'utf-8')
|
||||||
|
let plain = AES.decryptCBCSync(dataBlob, keyBytes, ivParams);
|
||||||
|
return StrUtil.unit8ArrayToStr(plain.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { USER_INFO, USER_PROFILE, LOGIN_PROFILE, TOKEN_LOGIN, TOKEN } from "../constants/AppConstants";
|
||||||
|
import { KVStore } from "../utils/KVStore";
|
||||||
|
import { router } from "@kit.ArkUI";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注意:涉及Router的只有启动页(SplashScreenPage.ets)和登录页(LoginPage.ets),其他页面请使用Navigator进行跳转
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function routerLogin(fromSource: string, isClear: boolean) {
|
||||||
|
if (isClear) {
|
||||||
|
KVStore.getInstance().put(USER_INFO, '');
|
||||||
|
KVStore.getInstance().put(USER_PROFILE, '')
|
||||||
|
KVStore.getInstance().put(LOGIN_PROFILE, ''); //持久化登录信息
|
||||||
|
KVStore.getInstance().put(TOKEN_LOGIN, ''); //持久化token
|
||||||
|
KVStore.getInstance().put(TOKEN, ''); //持久化token
|
||||||
|
}
|
||||||
|
|
||||||
|
//回到登录页(登录页将会清理router栈和NavPathStack栈)
|
||||||
|
router.pushUrl({
|
||||||
|
url: 'pages/LoginPage',
|
||||||
|
params: {
|
||||||
|
from: fromSource
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export function routerIndex() {
|
||||||
|
router.replaceUrl({ url: 'pages/Index' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { bundleManager, common, Want } from '@kit.AbilityKit';
|
||||||
|
import { PermissionUtil, StrUtil } from '@pura/harmony-utils';
|
||||||
|
import { identifier } from '@kit.AdsKit';
|
||||||
|
import { ToastUtils } from '../dialog/ToastUtils';
|
||||||
|
|
||||||
|
export async function openAppStore(context: common.UIAbilityContext) {
|
||||||
|
const bundleName = bundleManager.getBundleInfoForSelfSync(
|
||||||
|
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
|
||||||
|
).name;
|
||||||
|
try {
|
||||||
|
// 1. 尝试直接拉起应用市场客户端
|
||||||
|
const marketWant: Want = {
|
||||||
|
action: 'ohos.want.action.appdetail',
|
||||||
|
uri: `store://appgallery.huawei.com/app/detail?id=${bundleName}` // 替换为实际包名
|
||||||
|
};
|
||||||
|
await context.startAbility(marketWant);
|
||||||
|
} catch (err) {
|
||||||
|
// 2. 彻底失败,提示手动更新
|
||||||
|
ToastUtils.normalToast({message:'无法跳转,请手动打开应用市场搜索更新'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOAID(): Promise<string>{
|
||||||
|
try {
|
||||||
|
const isGranted = await PermissionUtil.checkRequestPermissions('ohos.permission.APP_TRACKING_CONSENT');
|
||||||
|
if(isGranted){
|
||||||
|
const oaid = await identifier.getOAID();
|
||||||
|
if(StrUtil.isNotEmpty(oaid)) {
|
||||||
|
return Promise.resolve(oaid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
return Promise.resolve('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAppVersion() {
|
||||||
|
try {
|
||||||
|
// 获取应用自身的 BundleInfo
|
||||||
|
// BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION 表示获取应用的基本信息
|
||||||
|
let bundleInfo = await bundleManager.getBundleInfoForSelf(
|
||||||
|
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
|
||||||
|
);
|
||||||
|
|
||||||
|
let versionName = bundleInfo.versionName; // 对应 app.json5 中的 "versionName" (如 1.0.0)
|
||||||
|
let versionCode = bundleInfo.versionCode; // 对应 app.json5 中的 "versionCode" (如 1000001)
|
||||||
|
|
||||||
|
console.info(`版本名称: ${versionName}, 版本代码: ${versionCode}`);
|
||||||
|
return versionName;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取版本信息失败: ' + JSON.stringify(err));
|
||||||
|
return '未知版本';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { mediaquery } from '@kit.ArkUI';
|
||||||
|
import { commonConstants } from '../constants/CommonConstants';
|
||||||
|
|
||||||
|
declare interface BreakPointTypeOption<T> extends Record<string, T | undefined> {
|
||||||
|
sm?: T;
|
||||||
|
md?: T;
|
||||||
|
lg?: T;
|
||||||
|
xl?: T;
|
||||||
|
xxl?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BreakPointType<T> {
|
||||||
|
options: BreakPointTypeOption<T>;
|
||||||
|
|
||||||
|
constructor(option: BreakPointTypeOption<T>) {
|
||||||
|
this.options = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(currentBreakPoint: string): T {
|
||||||
|
return this.options[currentBreakPoint] as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Breakpoint {
|
||||||
|
name: string = '';
|
||||||
|
size: number = 0;
|
||||||
|
mediaQueryListener?: mediaquery.MediaQueryListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BreakpointSystem {
|
||||||
|
private currentBreakpoint: string = commonConstants.breakpointsInitializeName;
|
||||||
|
private readonly breakpointId: string = commonConstants.breakpointIdInitializeName;
|
||||||
|
private readonly breakpoints: Breakpoint[] = [
|
||||||
|
{ name: commonConstants.breakpointsSmName, size: commonConstants.breakpointsSmSize },
|
||||||
|
{ name: commonConstants.breakpointsMdName, size: commonConstants.breakpointsMdSize },
|
||||||
|
{ name: commonConstants.breakpointsLgName, size: commonConstants.breakpointsLgSize },
|
||||||
|
{ name: commonConstants.breakpointsXlName, size: commonConstants.breakpointsXlSize }
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(breakpointId: string) {
|
||||||
|
this.breakpointId = breakpointId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public register(uiContext: UIContext): void {
|
||||||
|
this.breakpoints.forEach((breakpoint: Breakpoint, index: number) => {
|
||||||
|
let condition: string = '';
|
||||||
|
if (index === this.breakpoints.length - 1) {
|
||||||
|
condition = `(${breakpoint.size}vp<=width)`;
|
||||||
|
} else {
|
||||||
|
condition = `(${breakpoint.size}vp<=width<${this.breakpoints[index + 1].size}vp)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
breakpoint.mediaQueryListener = uiContext.getMediaQuery().matchMediaSync(condition);
|
||||||
|
breakpoint.mediaQueryListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => {
|
||||||
|
if (mediaQueryResult.matches) {
|
||||||
|
this.updateCurrentBreakpoint(breakpoint.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public unregister(): void {
|
||||||
|
this.breakpoints.forEach((breakpoint: Breakpoint) => {
|
||||||
|
breakpoint.mediaQueryListener?.off('change');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCurrentBreakpoint(breakpoint: string): void {
|
||||||
|
if (this.currentBreakpoint !== breakpoint) {
|
||||||
|
this.currentBreakpoint = breakpoint;
|
||||||
|
AppStorage.set<string>(this.breakpointId, this.currentBreakpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { request } from '@kit.BasicServicesKit';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
import { fileIo as fs } from '@kit.CoreFileKit';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件下载
|
||||||
|
* @param callback
|
||||||
|
* status: 0-默认, 1-进行中(progress为百分比), 2-成功(progress为0, path为路径), -1-失败
|
||||||
|
*/
|
||||||
|
export async function downloadFile(
|
||||||
|
context: Context,
|
||||||
|
url: string,
|
||||||
|
fileName: string,
|
||||||
|
callback: (status: number, progress: number, path?: string) => void
|
||||||
|
) {
|
||||||
|
const filePath = `${context.filesDir}/${fileName}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// --- 清理旧文件逻辑 ---
|
||||||
|
if (fs.accessSync(filePath)) {
|
||||||
|
console.info(`检测到旧文件,正在删除: ${filePath}`);
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`删除旧文件失败: ${(e as BusinessError).message}`);
|
||||||
|
// 即使删除失败(如文件被占用),通常也可以继续,因为下载配置了 overwrite: true
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: request.agent.Config = {
|
||||||
|
action: request.agent.Action.DOWNLOAD,
|
||||||
|
url: url,
|
||||||
|
saveas: filePath,
|
||||||
|
mode: request.agent.Mode.FOREGROUND,
|
||||||
|
overwrite: true // 即使删除逻辑失败,此参数也会确保覆盖
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const task = await request.agent.create(context, config);
|
||||||
|
|
||||||
|
task.on('progress', (progress) => {
|
||||||
|
if (progress.sizes && progress.sizes.length > 0 && progress.sizes[0] > 0) {
|
||||||
|
const percent = Math.floor((progress.processed / progress.sizes[0]) * 100);
|
||||||
|
callback(1, percent);
|
||||||
|
} else {
|
||||||
|
callback(1, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
task.on('completed', () => {
|
||||||
|
callback(2, 100, filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
task.on('failed', (info) => {
|
||||||
|
console.error(`任务失败详情: ${JSON.stringify(info)}`);
|
||||||
|
callback(-1, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
await task.start();
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as BusinessError;
|
||||||
|
console.error(`创建任务失败: ${err.code} ${err.message}`);
|
||||||
|
callback(-1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,410 @@
|
||||||
|
import { image } from '@kit.ImageKit';
|
||||||
|
import { fileIo as fs } from '@kit.CoreFileKit';
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { BusinessError } from '@ohos.base';
|
||||||
|
import { util } from '@kit.ArkTS';
|
||||||
|
import { PickerUtil } from '@pura/picker_utils';
|
||||||
|
import componentSnapshot from '@ohos.arkui.componentSnapshot';
|
||||||
|
import { promptAction } from '@kit.ArkUI';
|
||||||
|
import { photoAccessHelper } from '@kit.MediaLibraryKit';
|
||||||
|
import { subjectSegmentation } from '@kit.CoreVisionKit';
|
||||||
|
import { StickerItem } from '../viewmodel/LocalBean';
|
||||||
|
import buffer from '@ohos.buffer';
|
||||||
|
import fileIo from '@ohos.file.fs';
|
||||||
|
import { ToastUtils } from '../dialog/ToastUtils';
|
||||||
|
|
||||||
|
|
||||||
|
export class ImageUtils {
|
||||||
|
private static readonly TAG: string = 'ImageUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 压缩图片并保存到沙箱缓存目录
|
||||||
|
* @param context 上下文
|
||||||
|
* @param uri 原始图片URI (如 datashare:// 或 file://)
|
||||||
|
* @param quality 压缩质量 (0-100),默认 80
|
||||||
|
* @returns 压缩后的沙箱文件路径
|
||||||
|
*/
|
||||||
|
static async compressImage(context: common.Context, uri: string, quality: number = 80): Promise<string> {
|
||||||
|
let file: fs.File | undefined;
|
||||||
|
try {
|
||||||
|
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||||||
|
const packingOptions: image.PackingOption = {
|
||||||
|
format: 'image/jpeg',
|
||||||
|
quality: quality
|
||||||
|
};
|
||||||
|
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||||||
|
const arrayBuffer: ArrayBuffer = await imagePacker.packing(imageSource, packingOptions);
|
||||||
|
const fileName = `compress_${Date.now()}.jpg`;
|
||||||
|
const cachePath = `${context.cacheDir}/${fileName}`;
|
||||||
|
const cacheFile = fs.openSync(cachePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||||||
|
fs.writeSync(cacheFile.fd, arrayBuffer);
|
||||||
|
fs.closeSync(cacheFile);
|
||||||
|
|
||||||
|
await imagePacker.release();
|
||||||
|
await imageSource.release();
|
||||||
|
|
||||||
|
console.info(ImageUtils.TAG, `图片压缩成功: ${cachePath}`);
|
||||||
|
return `file://${cachePath}`; // 返回符合上传要求的 file 协议路径
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as BusinessError;
|
||||||
|
console.error(ImageUtils.TAG, `压缩失败: ${err.code} ${err.message}`);
|
||||||
|
return uri; // 如果压缩失败,返回原路径尝试直接上传
|
||||||
|
} finally {
|
||||||
|
if (file) {
|
||||||
|
fs.closeSync(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片 URI 转换为 Base64 字符串
|
||||||
|
* @param uri 相册选择的 datashare:// 或沙箱路径
|
||||||
|
*/
|
||||||
|
static async base64Image(uri: string): Promise<string> {
|
||||||
|
const context = getContext();
|
||||||
|
// 使用临时路径存储拷贝的文件
|
||||||
|
const tempFileName = `${Date.now()}.jpg`;
|
||||||
|
const destPath = `${context.cacheDir}/${tempFileName}`;
|
||||||
|
|
||||||
|
let srcFile: fs.File | null = null;
|
||||||
|
try {
|
||||||
|
srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
fs.copyFileSync(srcFile.fd, destPath);
|
||||||
|
const base64Data = await ImageUtils.fileToBase64(destPath);
|
||||||
|
fs.unlinkSync(destPath);
|
||||||
|
|
||||||
|
return base64Data;
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as BusinessError;
|
||||||
|
console.error(`base64Image 转换失败: ${error.message}`);
|
||||||
|
return "";
|
||||||
|
} finally {
|
||||||
|
if (srcFile) {
|
||||||
|
fs.closeSync(srcFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async pixelMapToBase64(pixelMap: image.PixelMap): Promise<string> {
|
||||||
|
const imagePackerApi = image.createImagePacker();
|
||||||
|
const packOptions: image.PackingOption = { format: 'image/png', quality: 100 };
|
||||||
|
const data = await imagePackerApi.packing(pixelMap, packOptions);
|
||||||
|
|
||||||
|
let base64Str = buffer.from(data).toString('base64');
|
||||||
|
return `data:image/png;base64,${base64Str}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部方法:读取文件转 Base64
|
||||||
|
*/
|
||||||
|
private static async fileToBase64(filePath: string): Promise<string> {
|
||||||
|
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
|
||||||
|
const stat = fs.statSync(file.fd);
|
||||||
|
const buffer = new ArrayBuffer(stat.size);
|
||||||
|
fs.readSync(file.fd, buffer);
|
||||||
|
fs.closeSync(file);
|
||||||
|
|
||||||
|
const helper = new util.Base64Helper();
|
||||||
|
return helper.encodeToStringSync(new Uint8Array(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存图片到指定路径(用户自己选择)
|
||||||
|
* @param pixelMap
|
||||||
|
*/
|
||||||
|
static async savePixelMapToGallery(pixelMap: image.PixelMap) {
|
||||||
|
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||||||
|
const packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.jpg`]);
|
||||||
|
|
||||||
|
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||||||
|
|
||||||
|
if (!targetUri) {
|
||||||
|
console.warn('用户取消保存或路径获取失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packOpts) as ArrayBuffer;
|
||||||
|
const file = fs.openSync(targetUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||||||
|
fs.writeSync(file.fd, buffer);
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
|
||||||
|
console.info('保存成功!路径: ' + targetUri);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('保存过程中出错: ' + JSON.stringify(err));
|
||||||
|
} finally {
|
||||||
|
imagePacker.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片(image.PixelMap)转成jpg、png、webp等
|
||||||
|
* 保存图片到指定路径(用户自己选择)
|
||||||
|
* @param pixelMap
|
||||||
|
* @param format 支持: 'image/jpeg', 'image/png', 'image/webp'
|
||||||
|
*/
|
||||||
|
static async saveFormatToGallery(pixelMap: image.PixelMap, format: string) {
|
||||||
|
const imagePacker: image.ImagePacker = image.createImagePacker();
|
||||||
|
const packOpts: image.PackingOption = { format: format, quality: 100 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.${format === 'image/jpeg' ? 'jpg' : format.substring(6)}`]);
|
||||||
|
|
||||||
|
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||||||
|
|
||||||
|
if (!targetUri) {
|
||||||
|
console.warn('用户取消保存或路径获取失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packOpts) as ArrayBuffer;
|
||||||
|
const file = fs.openSync(targetUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||||||
|
fs.writeSync(file.fd, buffer);
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
|
||||||
|
|
||||||
|
ToastUtils.normalToast({ message: '图片已保存' });
|
||||||
|
} catch (err) {
|
||||||
|
ToastUtils.normalToast({ message: '保存失败' });
|
||||||
|
} finally {
|
||||||
|
imagePacker.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片(image.PixelMap)转成SVG
|
||||||
|
* @param pixelMap
|
||||||
|
*/
|
||||||
|
static async saveSvgToGallery(pixelMap: image.PixelMap) {
|
||||||
|
let imagePackerApi = image.createImagePacker();
|
||||||
|
const info = await pixelMap.getImageInfo();
|
||||||
|
const width = info.size.width;
|
||||||
|
const height = info.size.height;
|
||||||
|
|
||||||
|
const uris = await PickerUtil.savePhoto([`Rabbit_IMG_${Date.now()}.svg`]);
|
||||||
|
const targetUri: string = Array.isArray(uris) ? uris[0] : uris;
|
||||||
|
if (!targetUri) {
|
||||||
|
console.warn('用户取消保存或路径获取失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packOptions: image.PackingOption = {
|
||||||
|
format: 'image/png',
|
||||||
|
quality: 100
|
||||||
|
};
|
||||||
|
const arrayBuffer = await imagePackerApi.packing(pixelMap, packOptions);
|
||||||
|
|
||||||
|
// 将二进制数据转为 Base64 字符串,buffer.from().toString('base64') 默认不换行的 (等同于 NO_WRAP)
|
||||||
|
const base64Data = buffer.from(arrayBuffer).toString('base64');
|
||||||
|
|
||||||
|
// 构建 SVG 内容
|
||||||
|
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<image width="${width}" height="${height}"
|
||||||
|
xlink:href="data:image/png;base64,${base64Data}"
|
||||||
|
href="data:image/png;base64,${base64Data}" />
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
let file = fileIo.openSync(targetUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC);
|
||||||
|
fileIo.writeSync(file.fd, svgContent);
|
||||||
|
fileIo.closeSync(file);
|
||||||
|
ToastUtils.normalToast({ message: '图片已保存' });
|
||||||
|
} catch (err) {
|
||||||
|
ToastUtils.normalToast({ message: '保存失败' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存图片(通过布局ID保存图片)
|
||||||
|
* 注意:id为控件的ID
|
||||||
|
*/
|
||||||
|
static async saveImageForID(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
let pixelMap = await componentSnapshot.get(id);
|
||||||
|
|
||||||
|
if (pixelMap) {
|
||||||
|
await ImageUtils.savePixelMapToGallery(pixelMap);
|
||||||
|
ToastUtils.normalToast({ message: '图片已保存' });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`快照捕获失败: ${JSON.stringify(err)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开图库(仅可选单张图片)
|
||||||
|
*/
|
||||||
|
static async openGallery(): Promise<StickerItem|null> {
|
||||||
|
let file: fs.File | null = null;
|
||||||
|
try {
|
||||||
|
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||||||
|
const options = new photoAccessHelper.PhotoSelectOptions();
|
||||||
|
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||||
|
options.maxSelectNumber = 1;
|
||||||
|
|
||||||
|
const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options);
|
||||||
|
if (result.photoUris.length === 0) return null;
|
||||||
|
|
||||||
|
const uri: string = result.photoUris[0];
|
||||||
|
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||||||
|
const pixelMap: image.PixelMap = await imageSource.createPixelMap();
|
||||||
|
let newItem = new StickerItem(pixelMap,0);
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
} catch (err) {
|
||||||
|
ToastUtils.normalToast({ message: '获取图片失败!' });
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (file) fs.closeSync(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开图库(仅可选单张图片-抠图)
|
||||||
|
*/
|
||||||
|
static async openCutoutGallery(): Promise<StickerItem|null> {
|
||||||
|
let file: fs.File | null = null;
|
||||||
|
try {
|
||||||
|
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||||||
|
const options = new photoAccessHelper.PhotoSelectOptions();
|
||||||
|
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||||
|
options.maxSelectNumber = 1;
|
||||||
|
|
||||||
|
const result: photoAccessHelper.PhotoSelectResult = await photoPicker.select(options);
|
||||||
|
if (result.photoUris.length === 0) return null;
|
||||||
|
|
||||||
|
const uri: string = result.photoUris[0];
|
||||||
|
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||||||
|
const pixelMap: image.PixelMap = await imageSource.createPixelMap();
|
||||||
|
|
||||||
|
ToastUtils.normalToast({ message: '抠图中...' });
|
||||||
|
|
||||||
|
await subjectSegmentation.init();
|
||||||
|
const visionInfo: subjectSegmentation.VisionInfo = { pixelMap: pixelMap };
|
||||||
|
const config: subjectSegmentation.SegmentationConfig = {
|
||||||
|
enableSubjectForegroundImage: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const segResult: subjectSegmentation.SegmentationResult =
|
||||||
|
await subjectSegmentation.doSegmentation(visionInfo, config);
|
||||||
|
if (segResult && segResult.fullSubject && segResult.fullSubject.foregroundImage) {
|
||||||
|
const pixelMap = segResult.fullSubject.foregroundImage;
|
||||||
|
let newItem = new StickerItem(pixelMap,0);
|
||||||
|
|
||||||
|
await subjectSegmentation.release();
|
||||||
|
return newItem;
|
||||||
|
}else{
|
||||||
|
await subjectSegmentation.release();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ToastUtils.normalToast({ message: '抠图失败,请使用规范图片' });
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (file) fs.closeSync(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开图库(可选择多张图片)
|
||||||
|
*/
|
||||||
|
static async openMultipleGallery(): Promise<image.PixelMap[]> {
|
||||||
|
const pixelMaps: image.PixelMap[] = [];
|
||||||
|
try {
|
||||||
|
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
|
||||||
|
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
||||||
|
photoSelectOptions.maxSelectNumber = 10;
|
||||||
|
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
||||||
|
const photoSelectResult = await photoPicker.select(photoSelectOptions);
|
||||||
|
|
||||||
|
if (!photoSelectResult || photoSelectResult.photoUris.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
for (const uri of photoSelectResult.photoUris) {
|
||||||
|
let file: fs.File | null = null;
|
||||||
|
try {
|
||||||
|
file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(file.fd);
|
||||||
|
const decodingOptions: image.DecodingOptions = {
|
||||||
|
editable: true,
|
||||||
|
desiredPixelFormat: image.PixelMapFormat.RGBA_8888,
|
||||||
|
};
|
||||||
|
const pm = await imageSource.createPixelMap(decodingOptions);
|
||||||
|
if (pm) {
|
||||||
|
pixelMaps.push(pm);
|
||||||
|
}
|
||||||
|
imageSource.release();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`解析图片失败: ${uri}, 错误: ${JSON.stringify(e)}`);
|
||||||
|
} finally {
|
||||||
|
if (file) {
|
||||||
|
fs.closeSync(file.fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pixelMaps;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('选择图片过程发生错误: ' + JSON.stringify(err));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Resource 对应的 PixelMap
|
||||||
|
* @param res Resource 对象
|
||||||
|
* @returns PixelMap 对象
|
||||||
|
*/
|
||||||
|
static async getPixelMapFromResource(res: Resource, context: common.UIAbilityContext): Promise<image.PixelMap> {
|
||||||
|
const resourceMgr = context.resourceManager;
|
||||||
|
const fileData = await resourceMgr.getMediaContent(res);
|
||||||
|
const imageSource = image.createImageSource(fileData.buffer as ArrayBuffer);
|
||||||
|
return await imageSource.createPixelMap({
|
||||||
|
editable: true,
|
||||||
|
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 PixelMap 转换为 ImageSource
|
||||||
|
* @param pixelMap 输入的位图对象
|
||||||
|
* @returns 转换后的 ImageSource 对象
|
||||||
|
*/
|
||||||
|
static async pixelMapToImageSource(pixelMap: image.PixelMap): Promise<image.ImageSource|undefined> {
|
||||||
|
const imagePacker = image.createImagePacker();
|
||||||
|
|
||||||
|
const packingOptions: image.PackingOption = {
|
||||||
|
format: "image/jpeg",
|
||||||
|
quality: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer: ArrayBuffer = await imagePacker.packing(pixelMap, packingOptions);
|
||||||
|
const imageSource: image.ImageSource = image.createImageSource(buffer);
|
||||||
|
|
||||||
|
return imageSource;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('转换失败: ' + JSON.stringify(err));
|
||||||
|
return undefined;
|
||||||
|
} finally {
|
||||||
|
imagePacker.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { isExistsUniMP } from '@dcloudio/uni-app-runtime';
|
||||||
|
import { UNIMP_DATA_VERSION, WX_AUTH_CODE } from '../constants/AppConstants'
|
||||||
|
import { UniMpLocalVersion } from '../viewmodel/LocalBean';
|
||||||
|
import { KVStore } from './KVStore'
|
||||||
|
import { StringUtils } from './StringUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断小程序是否需要下载(跳转指定路径)
|
||||||
|
* @param uniId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function isUniMpNeedDownloadForPath(uniId: string): Promise<boolean>{
|
||||||
|
let uniMpVersions = await getUniMpNeedDownload();
|
||||||
|
let uniMpVersion = uniMpVersions.find(item => {return item.id === uniId;});
|
||||||
|
|
||||||
|
let isExists = false;
|
||||||
|
if(uniMpVersion !== undefined && isExistsUniMP(uniId)){
|
||||||
|
isExists = true
|
||||||
|
} else {
|
||||||
|
isExists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断小程序是否需要下载(默认,目标版本判断)
|
||||||
|
* @param uniId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function isUniMpNeedDownload(uniId: string, targetVersion: string|undefined): Promise<boolean> {
|
||||||
|
if(!targetVersion){
|
||||||
|
true
|
||||||
|
}
|
||||||
|
let uniMpVersions = await getUniMpNeedDownload();
|
||||||
|
let uniMpVersion = uniMpVersions.find(item => {return item.id === uniId});
|
||||||
|
if(!StringUtils.checkVersionLegal(uniMpVersion?.version??'0.0.0', targetVersion??'0.0.0') || !StringUtils.isNewVersionAvailable(uniMpVersion?.version??'0.0.0', targetVersion??'0.0.0')){
|
||||||
|
return false;
|
||||||
|
}else{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUniMpNeedDownload(): Promise<UniMpLocalVersion[]> {
|
||||||
|
// 注意:JSON 内部必须使用双引号 "",单引号会引发解析错误
|
||||||
|
const defaultJson = '[]';//'[{"id":"","type":"","version":"1.0.0","path":""},{"id":"","type":"","version":"1.0.0","path":""}]';
|
||||||
|
|
||||||
|
let storeJson = await KVStore.getInstance()
|
||||||
|
.get(UNIMP_DATA_VERSION, defaultJson);
|
||||||
|
let uniMpVersions = JSON.parse(storeJson) as UniMpLocalVersion[];
|
||||||
|
return uniMpVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setUniMpNeedDownload(id: string, type: string, version: string, path: string): Promise<void> {
|
||||||
|
let uniMpVersions = await getUniMpNeedDownload();
|
||||||
|
let uniMpVersion = uniMpVersions.find(item => {return item.id === id});
|
||||||
|
if(uniMpVersion){
|
||||||
|
uniMpVersion.type = type;
|
||||||
|
uniMpVersion.version = version;
|
||||||
|
uniMpVersion.path = path;
|
||||||
|
}else{
|
||||||
|
uniMpVersions.push({
|
||||||
|
id: id,
|
||||||
|
type: type,
|
||||||
|
version: version,
|
||||||
|
path: path
|
||||||
|
});
|
||||||
|
}
|
||||||
|
KVStore.getInstance().put(UNIMP_DATA_VERSION, JSON.stringify(uniMpVersions))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setWechatAuthCode(wechatCode: string): void {
|
||||||
|
KVStore.getInstance().put(WX_AUTH_CODE, wechatCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWechatAuthCode(): Promise<string> {
|
||||||
|
return await KVStore.getInstance().get(WX_AUTH_CODE, '')
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { preferences } from '@kit.ArkData';
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { Logger } from './Logger';
|
||||||
|
import emitter from '@ohos.events.emitter';
|
||||||
|
import { EVT_KV_INIT_DONE } from '../constants/EventConstants';
|
||||||
|
import { IS_KV_STORE_INIT_FINISHED } from '../constants/AppConstants';
|
||||||
|
|
||||||
|
export class KVStore {
|
||||||
|
private static instance: KVStore | null = null;
|
||||||
|
private pref: preferences.Preferences | null = null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
// 初始化(建议在 UIAbility 的 onCreate 中调用一次)
|
||||||
|
static async init(context: common.Context): Promise<void> {
|
||||||
|
if (!KVStore.instance) {
|
||||||
|
const store = new KVStore();
|
||||||
|
// 获取 Preferences 实例必须异步等待
|
||||||
|
store.pref = await preferences.getPreferences(context, 'cn_img_rabbit_kv_store');
|
||||||
|
KVStore.instance = store;
|
||||||
|
|
||||||
|
emitter.emit({ eventId: EVT_KV_INIT_DONE });
|
||||||
|
AppStorage.setOrCreate<boolean>(IS_KV_STORE_INIT_FINISHED, true);
|
||||||
|
Logger.info('Rabbit_KVStore', 'KVStore initialized finish.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): KVStore {
|
||||||
|
if (!KVStore.instance) {
|
||||||
|
// 抛出错误比返回 undefined 更好,能让你一眼看到是不是顺序错了
|
||||||
|
throw new Error("Rabbit_KVStore is not initialized. Please call await KVStore.init() in Ability onCreate.");
|
||||||
|
}
|
||||||
|
return KVStore.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储数据 (模拟 MMKV put)
|
||||||
|
async put(key: string, value: preferences.ValueType): Promise<void> {
|
||||||
|
if (!this.pref) {
|
||||||
|
// 如果 pref 为空,尝试重新补救(或者抛出错误方便排查)
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: pref is null when putting key: ${key}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.pref.put(key, value);
|
||||||
|
await this.pref.flush(); // 持久化到磁盘
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: Successfully saved ${key} ---- ${value}`);
|
||||||
|
} catch (err) {
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: Save failed, error: ${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 读取数据 (模拟 MMKV get)
|
||||||
|
async get<T extends preferences.ValueType>(key: string, defaultValue: T): Promise<T> {
|
||||||
|
if (this.pref) {
|
||||||
|
const val = await this.pref.get(key, defaultValue) as T;
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: Fetching ${key}, value is: ${val}`);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
Logger.info('Rabbit_KVStore', `KVStore: pref is null, returning default for ${key}`);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步读取数据 (对应 Android MMKV.decodeString)
|
||||||
|
* 必须确保 pref 已经初始化完成
|
||||||
|
*/
|
||||||
|
getSync<T extends preferences.ValueType>(key: string, defaultValue: T): T {
|
||||||
|
if (this.pref !== null) {
|
||||||
|
// 使用系统提供的 getSync 接口
|
||||||
|
return this.pref.getSync(key, defaultValue) as T;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除数据
|
||||||
|
async delete(key: string): Promise<void> {
|
||||||
|
if (this.pref) {
|
||||||
|
await this.pref.delete(key);
|
||||||
|
await this.pref.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* Logger工具类,提供统一的日志记录功能
|
||||||
|
*/
|
||||||
|
export class Logger {
|
||||||
|
private static readonly TAG: string = 'AppLogger'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录方法调用日志
|
||||||
|
* @param className 类名
|
||||||
|
* @param methodName 方法名
|
||||||
|
*/
|
||||||
|
static logMethodCall(className: string, methodName: string): void {
|
||||||
|
console.info(`[${Logger.TAG}] ${className}.${methodName}() called`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录信息级别日志
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param message 日志消息
|
||||||
|
*/
|
||||||
|
static info(tag: string, message: string): void {
|
||||||
|
console.info(`[${Logger.TAG}] [${tag}] INFO: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录错误级别日志
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param error 错误对象或消息
|
||||||
|
* @param message 错误描述
|
||||||
|
*/
|
||||||
|
static logError(tag: string, error: object, message: string): void {
|
||||||
|
console.error(`[${Logger.TAG}] [${tag}] ERROR: ${message}`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录错误级别日志(兼容性方法)
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param message 日志消息
|
||||||
|
*/
|
||||||
|
static error(tag: string, message: string): void {
|
||||||
|
console.error(`[${Logger.TAG}] [${tag}] ERROR: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录调试级别日志
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param message 日志消息
|
||||||
|
*/
|
||||||
|
static debug(tag: string, message: string): void {
|
||||||
|
console.debug(`[${Logger.TAG}] [${tag}] DEBUG: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录警告级别日志
|
||||||
|
* @param tag 日志标签
|
||||||
|
* @param message 日志消息
|
||||||
|
*/
|
||||||
|
static warn(tag: string, message: string): void {
|
||||||
|
console.warn(`[${Logger.TAG}] [${tag}] WARN: ${message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { pasteboard } from '@kit.BasicServicesKit';
|
||||||
|
import { BusinessError } from '@ohos.base';
|
||||||
|
import { promptAction } from '@kit.ArkUI';
|
||||||
|
import { Logger } from './Logger';
|
||||||
|
import { ToastUtils } from '../dialog/ToastUtils';
|
||||||
|
|
||||||
|
export class StringUtils {
|
||||||
|
/**
|
||||||
|
* 复制纯文本到剪切板
|
||||||
|
* @param text 待复制的字符串
|
||||||
|
* @param showToast 是否显示成功提示,默认为 true
|
||||||
|
*/
|
||||||
|
static copyText(text: string, showToast: boolean = true): void {
|
||||||
|
if (!text) {
|
||||||
|
console.warn('ClipboardUtil: Text is empty, skip copying.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 创建剪贴板数据对象
|
||||||
|
const pasteData: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
|
||||||
|
// 2. 获取系统剪贴板
|
||||||
|
const systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard();
|
||||||
|
|
||||||
|
// 3. 写入数据 (显式指定 BusinessError 类型消除 any 报错)
|
||||||
|
systemPasteboard.setData(pasteData, (err: BusinessError) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`ClipboardUtil: Failed to set data. Code: ${err.code}, message: ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (showToast) {
|
||||||
|
ToastUtils.normalToast({message:'已复制到剪切板'});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 4. 类型断言消除 catch 中的 any/unknown 报错
|
||||||
|
const err: BusinessError = error as BusinessError;
|
||||||
|
console.error(`ClipboardUtil: Unexpected error. Code: ${err.code}, message: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查版本是否符合要求(要求规定三段式:0.0.0)
|
||||||
|
* @param v1 基准版本(本地安装的版本号)
|
||||||
|
* @param v2 目标版本(服务器要求更新的版本号)
|
||||||
|
* @returns true 可用于比较版本号,不可(且无法)用于比较版本号
|
||||||
|
*/
|
||||||
|
static checkVersionLegal(v1: string, v2: string): boolean {
|
||||||
|
const v1Parts: string[] = v1.split('.');
|
||||||
|
const v2Parts: string[] = v2.split('.');
|
||||||
|
if(v1Parts.length === v2Parts.length){
|
||||||
|
return true
|
||||||
|
}else{
|
||||||
|
if(v1Parts.length < 3 || v1Parts.length > 3){
|
||||||
|
Logger.info('isNewVersionAvailable', '基准版本:'+v1 +',版本长度不合规,应该(三段式)为:0.0.0')
|
||||||
|
}
|
||||||
|
if(v2Parts.length < 3 || v2Parts.length > 3){
|
||||||
|
Logger.info('isNewVersionAvailable', '目标版本:'+v2 + ',版本长度不合规,应该(三段式)为:0.0.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较版本号
|
||||||
|
* @param v1 基准版本 (如 '1.1.0')
|
||||||
|
* @param v2 目标版本 (如 '1.0.1')
|
||||||
|
* @returns 如果 v2 < v1 返回 true,否则返回 false
|
||||||
|
*/
|
||||||
|
static isNewVersionAvailable(v1: string, v2: string): boolean {
|
||||||
|
Logger.info('isNewVersionAvailable', v1 +'----------'+v2)
|
||||||
|
// 1. 按点拆分数组
|
||||||
|
const v1Parts: string[] = v1.split('.');
|
||||||
|
const v2Parts: string[] = v2.split('.');
|
||||||
|
// 2. 长度不相等,无法比较
|
||||||
|
if(v1Parts.length!=v2Parts.length){
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 确定最大比较长度
|
||||||
|
const maxLength: number = Math.max(v1Parts.length, v2Parts.length);
|
||||||
|
|
||||||
|
// 4. 版本比较
|
||||||
|
for (let i = 0; i < maxLength; i++) {
|
||||||
|
// 使用 Number() 或 parseInt,并处理 NaN 的情况
|
||||||
|
const n1: number = i < v1Parts.length ? (parseInt(v1Parts[i], 10) || 0) : 0;
|
||||||
|
const n2: number = i < v2Parts.length ? (parseInt(v2Parts[i], 10) || 0) : 0;
|
||||||
|
|
||||||
|
Logger.info('isNewVersionAvailable', n1 +'*************'+n2)
|
||||||
|
|
||||||
|
if (n1 < n2) return true;
|
||||||
|
if (n1 > n2) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 循环结束仍相等
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { openUniMP, releaseWgtToRunPath, UniMP } from '@dcloudio/uni-app-runtime'
|
||||||
|
import { AESDecrypt, PLATFORM, RELEASE_BASE_URL, Signature, TOKEN } from '../constants/AppConstants'
|
||||||
|
import { ToastUtils } from '../dialog/ToastUtils'
|
||||||
|
import { UniVersionEntity,TradeData, UniIconEntity } from '../viewmodel/DataBean'
|
||||||
|
import { downloadFile } from './DownloaderUtils'
|
||||||
|
import { isUniMpNeedDownload, isUniMpNeedDownloadForPath } from './KVManager'
|
||||||
|
import { KVStore } from './KVStore'
|
||||||
|
import { Logger } from './Logger'
|
||||||
|
import { bundleManager, common } from '@kit.AbilityKit'
|
||||||
|
import { DeviceUtil } from '@pura/harmony-utils'
|
||||||
|
import { deviceInfo } from '@kit.BasicServicesKit'
|
||||||
|
import { CHANNEL } from '../../../../BuildProfile'
|
||||||
|
import { CommonService } from '../viewmodel/CommonService'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载并启动模拟器
|
||||||
|
* @param context
|
||||||
|
* @param entity
|
||||||
|
* 注意:status: -1-失败, 0-默认, 1-进行中(progress为百分比), 2-成功(progress为0, path为路径), 3-启动, 4-释放成功, 5-释放失败
|
||||||
|
*/
|
||||||
|
export function downStartUp(context: UIContext, entity: UniVersionEntity, callback: (status: number, progress: number, path?: string) => void) {
|
||||||
|
let uniMpId = entity.unimp_id;//'__UNI__D535736';
|
||||||
|
let uniMpType = entity.unimp_type;
|
||||||
|
let uniMpUrl = entity.url;//'https://cdn.batiao8.com/flaunt/uni_mp/wgt/alipay/__UNI__D535736_2026-4-20-test4.wgt';
|
||||||
|
let uniVersion = entity.version??'0.0.0';
|
||||||
|
if(uniMpId){
|
||||||
|
isUniMpNeedDownload(uniMpId, uniVersion).then(res => {
|
||||||
|
if(res == true){
|
||||||
|
callback(1, 0)
|
||||||
|
//需要更新,则删除旧文件,重新下载wgt文件
|
||||||
|
if(!uniMpId){
|
||||||
|
ToastUtils.showToast(context, '加载失败,请重试或联系客服!')
|
||||||
|
|
||||||
|
callback(-1, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//下载模拟器
|
||||||
|
downloadFile(getContext(), uniMpUrl??'', uniMpId + '.wgt', (status, progress, path) => {
|
||||||
|
if(status == 2){
|
||||||
|
callback(status, progress, path)
|
||||||
|
//已下载完成,释放并启动
|
||||||
|
releaseRunUniMp(context, uniMpId??'', '', (status) => {
|
||||||
|
callback(status, progress, path)
|
||||||
|
})
|
||||||
|
}else if(status == -1){
|
||||||
|
ToastUtils.showToast(context, '加载失败,请重试或联系客服!')
|
||||||
|
callback(status, progress, path)
|
||||||
|
}else{
|
||||||
|
callback(status, progress, path)
|
||||||
|
}
|
||||||
|
Logger.info('UniMp','downloadFile status:' + status + ', progress:' + progress)
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
//不需要更新,已释放则直接打开模拟器,否则先释放,再打开
|
||||||
|
runUniMp(context, uniMpId??'', '', (status) => {
|
||||||
|
callback(status, 100)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Logger.info('UniMp','uniMpId:'+uniMpId+ ', uniMpType:' + uniMpType + ', uniMpUrl:' + uniMpUrl + ', uniMpVersion:' + uniVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downStartUpForPath(context: UIContext, uniMpVersions: UniVersionEntity[], uniIconEntity: UniIconEntity, callback: (status: number, progress: number, path?: string) => void) {
|
||||||
|
const uniMpType = uniIconEntity.type
|
||||||
|
const uniMpEntity = uniMpVersions.find((item: UniVersionEntity) => {
|
||||||
|
return item.unimp_type === uniMpType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetPath = uniIconEntity.url??''
|
||||||
|
const uniMpUrl = uniMpEntity?.url??''//'https://cdn.batiao8.com/flaunt/uni_mp/wgt/alipay/__UNI__D535736_2026-4-20-test4.wgt';
|
||||||
|
const uniMpId = uniMpEntity?.unimp_id??''
|
||||||
|
isUniMpNeedDownloadForPath(uniMpId).then(res => {
|
||||||
|
if(res == true){
|
||||||
|
// 已释放,直接运行
|
||||||
|
runUniMp(context, uniMpId??'', '', (status) => {
|
||||||
|
callback(status, 100)
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
// 未下载,先下载释放,再运行
|
||||||
|
downloadFile(getContext(), uniMpUrl??'', uniMpId + '.wgt', (status, progress, path) => {
|
||||||
|
if(status == 2){
|
||||||
|
callback(status, progress, path)
|
||||||
|
//已下载完成,释放并启动
|
||||||
|
releaseRunUniMp(context, uniMpId??'', targetPath, (status) => {
|
||||||
|
callback(status, progress, path)
|
||||||
|
})
|
||||||
|
}else if(status == -1){
|
||||||
|
ToastUtils.showToast(context, '加载失败,请重试或联系客服!')
|
||||||
|
callback(status, progress, path)
|
||||||
|
}else{
|
||||||
|
callback(status, progress, path)
|
||||||
|
}
|
||||||
|
Logger.info('UniMp','downloadFile status:' + status + ', progress:' + progress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行模拟器
|
||||||
|
* 1、如已释放,直接运行
|
||||||
|
* 2、如未释放,先释放,再运行
|
||||||
|
* @param uniMpId 小程序ID
|
||||||
|
* @param targetPath 跳转小程序路径
|
||||||
|
* 注意:status: -1-失败, 0-默认, 1-进行中(progress为百分比), 2-成功(progress为0, path为路径), 3-启动, 4-释放成功, 5-释放失败
|
||||||
|
*/
|
||||||
|
export function runUniMp(context: UIContext, uniMpId: string, targetPath:string, callback: (status: number) => void){
|
||||||
|
releaseRunUniMp(context, uniMpId, targetPath, callback)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 释放并运行,需要先释放,再运行
|
||||||
|
* 1、删除旧文件
|
||||||
|
* 2、先释放,再运行
|
||||||
|
* @param uniMpId 小程序ID
|
||||||
|
* @param targetPath 跳转小程序路径
|
||||||
|
* 注意:status: -1-失败, 0-默认, 1-进行中(progress为百分比), 2-成功(progress为0, path为路径), 3-启动, 4-释放成功, 5-释放失败
|
||||||
|
*/
|
||||||
|
export function releaseRunUniMp(context: UIContext, uniMpId: string, targetPath: string, callback: (status: number) => void){
|
||||||
|
let path = getContext().filesDir + "/"+uniMpId+".wgt"
|
||||||
|
uniMpConfig().then(res => {
|
||||||
|
releaseWgtToRunPath(uniMpId, path, (code: 1 | -1, _) =>{
|
||||||
|
if(code === 1){
|
||||||
|
callback(4)
|
||||||
|
// 释放成功,运行
|
||||||
|
const mp = openUniMP(uniMpId, {
|
||||||
|
redirectPath: targetPath,
|
||||||
|
extraData: res,
|
||||||
|
showCapsuleButton: false,
|
||||||
|
showSplashScreen: true,
|
||||||
|
}) as UniMP
|
||||||
|
|
||||||
|
mp.on('close',()=>{
|
||||||
|
console.log('UniMP-close')
|
||||||
|
})
|
||||||
|
mp.on('show',()=>{
|
||||||
|
console.log('UniMP-show')
|
||||||
|
callback(3)
|
||||||
|
})
|
||||||
|
mp.on('hide',()=>{
|
||||||
|
console.log('UniMP-hide')
|
||||||
|
})
|
||||||
|
mp.on('uniMPEvent', (event: string, data: object, _notify: boolean) => {
|
||||||
|
Logger.info('UniMp', 'uniMPEvent:' + event + ', data:' + data)
|
||||||
|
|
||||||
|
if(event === 'start_combo_pay'){
|
||||||
|
const jsonStr = typeof data === 'string' ? data : JSON.stringify(data);
|
||||||
|
const obj = JSON.parse(jsonStr) as TradeData;
|
||||||
|
|
||||||
|
const weixinMpOriId: string = obj.weixinMpOriId ?? "";
|
||||||
|
const outTradeNo: string = obj.outTradeNo ?? "";
|
||||||
|
CommonService.startUniPay(getContext() as common.UIAbilityContext, weixinMpOriId, outTradeNo)
|
||||||
|
}else if(event === 'closeUniAPP'){
|
||||||
|
mp.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}else{
|
||||||
|
// 释放失败,提示用户
|
||||||
|
ToastUtils.showToast(context, '加载失败,请重试或联系客服!')
|
||||||
|
callback(5)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uniMpConfig(): Promise<Record<string, string>>{
|
||||||
|
let extraData: Record<string, string> = {};
|
||||||
|
|
||||||
|
const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
|
||||||
|
const versionName = bundleInfo.versionName;
|
||||||
|
const bundleName = bundleInfo.name;
|
||||||
|
|
||||||
|
const accessToken: string = KVStore.getInstance().getSync(TOKEN, '');
|
||||||
|
const deviceId: string = DeviceUtil.getDeviceId();
|
||||||
|
const brand: string = deviceInfo.brand;
|
||||||
|
const model: string = deviceInfo.productModel;
|
||||||
|
|
||||||
|
extraData["x-token"] = accessToken;
|
||||||
|
extraData["x-version"] = versionName;
|
||||||
|
extraData["x-platform"] = PLATFORM;
|
||||||
|
extraData["x-device-id"] = deviceId;
|
||||||
|
extraData["x-mobile-brand"] = brand;
|
||||||
|
extraData["x-mobile-model"] = model;
|
||||||
|
extraData["x-channel"] = CHANNEL;
|
||||||
|
extraData["x-package"] = bundleName;
|
||||||
|
extraData["x-click-id"] = '';
|
||||||
|
extraData["host"] = RELEASE_BASE_URL + '/';
|
||||||
|
extraData["decrypt"] = AESDecrypt;
|
||||||
|
extraData["encrypt"] = Signature;
|
||||||
|
extraData["isCombo"] = 'ok';
|
||||||
|
|
||||||
|
return extraData
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
@Component
|
||||||
|
export struct SplashWxUniMpView {
|
||||||
|
build() {
|
||||||
|
Stack() {
|
||||||
|
Image($r('app.media.ic_splash_wx_unimp'))
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
.width('100%')
|
||||||
|
.height('100%')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
import { AlipayProfile, CaptchaProfile, LoginProfile, UserInfo, UserProfile, UserProfileMapper } from "./DataBean";
|
||||||
|
import deviceInfo from "@ohos.deviceInfo";
|
||||||
|
import { DeviceUtil } from "@pura/harmony-utils";
|
||||||
|
import { Logger } from "../utils/Logger";
|
||||||
|
import { ApiManager } from "../provider/ApiManager";
|
||||||
|
import { DEVICE_OAID, LOGIN_PROFILE, TOKEN, TOKEN_LOGIN, USER_INFO, USER_PROFILE,
|
||||||
|
WXAPPID} from "../constants/AppConstants";
|
||||||
|
import { KVStore } from "../utils/KVStore";
|
||||||
|
import { RequestCallback } from "../provider/Callback";
|
||||||
|
import { EVT_USER_INFO,EVT_USER_CONFIG_PROFILE } from "../constants/EventConstants";
|
||||||
|
import { emitter } from "@kit.BasicServicesKit";
|
||||||
|
import { AFAuthServiceResponse, AFService, AFServiceCenter, AFServiceParams, AFWantParams } from "@alipay/afservicesdk";
|
||||||
|
import { common } from "@kit.AbilityKit";
|
||||||
|
import { ToastUtils } from "../dialog/ToastUtils";
|
||||||
|
|
||||||
|
import * as wxopensdk from '@tencent/wechat_open_sdk';
|
||||||
|
// 注意:全局只能用同一个WXApi对象,否则无法监听回调
|
||||||
|
export const WXApi = wxopensdk.WXAPIFactory.createWXAPI(WXAPPID)
|
||||||
|
|
||||||
|
export class CommonService {
|
||||||
|
private static userConfigEvent(isSuccess: boolean){
|
||||||
|
const eventData: emitter.EventData = {
|
||||||
|
data: { "result": isSuccess }
|
||||||
|
};
|
||||||
|
emitter.emit({ eventId: EVT_USER_CONFIG_PROFILE }, eventData);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 请求用户配置信息 (带参请求)
|
||||||
|
*/
|
||||||
|
static async getUserProfile(): Promise<UserProfile> {
|
||||||
|
const osVersion: number = deviceInfo.sdkApiVersion;
|
||||||
|
const oaid: string = AppStorage.get<string>(DEVICE_OAID)??'';
|
||||||
|
const imei: string = DeviceUtil.getDeviceId();
|
||||||
|
|
||||||
|
Logger.info('UserService',
|
||||||
|
` 请求参数:----> oaid:${oaid}---> osVersion:${osVersion}---> imei:${imei}`);
|
||||||
|
const queryParams: Record<string, Object> = {
|
||||||
|
'oaid': oaid,
|
||||||
|
'os_version': osVersion,
|
||||||
|
"imei": imei,
|
||||||
|
"cid": ''
|
||||||
|
};
|
||||||
|
let result = await ApiManager.getInstance().requestForMapper<UserProfile>(
|
||||||
|
'/api/user/config',
|
||||||
|
queryParams, // 传参,如有则传入
|
||||||
|
UserProfileMapper // 映射,数据映射器
|
||||||
|
);
|
||||||
|
AppStorage.setOrCreate<UserProfile>(USER_PROFILE, result);
|
||||||
|
if (result.token && result.token.length > 0) {
|
||||||
|
KVStore.getInstance().put(TOKEN, result.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
CommonService.userConfigEvent(true)
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送验证码
|
||||||
|
* @param phone
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
static async sendSmsCode(phone: string, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
const postParms: Record<string, string> = {
|
||||||
|
"phone": phone
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await ApiManager.getInstance().request<CaptchaProfile>(
|
||||||
|
'api/user/code', // api
|
||||||
|
undefined, // get请求参数,会拼接到url中
|
||||||
|
'post', // 请求方式
|
||||||
|
postParms // post请求参数
|
||||||
|
);
|
||||||
|
|
||||||
|
if(result.code === 200 || result.code === 0){
|
||||||
|
if (callback && callback.onSuccess) {
|
||||||
|
callback.onSuccess(result.data.timestamp);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if (callback && callback.onFailure) {
|
||||||
|
callback.onFailure(result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录:验证码登录
|
||||||
|
* @param phone
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
static async loginForCaptcha(phone: string, captchaCode: string, captchaTimestamp: string, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
const jsonPhone: Record<string, string | number> = {
|
||||||
|
"timestamp": captchaTimestamp, // 确保这个 value 是 string 或 number
|
||||||
|
"phone": phone,
|
||||||
|
"code": captchaCode
|
||||||
|
};
|
||||||
|
const postData: Record<string, Object> = {
|
||||||
|
"login_type": "phone",
|
||||||
|
"phone": jsonPhone
|
||||||
|
};
|
||||||
|
|
||||||
|
CommonService.login(postData, callback);
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录(通用)
|
||||||
|
* @param requestData
|
||||||
|
* @param callback
|
||||||
|
*/
|
||||||
|
static async login(requestData: Record<string, Object>, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
let result = await ApiManager.getInstance().request<LoginProfile>(
|
||||||
|
'/api/user/login',
|
||||||
|
undefined,
|
||||||
|
'post',
|
||||||
|
requestData
|
||||||
|
);
|
||||||
|
|
||||||
|
if(result.code === 200 || result.code === 0){
|
||||||
|
const token = result.data.token
|
||||||
|
if(token && token.length > 0){
|
||||||
|
//将登录信息持久化(const savedProfile = JSON.parse(rawJson) as LoginProfile;)
|
||||||
|
const loginJson: string = JSON.stringify(result);
|
||||||
|
KVStore.getInstance().put(LOGIN_PROFILE, loginJson); //持久化登录信息
|
||||||
|
KVStore.getInstance().put(TOKEN_LOGIN, token ?? ''); //持久化token
|
||||||
|
KVStore.getInstance().put(TOKEN, token ?? ''); //持久化token
|
||||||
|
}
|
||||||
|
if (callback && callback.onSuccess) {
|
||||||
|
callback.onSuccess(result.data);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
if (callback && callback.onFailure) {
|
||||||
|
callback.onFailure(result.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static userInfoEvent(isSuccess: boolean){
|
||||||
|
const eventData: emitter.EventData = {
|
||||||
|
data: { "result": isSuccess }
|
||||||
|
};
|
||||||
|
emitter.emit({ eventId: EVT_USER_INFO }, eventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
static async getUserInfo(): Promise<UserInfo> {
|
||||||
|
const result = await ApiManager.getInstance().request<UserInfo>(
|
||||||
|
'/api/user',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const userInfo = result.data
|
||||||
|
|
||||||
|
//将用户信息持久化(const savedProfile = JSON.parse(rawJson) as UserInfo;)
|
||||||
|
const userInfoJson: string = JSON.stringify(userInfo);
|
||||||
|
KVStore.getInstance().put(USER_INFO, userInfoJson); //持久化用户信息
|
||||||
|
|
||||||
|
CommonService.userInfoEvent(true)
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉起微信,获取微信授权
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
|
static async wxAuthorize(context: common.UIAbilityContext, isBindAuth: boolean){
|
||||||
|
const isInstalled = WXApi.isWXAppInstalled()
|
||||||
|
if(!isInstalled){
|
||||||
|
const uiContext = context.windowStage.getMainWindowSync().getUIContext();
|
||||||
|
ToastUtils.showToast(uiContext, '您没有安装微信客户端,请先下载安装')
|
||||||
|
}else{
|
||||||
|
const bundleName: string = context.abilityInfo.bundleName;
|
||||||
|
let req = new wxopensdk.SendAuthReq;
|
||||||
|
req.isOption1 = false
|
||||||
|
req.nonAutomatic = true
|
||||||
|
req.scope = 'snsapi_userinfo';// 只能填 snsapi_userinfo
|
||||||
|
req.state = bundleName + Math.random() * 1000 + "_phone";
|
||||||
|
req.transaction = isBindAuth ? 'rabbit_bind' : 'rabbit_login'
|
||||||
|
|
||||||
|
WXApi.sendReq(context, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async wxLogin(authCode: string, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
const jsonWxAuth: Record<string, string | number> = {
|
||||||
|
"code": authCode,
|
||||||
|
"code_type": ''
|
||||||
|
};
|
||||||
|
const postData: Record<string, Object> = {
|
||||||
|
"login_type": "weixin",
|
||||||
|
"weixin": jsonWxAuth
|
||||||
|
};
|
||||||
|
|
||||||
|
CommonService.login(postData, callback);
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取支付宝登录参数
|
||||||
|
*/
|
||||||
|
static async getAlipayLoginParams(): Promise<AlipayProfile> {
|
||||||
|
const result = await ApiManager.getInstance().request<AlipayProfile>(
|
||||||
|
'api/alipay/app_param',
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async aliAuthorize(profile: AlipayProfile, callback: (result: string) => void) {
|
||||||
|
const queryStr: string = profile.param??'';
|
||||||
|
const match: RegExpMatchArray | null = queryStr.match(/app_id=([^&]*)/);
|
||||||
|
let appId: string = '';
|
||||||
|
if (match !== null && match[1]) {
|
||||||
|
appId = match[1];
|
||||||
|
}
|
||||||
|
// 获取支付宝登录参数
|
||||||
|
//1. 构建参数
|
||||||
|
let bizParams = new Map<string, string>()
|
||||||
|
let url = encodeURIComponent('https://authweb.alipay.com/auth?auth_type=PURE_OAUTH_SDK&app_id='+appId+'&scope=auth_user&state=init')
|
||||||
|
bizParams.set("url", url);
|
||||||
|
|
||||||
|
let backWant: AFWantParams = {
|
||||||
|
bundleName: "com.img.rabbit",
|
||||||
|
moduleName: "app",
|
||||||
|
abilityName: "AppAbility"
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = new AFServiceParams(bizParams, false, true, '', backWant, (response: AFAuthServiceResponse) => {
|
||||||
|
//授权返回值
|
||||||
|
let result = response.result
|
||||||
|
if (result) {
|
||||||
|
let parameters = result['parameters']
|
||||||
|
if (parameters) {
|
||||||
|
let authCode: string = parameters['auth_code']
|
||||||
|
callback(authCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
AFServiceCenter.call(AFService.AFServiceAuth, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async aliLogin(authCode: string, callback?: RequestCallback) {
|
||||||
|
try {
|
||||||
|
const jsonAliAuth: Record<string, string | number> = {
|
||||||
|
"auth_code": authCode
|
||||||
|
};
|
||||||
|
const postData: Record<string, Object> = {
|
||||||
|
"type": "alipay",
|
||||||
|
"bind": "",
|
||||||
|
"data": jsonAliAuth
|
||||||
|
};
|
||||||
|
|
||||||
|
CommonService.login(postData, callback);
|
||||||
|
} catch (err) {
|
||||||
|
if (callback && callback.onError) {
|
||||||
|
callback.onError(err as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static startUniPay(context: common.UIAbilityContext, weixinMpOriId: string, outTradeNo: string) {
|
||||||
|
const req: wxopensdk.LaunchMiniProgramReq = new wxopensdk.LaunchMiniProgramReq();
|
||||||
|
|
||||||
|
req.userName = weixinMpOriId;
|
||||||
|
req.path = `pages/index/index?outTradeNo=${outTradeNo}`;
|
||||||
|
req.miniprogramType = 0;
|
||||||
|
|
||||||
|
WXApi.sendReq(context, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
import ArrayList from "@ohos.util.ArrayList";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserProfile 的专属转换器
|
||||||
|
*/
|
||||||
|
export const UserProfileMapper = (raw: Record<string, Object>): UserProfile => {
|
||||||
|
return {
|
||||||
|
token: (raw['token'] as string) ?? "",
|
||||||
|
temp: (raw['temp'] as boolean) ?? false,
|
||||||
|
name: (raw['name'] as string) ?? "",
|
||||||
|
user_id: (raw['user_id'] as string) ?? "",
|
||||||
|
config: ConfigMapper(raw['config'] as Record<string, Object>)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置转换器
|
||||||
|
*/
|
||||||
|
export const ConfigMapper = (raw: Record<string, Object>): ConfigEntity => {
|
||||||
|
return {
|
||||||
|
versionEntity: (raw['client.version.upgrade'] as VersionEntity) ?? null,
|
||||||
|
uniVersionList: (raw['client.uni.version.upgrade'] as UniVersionEntity[]) ?? [],
|
||||||
|
homeIconEntity: (raw['client.icon.uni.home'] as UniIconEntity[]) ?? [],
|
||||||
|
wgtPassword: (raw['client.wgt.password'] as string) ?? null,
|
||||||
|
isUniMpOpen: (raw['client.uni.open'] as boolean) ?? false
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 用户配置(Profile)
|
||||||
|
export interface UserProfile {
|
||||||
|
token: string;
|
||||||
|
temp: boolean;
|
||||||
|
name: string; // 显示的文本内容
|
||||||
|
user_id: string;
|
||||||
|
config: ConfigEntity; // 可选的图标资源
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置(Config)
|
||||||
|
export interface ConfigEntity {
|
||||||
|
versionEntity: VersionEntity | null;
|
||||||
|
uniVersionList: UniVersionEntity[];
|
||||||
|
homeIconEntity: UniIconEntity[];
|
||||||
|
wgtPassword: string | null;
|
||||||
|
isUniMpOpen: boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置(App版本)
|
||||||
|
export interface VersionEntity {
|
||||||
|
description: string | null;
|
||||||
|
force: boolean;
|
||||||
|
last_version_force: string | null;
|
||||||
|
title: string | null;
|
||||||
|
url: string | null;
|
||||||
|
app_size: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置(UniMp版本)
|
||||||
|
export interface UniVersionEntity {
|
||||||
|
version: string | null;
|
||||||
|
url: string | null;
|
||||||
|
last_version_force: string | null;
|
||||||
|
force: boolean;
|
||||||
|
title: string | null;
|
||||||
|
description: string | null;
|
||||||
|
unimp_id: string | null;
|
||||||
|
unimp_type: string | null;
|
||||||
|
icon: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置(UniMp指定路径:Path)
|
||||||
|
export interface UniIconEntity {
|
||||||
|
icon: string | null;
|
||||||
|
text: string | null;
|
||||||
|
url: string | null;
|
||||||
|
type: string | null;
|
||||||
|
enable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证码信息
|
||||||
|
export interface CaptchaProfile {
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录信息
|
||||||
|
export interface LoginProfile {
|
||||||
|
user_id: string | null;
|
||||||
|
name: string | null;
|
||||||
|
avater: string | null;
|
||||||
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 用户信息(字段由/api/user 与 /api/user/account共同提供)
|
||||||
|
export interface UserInfo {
|
||||||
|
alipayid: string | null;
|
||||||
|
app_id: string | null;
|
||||||
|
appleid: string | null;
|
||||||
|
avater: string | null;
|
||||||
|
balance: string | null;
|
||||||
|
client_cid: string | null;
|
||||||
|
coupon_count: string | null;
|
||||||
|
ip_area: string | null;
|
||||||
|
ip_area_name: string | null;
|
||||||
|
name: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
role: string | null;
|
||||||
|
temp: boolean | true;
|
||||||
|
unionid: string | null;
|
||||||
|
user_id: string | null;
|
||||||
|
vip: number | 0;
|
||||||
|
vip_expire: string | null;
|
||||||
|
vip_expire_time: string | null;
|
||||||
|
vip_name: string | null;
|
||||||
|
weixinAppId: string | null;
|
||||||
|
weixinAppIdType: string | null;
|
||||||
|
weixinAppOpenId: string | null;
|
||||||
|
weixinOpenId: string | null;
|
||||||
|
//以下字段,目前由/api/user/account提供
|
||||||
|
vip_type: number | 0;
|
||||||
|
create_time: string | null;
|
||||||
|
bind: ArrayList<string> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付宝授权登录参数
|
||||||
|
export interface AlipayProfile {
|
||||||
|
param: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UniMp小程序微信支付参数
|
||||||
|
export interface TradeData {
|
||||||
|
weixinMpOriId?: string;
|
||||||
|
outTradeNo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {
|
||||||
|
* "icon": "https://cdn.batiao8.com/flaunt/uni_mp/icon/other/ticket.png",
|
||||||
|
* "text": "机票",
|
||||||
|
* "url": "pages/other/tickets-app/index",
|
||||||
|
* "type": "alipay",
|
||||||
|
* "enable": true
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
// 小程序(指定Page)
|
||||||
|
export interface UniMpItem {
|
||||||
|
id: string;
|
||||||
|
text: string; // 显示的文本内容
|
||||||
|
icon: Resource; // 可选的图标资源
|
||||||
|
url: string; // 跳转路径
|
||||||
|
type: string; // 类型
|
||||||
|
enable: boolean; // 是否可用
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { image } from "@kit.ImageKit";
|
||||||
|
import { util } from "@kit.ArkTS";
|
||||||
|
|
||||||
|
// 本地数据,用于处理抠图
|
||||||
|
@Observed
|
||||||
|
export class StickerItem {
|
||||||
|
id: string = util.generateRandomUUID();
|
||||||
|
pixelMap: image.PixelMap;
|
||||||
|
type: number = 0; // 0:人像, 1:服装, 2:发型
|
||||||
|
posX: number = 0;
|
||||||
|
posY: number = 0;
|
||||||
|
lastX: number = 0;
|
||||||
|
lastY: number = 0;
|
||||||
|
scaleValue: number = 1.0;
|
||||||
|
lastScale: number = 1.0;
|
||||||
|
rotateValue: number = 0;
|
||||||
|
lastRotate: number = 0;
|
||||||
|
zIndex: number = 0;
|
||||||
|
isHidden: boolean = false;
|
||||||
|
|
||||||
|
constructor(pm: image.PixelMap, type: number) {
|
||||||
|
this.pixelMap = pm;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地数据,用于处理下载更新UniMp资源
|
||||||
|
export class UniMpLocalVersion {
|
||||||
|
id:string =''; // __UNI__F1F2FC0
|
||||||
|
type: string = ''; // wx:微信, alipay:阿里
|
||||||
|
version: string = ''; // 1.0.0
|
||||||
|
path: string = ''; // data/storage/el2/base/haps/app/files/__UNI__F1F2FC0.wgt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
// 页面参数数据
|
||||||
|
export interface WebParams {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抠图参数数据
|
||||||
|
export interface CuteParams {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定参数数据
|
||||||
|
export interface BindParams {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: number;//1:phone,2:wx
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"module": {
|
||||||
|
"name": "common",
|
||||||
|
"type": "har",
|
||||||
|
"deviceTypes": [
|
||||||
|
"phone"
|
||||||
|
],
|
||||||
|
"requestPermissions": [
|
||||||
|
{
|
||||||
|
"name": "ohos.permission.INTERNET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ohos.permission.APP_TRACKING_CONSENT",
|
||||||
|
"reason": "$string:reason_oaid",
|
||||||
|
"usedScene": {
|
||||||
|
"abilities": ["AppAbility"],
|
||||||
|
"when": "inuse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue