1、完整的项目初始化。
This commit is contained in:
shenzuqiang 2026-05-07 09:56:31 +08:00
commit 247e14703d
273 changed files with 15689 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
/node_modules
/oh_modules
/local.properties
/.idea
**/build
/.hvigor
.cxx
/.clangd
/.clang-format
/.clang-tidy
**/.test
/.appanalyzer

10
AppScope/app.json5 Normal file
View File

@ -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"
}
}

View File

@ -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

View File

@ -0,0 +1,7 @@
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}

3
PROXYAI.md Normal file
View File

@ -0,0 +1,3 @@
# ProxyAI Instructions
Describe goals, conventions, risky areas, and review rules here.

114
build-profile.json5 Normal file
View File

@ -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"
]
}
]
}
]
}

32
code-linter.json5 Normal file
View File

@ -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"
}
}

21
common/BuildProfile.ets Normal file
View File

@ -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;
}

3
common/Index.ets Normal file
View File

@ -0,0 +1,3 @@
export { Logger } from './src/main/ets/utils/Logger';
export { BreakpointSystem, BreakPointType } from './src/main/ets/utils/BreakpointSystem';

View File

@ -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"
}
]
}

View File

6
common/hvigorfile.ts Normal file
View File

@ -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. */
}

View File

@ -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

9
common/oh-package.json5 Normal file
View File

@ -0,0 +1,9 @@
{
"name": "@ohos/common",
"version": "0.0.1",
"description": "通用模块,用于公共页面、工具或者请求的模块。",
"main": "Index.ets",
"author": "",
"license": "Apache-2.0",
"dependencies": {}
}

View File

@ -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'

View File

@ -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'
};

View File

@ -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;

View File

@ -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);
}

View File

@ -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)
}
}

View File

@ -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) // 较大的圆角
}
}

View File

@ -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);
});
}
}

View File

@ -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; // 可选
}

View File

@ -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) // 较大的圆角
}
}

View File

@ -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 是 asyncAxios 会自动等待它执行完
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;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
export interface RequestCallback<T = Object> {
onSuccess?: (data: T) => void; // 修改这里:接收一个参数
onFailure?: (message:string) => void;
onError?: (err: Error) => void;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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' });
}

View File

@ -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 '未知版本';
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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, '')
}

View File

@ -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();
}
}
}

View File

@ -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}`)
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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%')
}
}

View File

@ -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)
}
}

View File

@ -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; // 是否可用
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
}
}
],
}
}

View File

@ -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

View File

@ -0,0 +1,5 @@
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}

View File

@ -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);
});
});
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export { FeedbackLayout } from './src/main/ets/pages/FeedbackLayout';

View File

@ -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"
}
]
}

View File

View File

@ -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. */
}

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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",
}
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export { Logger } from './src/main/ets/utils/Logger';
export { BreakpointSystem, BreakPointType } from './src/main/ets/utils/BreakpointSystem';

View File

@ -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"
}
]
}

View File

@ -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. */
}

View File

@ -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

View File

@ -0,0 +1,9 @@
{
"name": "@ohos/common",
"version": "0.0.1",
"description": "通用模块,用于公共页面、工具或者请求的模块。",
"main": "Index.ets",
"author": "",
"license": "Apache-2.0",
"dependencies": {}
}

View File

@ -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'

View File

@ -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'
};

View File

@ -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;

View File

@ -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);
}

View File

@ -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)
}
}

View File

@ -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) // 较大的圆角
}
}

View File

@ -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);
});
}
}

View File

@ -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; // 可选
}

View File

@ -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) // 较大的圆角
}
}

View File

@ -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 是 asyncAxios 会自动等待它执行完
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;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
export interface RequestCallback<T = Object> {
onSuccess?: (data: T) => void; // 修改这里:接收一个参数
onFailure?: (message:string) => void;
onError?: (err: Error) => void;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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' });
}

View File

@ -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 '未知版本';
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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, '')
}

View File

@ -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();
}
}
}

View File

@ -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}`)
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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%')
}
}

View File

@ -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)
}
}

View File

@ -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; // 是否可用
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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